APP啟動如果得到很好的優化,增強用戶體驗增加用戶流量;如果app啟動時間過長影響用戶體驗,從而會造成流失用戶。所以做啟動優化是有必須的。
- 概念:當應用在設備開機或者系統主動 kill APP 進程之後,用戶點擊桌面icon圖標啟動,稱之為冷啟動。
- 場景:開機後第一次啟動應用 或者 應用被殺死後再次啟動
- 生命周期:Process.start->Application創建->attachBaseContext->onCreate->onStart->onResume->Activity生命周期
- 啟動速度:在幾種啟動類型中最慢,也是我們優化啟動速度最大的攔路虎
- 概念:溫啟動是APP進程還存活,因為內存不足Activity被回收了,當再次啟動APP時就會重新執行Activity生命周期,布局繪製等操作。
- 場景:應用已經啟動,返回鍵退出
- 生命周期:onCreate->onStart->onResume->Activity生命周期
- 啟動速度:較快
- 概念:當後臺存在該應用的進程或者服務時,用戶點擊icon圖標啟動,稱之為熱啟動。一般是用戶按了home鍵回到桌面,或者返回鍵沒有殺進程,或者app本身做了進程重啟的機制。
- 場景:Home鍵最小化應用
- 生命周期:onResume->Activity生命周期
- 啟動速度:快
從上面的總結可以看出,在應用的啟動過程中,APP啟動所需的時間為:冷啟動時間>溫啟動時間>熱啟動時間,冷啟動是最慢最耗時的,系統以及應用本身都有大量的工作需要處理,所以,冷啟動對於應用的啟動速度是最具挑戰以及最有必要進行優化的。
冷啟動啟動流程——當點擊app的啟動圖標時,安卓系統會從Zygote進程中fork創建出一個新的進程分配給該應用,之後會依次創建和初始化Application類(attachBaseContext()–>OnCreate())、創建MainActivity類、加載主題樣式Theme中的 windowBackground等屬性設置給MainActivity以及配置Activity層級上的一些屬性、再inflate布局、當onCreate/onStart/onResume方法都走完了,最後才進行contentView的measure/layout/draw顯示在界面上,所以直到這裡,應用的第一次啟動才算完成,這時候看到的界面也就是首幀。
大致流程如下:
- 無論是通過
Launcher
來啟動Activity
,還是通過Activity
內部調用startActivity
接口來啟動新的Activity,都通過Binder進程間通信
進入到ActivityManagerService
進程中,並且調用ActivityManagerService.startActivity
接口。ActivityManagerService
調用ActivityStack.startActivityMayWait
來做準備要啟動的Activity的相關信息。ActivityStack
通知ApplicationThread
要進行Activity啟動調度了,這裡的ApplicationThread
代表的是調用ActivityManagerService.startActivity
接口的進程,對於通過點擊應用程式圖標的情景來說,這個進程就是Launcher了,而對於通過在Activity內部調用startActivity的情景來說,這個進程就是這個Activity所在的進程了。ApplicationThread
不執行真正的啟動操作,它通過調用ActivityManagerService.activityPaused
接口進入到ActivityManagerService
進程中,看看是否需要創建新的進程來啟動Activity。- 對於通過點擊應用程式圖標來啟動Activity的情景來說,
ActivityManagerService
在這一步中,會調用startProcessLocked
來創建一個新的進程,而對於通過在Activity內部調用startActivity來啟動新的Activity來說,這一步是不需要執行的,因為新的Activity就在原來的Activity所在的進程中進行啟動。ActivityManagerServic
調用ApplicationThread.scheduleLaunchActivity
接口,通知相應的進程執行啟動Activity的操作。ApplicationThread
把這個啟動Activity的操作轉發給ActivityThread
,ActivityThread
通過ClassLoader導入相應的Activity類,然後把它啟動起來。
所以相應的優化方案:
- 減少 onCreate方法的工作量。
- 不要讓Application參與業務邏輯。
- 不要在Application中做耗時操作,一些初始化操作可以開啟子線程來完成。
- 不要以靜態變量方式在Application中保存數據。
- 布局優化/mainThread儘量延遲初始化。
- 啟動畫面的初始化可以使用設置主題背景的方式,速度回更快。
冷啟動指的是應用程式從進程在系統不存在,到系統創建應用運行進程空間的過程。冷啟動通常會發生在一下兩種情況:
在冷啟動的最開始,系統需要負責做三件事:
- 加載以及啟動app
- app啟動之後立刻顯示一個空白的預覽窗口
- 創建app進程
一旦系統完成創建app進程後,app進程將要接著負責完成下面的工作:
- 創建Application對象
- 創建並且啟動主線程ActivityThread
- 創建啟動第一個Activity
- Inflating views
- 布局屏幕
- 執行第一次繪製
一旦app進程完完成了第一次繪製工作,系統進程就會用main activity替換前面顯示的預覽窗口,這個時候,用戶就可以正式開始與app進行交互了。
從冷啟動的流程看,我們無法幹預app進程創建等系統操作,我們能夠幹預的有:
- 預覽窗口
- Application生命周期回調
- Activity生命周期回調
對研發人員來說,啟動速度是我們的「門面」,它清清楚楚可以被所有人看到,我們都希望自己應用的啟動速度可以秒殺所有競爭對手。
「工欲善其事必先利其器」,我們需要先找到一款適合做啟動優化分析的工具或者方式。
adb shell am start -W [packageName]/[ packageName. AppstartActivity]
在統計 app 啟動時間時,系統為我們提供了 adb 命令,可以輸出啟動時間。系統在繪製完成後,ActivityManagerService 會回調該方法,但是能夠方便我們通過腳本多次啟動測量 TotalTime,對比版本間啟動時間差異。但是統計時間不如 Systrace 準確。
代碼埋點
通過代碼埋點來準確獲取記錄每個方法的執行時間,知道哪些地方耗時,然後再有針對性地優化。例如通過在 app 啟動生命周期中,關鍵位置加入時間點記錄,達到測量目的;又例如可以在 Application 的 attachBaseContext方法中記錄開始時間,然後在啟動的第一個 Activity 的 onWindowFocusChanged方法記錄結束時間。但是從用戶點擊 app Icon 到 Application 被創建,再到 Activity 的渲染,中間還是有很多步驟的,比如冷啟動的進程創建過程,而這個時間用此版本是沒辦法統計了,必須得承受這點數據的不準確性。
Nanoscope
Nanoscope 非常真實,不過暫時只支持 Nexus 6 和 x86 模擬器。
Simpleperf
Simpleperf 的火焰圖並不適合做啟動流程分析。
TraceView
通過 TraceView 主要可以得到兩種數據:單次執行耗時的方法 以及 執行次數多的方法。但是TraceView 性能耗損太大,不能比較正確反映真實情況。
Systrace
Systrace 能夠追蹤關鍵系統調用的耗時情況,如系統的 IO 操作、內核工作隊列、CPU 負載、Surface 渲染、GC 事件以及 Android 各個子系統的運行狀況等。但是不支持應用程式代碼的耗時分析。綜上所述,這幾種方式都各有各的優點以及缺點,我們都要掌握。
在拿到整個啟動流程的全景圖之後,我們可以清楚地看到這段時間內系統、應用各個進程和線程的運行情況,現在我們要開始真正開始「幹活」了。具體的優化方式,我把它們分為預覽窗口優化、業務梳理、業務優化、多進程優化、線程優化、GC 優化和系統調用優化、布局優化。
當用戶點擊應用桌面圖標啟動應用的時候,利用提前展示出來的 Window,快速展示出一個界面,用戶只需要很短的時間就可以看到「預覽頁」,這種完全「跟手」的感覺在高端機上體驗非常好,但對於中低端機,會把總的的閃屏時間變得更長。
如果點擊圖標沒有響應,用戶主觀上會認為是手機系統響應比較慢。所以比較推薦的做法是,只在 Android 6.0 或者 Android 7.0 以上才啟用「預覽窗口」方案,讓手機性能好的用戶可以有更好的體驗。
要實現預覽窗口的顯示,只需要在利用 activity 的windowBackground主題屬性提供一個簡單的自定義 drawable 給啟動的 activity,如下:
Layout XML file:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque"> <!-- The background color, preferably the same as your normal theme --> <item android:drawable="@android:color/white"/> <!-- Your product logo - 144dp color version of your app icon --> <item> <bitmap android:src="@drawable/product_logo_144dp" android:gravity="center"/> </item></layer-list>
Manifest file:
<activity ...android:theme="@style/AppTheme.Launcher" />
這樣一個 activity 啟動的時候,就會先顯示一個預覽窗口,給用戶快速響應的體驗。當 activity想要恢復原來 theme,可以通過在調用super.onCreate() 和setContentView()之前調用 setTheme(R.style.AppTheme),如下:
public class MyMainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { setTheme(R.style.Theme_MyApp); super.onCreate(savedInstanceState); }}
不要一股腦把全部初始化工作放在 Application 中做,需要梳理清楚當前啟動過程正在運行的每一個模塊,哪些是一定需要的、哪些可以砍掉、哪些可以懶加載。但是需要注意的是,懶加載要防止集中化,否則容易出現首頁顯示後用戶無法操作的情形。總的來說,用以下四個維度分整理啟動的各個點:
- 必要且耗時:啟動初始化,考慮用線程來初始化。
- 必要不耗時:首頁繪製。
- 非必要但耗時:數據上報、插件初始化。
- 非必要不耗時:不用想,這塊直接去掉,在需要用的時再加載。
把數據整理出來後,按需實現加載邏輯,採取分步加載、異步加載、延期加載策略,如下圖所示:
一句話概述,要提高應用的啟動速度,核心思想是在啟動過程中少做事情,越少越好。
通過梳理之後,剩下的都是啟動過程一定要用的模塊。這個時候,我們只能硬著頭皮去做進一步的優化。優化前期需要「抓大放小」,先看看主線程究竟慢在哪裡。最理想是通過算法進行優化,例如一個數據解密操作需要 1 秒,通過算法優化之後變成 10 毫秒。退而求其次,我們要考慮這些任務是不是可以通過異步線程預加載實現,但需要注意的是過多的線程預加載會讓我們的邏輯變得更加複雜。
業務優化做到後面,會發現一些架構和歷史包袱會拖累我們前進的步伐。比較常見的是一些事件會被各個業務模塊監聽,大量的回調導致很多工作集中執行,部分框架初始化「太厚」,例如一些插件化框架,啟動過程各種反射、各種 Hook,整個耗時至少幾百毫秒。還有一些歷史包袱非常沉重,而且「牽一髮動全身」,改動風險比較大。但是我想說,如果有合適的時機,我們依然需要勇敢去償還這些「歷史債務」。
Android app 是支持多進程的,在 Manifest 中只要在組件聲明中加入android:process屬性就可以讓組件在啟動時運行在不同的進程中。舉個例子: 對於多進程 app ,可能擁有主進程,插件進程以及下載進程,但開發者只能在 Manifest 中聲明一個 Application 組件,如果對應不同進程的組件啟動時,系統會創建三個進程,創建三個 Application 對象,同時attachBaseContext、onCreate等生命周期回調方法也會被調用三次。
但是每個進程需要初始化的內容肯定是不一樣的,所以,為了防止資源的浪費,我們需要在Application 中區分進程,對應進程只初始化對應的內容。
線程優化分兩方面:
第一,耗時任務異步化。子線程處理耗時任務,主線程做的事情越少,越早進入Acitivity繪製階段,界面越早展現。例如不在主線程做如 IO 、網絡等耗時操作。但是要注意,子線程不能阻塞主線程。
第二,線程池管理線程,控制線程的數量。線程數量太多會相互競爭 CPU 資源,導致分給主線程的時間片減少,從而導致啟動速度變慢。線程切換的數據我們可以通過卡頓優化中學到的 sched 文件查看,這裡特別需要注意 nr_involuntary_switches 被動切換的次數。
proc/[pid]/sched: nr_voluntary_switches:主動上下文切換次數,因為線程無法獲取所需資源導致上下文切換,最普遍的是 IO。 nr_involuntary_switches:被動上下文切換次數,線程被系統強制調度導致上下文切換,例如大量線程在搶佔 CPU。
第三,避免主線程與子線程之間的鎖阻塞等待。有一次我們把主線程內的一個耗時任務放到線程中並發執行,但是發現這樣做根本沒起作用。仔細檢查後發現線程內部會持有一個鎖,主線程很快就有其他任務因為這個鎖而等待。通過 Systrace 可以看到鎖等待的事件,我們需要排查這些等待是否可以優化,特別是防止主線程出現長時間的空轉。
特別是現在有很多啟動框架,會使用 Pipeline 機制,根據業務優先級規定業務初始化時機。比如微信內部使用的 mmkernel 、阿里最近開源的 Alpha 啟動框架,它們為各個任務建立依賴關係,最終構成一個有向無環圖。對於可以並發的任務,會通過線程池最大程度提升啟動速度。如果任務的依賴關係沒有配置好,很容易出現下圖這種情況,即主線程會一直等待 taskC 結束,空轉 2950 毫秒。
在啟動過程,要儘量減少 GC 的次數,避免造成主線程長時間的卡頓,特別是對 Dalvik 來說,我們可以通過 Systrace 單獨查看整個啟動過程 GC 的時間。
啟動過程避免進行大量的字符串操作,特別是序列化跟反序列化過程。一些頻繁創建的對象,例如網絡庫和圖片庫中的 Byte 數組、Buffer 可以復用。如果一些模塊實在需要頻繁創建對象,可以考慮移到 Native 實現。
Java 對象的逃逸也很容易引起 GC 問題,我們在寫代碼的時候比較容易忽略這個點。我們應該保證對象生命周期儘量的短,在棧上就進行銷毀。
部分系統的API使用是阻塞性的,文件很小可能無法感知,當文件過大,或者使用頻繁時,可能造成阻塞。例如:SharedPreference.Editor 的提交操作建議使用異步的 apply,而不是阻塞的 commit。
通過 systrace 的 System Service 類型,我們可以看到啟動過程 System Server 的CPU 工作情況。在啟動過程,我們儘量不要做系統調用,例如 PackageManagerService 操作、Binder 調用等待。
在啟動過程也不要過早地拉起應用的其他進程,System Server 和新的進程都會競爭 CPU 資源。特別是系統內存不足的時候,當我們拉起一個新的進程,可能會成為「壓死駱駝的最後一根稻草」。它可能會觸發系統的 low memorykiller 機制,導致系統殺死和拉起(保活)大量的進程,從而影響前臺進程的 CPU。舉個例子,之前一個程序在啟動過程會拉起下載和視頻播放進程,改為按需拉起後,線上啟動時間提高了 3%,對於 1GB 以下的低端機優化,整個啟動時間可以優化 5%~8%,效果還是非常明顯的。
布局越複雜,測量布局繪製的時間就越長。主要做到以下幾點:
- 布局的層級越少,加載速度越快。
- 一個控制項的屬性越少,解析越快,刪除控制項中的無用屬性。
- 使用< ViewStub/>標籤加載一些不常用的布局,做到使用時在加載。
- 使用 < merge/>標籤減少布局的嵌套層次。
- 儘可能少用wrap_content,wrap_content會增加布局measure時的計算成本,已知寬高為固定值時,不用wrap_content。
啟動優化,是一項長期的任務,任重而道遠。
開發者要未雨綢繆,在編碼過程中儘量減少給啟動帶來性能損耗的工作,主要注意以下幾個事項:
- 儘量避免啟動時在主線程做密集繁重的工作,如:避免 I/O 操作、反序列化、網絡操作、鎖等待等。
- 對模塊以及第三方庫按需加載,採取分步加載、異步加載、延期加載等策略。
- 利用線程池管理線程,避免創建大量線程,造成 CPU 競爭,導致主線程時間片減少。
- 啟動過程中,儘量避免頻繁創建的大量對象,減少 GC 給啟動性能帶來的卡頓影響。
- 儘量避免在啟動過程中調用阻塞性的系統調用。