序言
應用啟動是整個app工序的第一道流程。對于開發者,一般需要在應用啟動過程中進行初始化工作,啟動頁的UI展示。而對于用戶來說,啟動速度的快慢則極大地影響了使用體驗,并且間接地影響了用戶的留存率(對于某些追求流暢體驗的用戶,一個啟動緩慢的app會造成卡頓遲滯的印象,被直接劃掉也不是不可能),下圖是經過優化的沃家視頻啟動頁.
工欲善其事,必先利其器。如果想要對app的啟動過程進行優化,那么首先就要了解app的啟動過程和常用的優化工具,下面就結合安卓官方文檔,以及一些我在預研過程中閱讀的優質文章,來總結下app的三種啟動過程.
冷啟動
冷啟動代表app從運存數據完全被擦除的狀態啟動啟動的過程,在此之前,app所屬的進程還未被創建.冷啟動一般發生在系統重啟后或者app被系統殺死后app首次被啟動,
冷啟動分為以下三個步驟:
- 加載并啟動app
- 啟動后展示系統配置的空白Window
- 創建app進程
在創建完app進程后,則會進行下面幾個步驟:
- 創建app用到的對象
- 啟動主線程(UI線程)
- 創建app的main activity
- 加載activity的view
- 布局屏幕
- 完成首幀的繪制
而一旦完成首幀的繪制后,系統會將當前展示的background-window換出,替換為main-activity的背景。從這個時間點開始,用戶就可以開始使用app了.
兩個重要的時間點
第一個需要注意的時間點是,當我們點擊launcher上的應用圖標后,首先出現的是系統繪制的window的默認背景,根據app使用的不同theme.這個默認背景是白色或黑色的空白屏幕,在性能較好的機器上,這個默認背景可能會一晃而過,但在某些性能較差的機器或者機器卡頓的情況下會導致白屏停留時間過長,所以啟動屏的默認背景是一個需要優化的點,后面會詳細總結如何優化這個點.
第二個需要注意的地方是首幀的繪制時機,實際上我們知道在包括 Application.onCreate() ,Activity.onCreate() ,Activity.onResume() 等生命周期回掉函數執行時,view的布局和繪制都還沒有開始,在這些生命周期回調函數中,如果對一些短小的操作耗時操作做異步處理,很可能造成負優化的效果(線程切換增加耗時,實際上沒有延時的效果),最好的做法應該是在首幀繪制完成的前后異步處理耗時的邏輯,具體的做法后面總結.
冷啟動的總結
通過上面這張官方文檔提供的流程圖可以看出,冷啟動經過了 Application的創建,Activity的創建,首幀的繪制 等過程,截至到 Displayed-Time這個時間點完成了上述步驟,后面的Other Stuff 步驟可以看做是開發者自定義的一些初始化操作,如果要在log中查看這個時間點,可以通過reportFullyDrawn()這個函數來上報.
熱啟動
應用程序的熱啟動要比冷啟動簡單,消耗也更少,熱啟動的常見場景就是app的前后臺切換.在從后臺切換到前臺的過程中,如果應用程序的activities還駐留在內存中,app就不需要再重復經歷對象初始化,布局加載和渲染這些步驟.
但是,如果某些內存因為內存整理(比如說onTrimMemory())而導致被清理,那么在響應熱啟動事件時這些被清理的對象就需要重新創建.
熱啟動和冷啟動在屏幕表現上一致,在app完成activity的渲染之前都會一直展示空白屏幕.
溫啟動
溫啟動這個名詞平時不常見到,官方文檔中是這樣解釋的:溫啟動包含了冷啟動的一部分操作集,同時它的消耗要比冷啟動要少.溫啟動的常見場景如下:
- 用戶退出app后重新進入(很多app會在退出時重新啟動一個新的實例做到常駐,這里只討論部不常駐的場景)。當app退出后,進程有可能仍在運行,這時候如果重新啟動app,那么activity必須要從onCreate()生命周期開始重新創建.
- 系統干掉了駐留內存的app。這時如果重新啟動app,那么進程和activity都是需要重新創建,但onCreate() 會傳入 saveInstance,通過使用saveInstance可以節省耗時.總的來說溫啟動在耗時上介于冷啟動和熱啟動之間.
統計應用啟動時間
總結了應用啟動的幾種狀態,接著總結如何統計應用啟動的時間,因為應用啟動的時間一般很短,需要有精確的統計時間來支撐優化結果.
而統計應用啟動時間我們可以通過查看日志來獲取相關信息:
查看logcat日志
上圖是查看logcat中 TAG為 ActivityManager的日志得到的啟動時間信息.可以看到從冷啟動狀態打開知乎客戶端一共耗費了 +1s627ms(知乎客戶端啟動頁出現白屏的情況,感覺不應該出現這種情況).
adb shell am start
通過adb命令我們可以更加得到更加詳細的數據.
使用
adb shell am start -W -S [packageName]/[ActivityPath]
- -W 列出啟動過程中統計到的具體數據
- -S 強制停止當前的Activity,重新啟動
使用這個adb命令,我們可以得到較為詳盡的啟動統計數據,仍以知乎客戶端為例子.
adb shell am start -W -S com.zhihu.android/.app.ui.activity.MainActivity
Stopping: com.zhihu.android
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.zhihu.android/.app.ui.activity.MainActivity }
Status: ok
Activity: com.zhihu.android/.app.ui.activity.MainActivity
ThisTime: 1766
TotalTime: 1766
WaitTime: 1780
Complete
具體的啟動數據如上所示,這段貼出的adb命令得到的數據和上方截圖中的數據來自相同的一次測試,通過對比可以發現,通過adb shell得到的ThisTime這個值與log信息中的DisplayedTime是相同的.但是我們可以看到有三個時間數據: ThisTime TotalTime WaitTime。那么其它兩個時間字段分別代表什么含義呢?
ThisTime TotalTime WaitTime 各自的含義
講解著三個時間段的含義,不得不提到我看到的一篇質量很高的博文,博主長期負責FrameWork層的性能優化工作,對于啟動優化理解很深,我試著依樣畫葫蘆在心里講解了一遍,發現有很多很深的點我get不到,這里就先貼出博文,不再重復分析了.
http://androidperformance.com/2015/12/31/How-to-calculation-android-app-lunch-time.html
這里羅列下最后的結論(圖源為上方博文):
- 在第①個時間段內,AMS 創建 ActivityRecord 記錄塊和選擇合理的 Task、將當前Resume 的 Activity 進行 pause
- 在第②個時間段內,啟動進程、調用無界面 Activity 的 onCreate() 等、 pause/finish 無界面的 Activity
- 在第③個時間段內,調用有界面 Activity 的 onCreate、onResume
所以,這三個啟動時間間的關系為:
- WaitTime 就是總的耗時,包括前一個應用 Activity pause 的時間和新應用啟動的時間;
- ThisTime 表示一連串啟動 Activity 的最后一個 Activity 的啟動耗時;
- TotalTime 表示新應用啟動的耗時,包括新進程的啟動和 Activity 的啟動,但不包括前一個應用 Activity pause 的耗時。也就是說,開發者一般只要關心 TotalTime 即可,這個時間才是自己應用真正啟動的耗時。
總之,TotalTime是我們在開發過程中應該改關注的一個時間值,它基本上可以與應用的冷啟動過程掛鉤。
如何優化應用的啟動時間
總結了統計app啟動時間的方法后,就需要思考如何優化app啟動時間了.我的思路是:優化app啟動實際上就是優化冷啟動過程,因為冷啟動過程包含了啟動過程中的每一步.而從上面啟動概念總結和統計方法的總結也可以看出,優化應該分為兩步:優化進程創建過程的耗時(在應用層面就是優化Application的生命周期函數內的耗時操作)以及優化 Activity的創建過程
優化Applicatoin.onCreate()中的耗時操作
一般在Applicatoin的創建過程中,都會做一些初始化的工作,例如:MultiDex.install(),AppManager.init()(這里是偽代碼的形式,泛指各種管理類的初始化工作)。在這些初始化的操作中,難免會包含一些文件讀寫,數據庫的增刪改查,參數配置,等等耗時操作,優化這部分邏輯我的思路是:通過延時和異步來解決.
- 例如Manager.init() ,這種全局管理類的初始化操作有時候可以懶加載的,就是說通過單例模式結合懶加載在我們需要用到相關類的時候在進行初始化,而不是一股腦的全部放在Application.onCreate()中.
- 通過異步的方式.具體來說就是將不是特別緊急的耗時操作放在低優先級的工作線程中來異步進行,或者在后臺service中開啟線程來進行.但是這樣做有一個特別需要注意的地方,對于那些短耗時的操作來說,這么做其實很有可能造成負優化的效果,因為我們的最終目的是讓應用的首幀畫面盡快的加載出來,用戶能夠迅速地進入到MainActivity當中,但是不加考慮的將短耗時操作放入異步線程中,這些工作線程很有可能在MainActivity進行布局和渲染前就已經完成了自身使命,那么這樣來看布局初始化前的絕對時間并沒有減少,相反因為建立線程和切換線程等開銷,造成了更多的時間消耗,所以這是一個特別需要注意的地方.
Application的優化工作大概就這些,當然因為各個app復雜程度不同,業務類型不盡相同,具體的優化操作肯定會比較深入,這里我這是按照我在工作中進行的優化工作進行的自我總結,關于我的優化過程重點也不在這里。
Activity創建過程的優化
在這次對app的優化過程中,效果最為明顯的操作是對啟動頁(SplashActivity)的優化。通過上面的基礎概念的總結可以得出結論:在Activity的 onCreate() 和 onResume() 生命周期中, app的第一個frame其實都還沒有繪制出來.但因為Activity的創建過程和調用鏈相當長,這里先不總結Activity的創建流程.
只需要先知道Activity的布局和渲染過程其實是在 onResume執行之后才開始的.Activity在AMS置于resume狀態后,Activity所屬的Window會通過WindowManagerImpl,addView()將decorView放入到 ViewRoot中,然后ViewRoot發起 traversal遍歷整個view樹,進行布局和渲染,最終將畫面繪制到屏幕上.
所以說在不影響業務流程的情況下,為了盡快將app的首幀繪制到屏幕上,我們最后將啟動頁的耗時操作(例如下載廣告圖)放在首幀繪制完成之后進行.
那么問題來了,如何盡可能捕捉到應用完成首幀繪制的時間點呢?
在我的另外一篇總結 GPU-Rendering Profile分析中分析了android是如何將view繪制到屏幕上的,基本流程就是 Measure - Layout - Draw - GPU渲染,所以可以選擇hook這幾個時間點,選擇合適的時機插入耗時操作,使得耗時操作可以盡量延遲執行.
- 首先列出未優化前的代碼,直接在onResume方法中啟動耗時操作.
@Override
protected void onResume() {
super.onResume();
getWindow().getDecorView().post(mGetSplashDataRunnable);
- 優化方案選擇在ViewTreeObserver.onGlobalLayoutListener()的回調中插入耗時操作.
getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
getWindow().getDecorView().post(mGetSplashDataRunnable);
Log.i(Constants.TAG.TAG_TEST_LAUNCH_TIME, "onGlobalLayout");
}
});
然后列出兩種方案的啟動耗時測試,發現使用優化方案的平均啟動時間要減少80ms左右.
自定義默認窗口背景,優化用戶體驗
雖然通延時加載耗時任務能夠在一定程度上加快app首幀的顯示速度,但是縮短的80ms的啟動時間相比較總的耗時(520ms左右)來說,用戶體驗依舊沒有得到明顯地提升。
前面總結到,點擊桌面上的應用圖標,首先為我們展示的是一個空白的默認啟動背景,為了避免白屏的出現而影響體驗,最有效的方法還是需要修改默認的空白背景,替換為app的默認啟動頁畫面.
具體做法為新建一個 shape.xml,背景使用app的默認啟動圖
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:opacity="opaque">
<item android:drawable="@color/white" />
<item
android:drawable="@drawable/default_splash_ad"
android:gravity="fill" />
</layer-list>
然后建立一個新的主題,并將該主題設為啟動頁Activity的background。
<style name="AppSplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:background">@drawable/shape_launch</item>
</style>
這樣就從根本上解決了啟動頁會產生白屏的情況,當然對于啟動速度的優化仍然是必要的,否則長時間卡在啟動界面,即使沒有了白屏情況,用戶體驗也會很差.
以上。