-
序言
-
三種啟動(dòng)狀態(tài)及啟動(dòng)過程
-
統(tǒng)計(jì)應(yīng)用啟動(dòng)時(shí)間
-
如何優(yōu)化應(yīng)用啟動(dòng)的時(shí)間
序言
應(yīng)用啟動(dòng)是整個(gè)app工序的第一道流程。對(duì)于開發(fā)者,一般需要在應(yīng)用啟動(dòng)過程中進(jìn)行初始化工作,啟動(dòng)頁的UI展示。而對(duì)于用戶來說,啟動(dòng)速度的快慢則極大地影響了使用體驗(yàn),并且間接地影響了用戶的留存率(對(duì)于某些追求流暢體驗(yàn)的用戶,一個(gè)啟動(dòng)緩慢的app會(huì)造成卡頓遲滯的印象,被直接劃掉也不是不可能),下圖是經(jīng)過優(yōu)化的沃家視頻啟動(dòng)頁.

工欲善其事,必先利其器。如果想要對(duì)app的啟動(dòng)過程進(jìn)行優(yōu)化,那么首先就要了解app的啟動(dòng)過程和常用的優(yōu)化工具,下面就結(jié)合安卓官方文檔,以及一些我在預(yù)研過程中閱讀的優(yōu)質(zhì)文章,來總結(jié)下app的三種啟動(dòng)過程.
冷啟動(dòng)
冷啟動(dòng)代表app從運(yùn)存數(shù)據(jù)完全被擦除的狀態(tài)啟動(dòng)啟動(dòng)的過程,在此之前,app所屬的進(jìn)程還未被創(chuàng)建.冷啟動(dòng)一般發(fā)生在系統(tǒng)重啟后或者app被系統(tǒng)殺死后app首次被啟動(dòng),
冷啟動(dòng)分為以下三個(gè)步驟:
- 加載并啟動(dòng)app
- 啟動(dòng)后展示系統(tǒng)配置的空白Window
- 創(chuàng)建app進(jìn)程
在創(chuàng)建完app進(jìn)程后,則會(huì)進(jìn)行下面幾個(gè)步驟:
- 創(chuàng)建app用到的對(duì)象
- 啟動(dòng)主線程(UI線程)
- 創(chuàng)建app的main activity
- 加載activity的view
- 布局屏幕
- 完成首幀的繪制
而一旦完成首幀的繪制后,系統(tǒng)會(huì)將當(dāng)前展示的background-window換出,替換為main-activity的背景。從這個(gè)時(shí)間點(diǎn)開始,用戶就可以開始使用app了.
兩個(gè)重要的時(shí)間點(diǎn)
第一個(gè)需要注意的時(shí)間點(diǎn)是,當(dāng)我們點(diǎn)擊launcher上的應(yīng)用圖標(biāo)后,首先出現(xiàn)的是系統(tǒng)繪制的window的默認(rèn)背景,根據(jù)app使用的不同theme.這個(gè)默認(rèn)背景是白色或黑色的空白屏幕,在性能較好的機(jī)器上,這個(gè)默認(rèn)背景可能會(huì)一晃而過,但在某些性能較差的機(jī)器或者機(jī)器卡頓的情況下會(huì)導(dǎo)致白屏停留時(shí)間過長,所以啟動(dòng)屏的默認(rèn)背景是一個(gè)需要優(yōu)化的點(diǎn),后面會(huì)詳細(xì)總結(jié)如何優(yōu)化這個(gè)點(diǎn).
第二個(gè)需要注意的地方是首幀的繪制時(shí)機(jī),實(shí)際上我們知道在包括 Application.onCreate() ,Activity.onCreate() ,Activity.onResume() 等生命周期回掉函數(shù)執(zhí)行時(shí),view的布局和繪制都還沒有開始,在這些生命周期回調(diào)函數(shù)中,如果對(duì)一些短小的操作耗時(shí)操作做異步處理,很可能造成負(fù)優(yōu)化的效果(線程切換增加耗時(shí),實(shí)際上沒有延時(shí)的效果),最好的做法應(yīng)該是在首幀繪制完成的前后異步處理耗時(shí)的邏輯,具體的做法后面總結(jié).
冷啟動(dòng)的總結(jié)

通過上面這張官方文檔提供的流程圖可以看出,冷啟動(dòng)經(jīng)過了 Application的創(chuàng)建,Activity的創(chuàng)建,首幀的繪制 等過程,截至到 Displayed-Time這個(gè)時(shí)間點(diǎn)完成了上述步驟,后面的Other Stuff 步驟可以看做是開發(fā)者自定義的一些初始化操作,如果要在log中查看這個(gè)時(shí)間點(diǎn),可以通過reportFullyDrawn()這個(gè)函數(shù)來上報(bào).
熱啟動(dòng)
應(yīng)用程序的熱啟動(dòng)要比冷啟動(dòng)簡(jiǎn)單,消耗也更少,熱啟動(dòng)的常見場(chǎng)景就是app的前后臺(tái)切換.在從后臺(tái)切換到前臺(tái)的過程中,如果應(yīng)用程序的activities還駐留在內(nèi)存中,app就不需要再重復(fù)經(jīng)歷對(duì)象初始化,布局加載和渲染這些步驟.
但是,如果某些內(nèi)存因?yàn)閮?nèi)存整理(比如說onTrimMemory())而導(dǎo)致被清理,那么在響應(yīng)熱啟動(dòng)事件時(shí)這些被清理的對(duì)象就需要重新創(chuàng)建.
熱啟動(dòng)和冷啟動(dòng)在屏幕表現(xiàn)上一致,在app完成activity的渲染之前都會(huì)一直展示空白屏幕.
溫啟動(dòng)
溫啟動(dòng)這個(gè)名詞平時(shí)不常見到,官方文檔中是這樣解釋的:溫啟動(dòng)包含了冷啟動(dòng)的一部分操作集,同時(shí)它的消耗要比冷啟動(dòng)要少.溫啟動(dòng)的常見場(chǎng)景如下:
- 用戶退出app后重新進(jìn)入(很多app會(huì)在退出時(shí)重新啟動(dòng)一個(gè)新的實(shí)例做到常駐,這里只討論部不常駐的場(chǎng)景)。當(dāng)app退出后,進(jìn)程有可能仍在運(yùn)行,這時(shí)候如果重新啟動(dòng)app,那么activity必須要從onCreate()生命周期開始重新創(chuàng)建.
- 系統(tǒng)干掉了駐留內(nèi)存的app。這時(shí)如果重新啟動(dòng)app,那么進(jìn)程和activity都是需要重新創(chuàng)建,但onCreate() 會(huì)傳入 saveInstance,通過使用saveInstance可以節(jié)省耗時(shí).總的來說溫啟動(dòng)在耗時(shí)上介于冷啟動(dòng)和熱啟動(dòng)之間.
統(tǒng)計(jì)應(yīng)用啟動(dòng)時(shí)間
總結(jié)了應(yīng)用啟動(dòng)的幾種狀態(tài),接著總結(jié)如何統(tǒng)計(jì)應(yīng)用啟動(dòng)的時(shí)間,因?yàn)閼?yīng)用啟動(dòng)的時(shí)間一般很短,需要有精確的統(tǒng)計(jì)時(shí)間來支撐優(yōu)化結(jié)果.
而統(tǒng)計(jì)應(yīng)用啟動(dòng)時(shí)間我們可以通過查看日志來獲取相關(guān)信息:
查看logcat日志

上圖是查看logcat中 TAG為 ActivityManager的日志得到的啟動(dòng)時(shí)間信息.可以看到從冷啟動(dòng)狀態(tài)打開知乎客戶端一共耗費(fèi)了 +1s627ms(知乎客戶端啟動(dòng)頁出現(xiàn)白屏的情況,感覺不應(yīng)該出現(xiàn)這種情況).
adb shell am start
通過adb命令我們可以更加得到更加詳細(xì)的數(shù)據(jù).
使用
adb shell am start -W -S [packageName]/[ActivityPath]
- -W 列出啟動(dòng)過程中統(tǒng)計(jì)到的具體數(shù)據(jù)
- -S 強(qiáng)制停止當(dāng)前的Activity,重新啟動(dòng)
使用這個(gè)adb命令,我們可以得到較為詳盡的啟動(dòng)統(tǒng)計(jì)數(shù)據(jù),仍以知乎客戶端為例子.
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
具體的啟動(dòng)數(shù)據(jù)如上所示,這段貼出的adb命令得到的數(shù)據(jù)和上方截圖中的數(shù)據(jù)來自相同的一次測(cè)試,通過對(duì)比可以發(fā)現(xiàn),通過adb shell得到的ThisTime這個(gè)值與log信息中的DisplayedTime是相同的.但是我們可以看到有三個(gè)時(shí)間數(shù)據(jù): ThisTime TotalTime WaitTime。那么其它兩個(gè)時(shí)間字段分別代表什么含義呢?
ThisTime TotalTime WaitTime 各自的含義
講解著三個(gè)時(shí)間段的含義,不得不提到我看到的一篇質(zhì)量很高的博文,博主長期負(fù)責(zé)FrameWork層的性能優(yōu)化工作,對(duì)于啟動(dòng)優(yōu)化理解很深,我試著依樣畫葫蘆在心里講解了一遍,發(fā)現(xiàn)有很多很深的點(diǎn)我get不到,這里就先貼出博文,不再重復(fù)分析了.
http://androidperformance.com/2015/12/31/How-to-calculation-android-app-lunch-time.html
這里羅列下最后的結(jié)論(圖源為上方博文):

- 在第①個(gè)時(shí)間段內(nèi),AMS 創(chuàng)建 ActivityRecord 記錄塊和選擇合理的 Task、將當(dāng)前Resume 的 Activity 進(jìn)行 pause
- 在第②個(gè)時(shí)間段內(nèi),啟動(dòng)進(jìn)程、調(diào)用無界面 Activity 的 onCreate() 等、 pause/finish 無界面的 Activity
- 在第③個(gè)時(shí)間段內(nèi),調(diào)用有界面 Activity 的 onCreate、onResume
所以,這三個(gè)啟動(dòng)時(shí)間間的關(guān)系為:
- WaitTime 就是總的耗時(shí),包括前一個(gè)應(yīng)用 Activity pause 的時(shí)間和新應(yīng)用啟動(dòng)的時(shí)間;
- ThisTime 表示一連串啟動(dòng) Activity 的最后一個(gè) Activity 的啟動(dòng)耗時(shí);
- TotalTime 表示新應(yīng)用啟動(dòng)的耗時(shí),包括新進(jìn)程的啟動(dòng)和 Activity 的啟動(dòng),但不包括前一個(gè)應(yīng)用 Activity pause 的耗時(shí)。也就是說,開發(fā)者一般只要關(guān)心 TotalTime 即可,這個(gè)時(shí)間才是自己應(yīng)用真正啟動(dòng)的耗時(shí)。
總之,TotalTime是我們?cè)陂_發(fā)過程中應(yīng)該改關(guān)注的一個(gè)時(shí)間值,它基本上可以與應(yīng)用的冷啟動(dòng)過程掛鉤。
如何優(yōu)化應(yīng)用的啟動(dòng)時(shí)間
總結(jié)了統(tǒng)計(jì)app啟動(dòng)時(shí)間的方法后,就需要思考如何優(yōu)化app啟動(dòng)時(shí)間了.我的思路是:優(yōu)化app啟動(dòng)實(shí)際上就是優(yōu)化冷啟動(dòng)過程,因?yàn)槔鋯?dòng)過程包含了啟動(dòng)過程中的每一步.而從上面啟動(dòng)概念總結(jié)和統(tǒng)計(jì)方法的總結(jié)也可以看出,優(yōu)化應(yīng)該分為兩步:優(yōu)化進(jìn)程創(chuàng)建過程的耗時(shí)(在應(yīng)用層面就是優(yōu)化Application的生命周期函數(shù)內(nèi)的耗時(shí)操作)以及優(yōu)化 Activity的創(chuàng)建過程
優(yōu)化Applicatoin.onCreate()中的耗時(shí)操作
一般在Applicatoin的創(chuàng)建過程中,都會(huì)做一些初始化的工作,例如:MultiDex.install(),AppManager.init()(這里是偽代碼的形式,泛指各種管理類的初始化工作)。在這些初始化的操作中,難免會(huì)包含一些文件讀寫,數(shù)據(jù)庫的增刪改查,參數(shù)配置,等等耗時(shí)操作,優(yōu)化這部分邏輯我的思路是:通過延時(shí)和異步來解決.
- 例如Manager.init() ,這種全局管理類的初始化操作有時(shí)候可以懶加載的,就是說通過單例模式結(jié)合懶加載在我們需要用到相關(guān)類的時(shí)候在進(jìn)行初始化,而不是一股腦的全部放在Application.onCreate()中.
- 通過異步的方式.具體來說就是將不是特別緊急的耗時(shí)操作放在低優(yōu)先級(jí)的工作線程中來異步進(jìn)行,或者在后臺(tái)service中開啟線程來進(jìn)行.但是這樣做有一個(gè)特別需要注意的地方,對(duì)于那些短耗時(shí)的操作來說,這么做其實(shí)很有可能造成負(fù)優(yōu)化的效果,因?yàn)槲覀兊淖罱K目的是讓應(yīng)用的首幀畫面盡快的加載出來,用戶能夠迅速地進(jìn)入到MainActivity當(dāng)中,但是不加考慮的將短耗時(shí)操作放入異步線程中,這些工作線程很有可能在MainActivity進(jìn)行布局和渲染前就已經(jīng)完成了自身使命,那么這樣來看布局初始化前的絕對(duì)時(shí)間并沒有減少,相反因?yàn)榻⒕€程和切換線程等開銷,造成了更多的時(shí)間消耗,所以這是一個(gè)特別需要注意的地方.
Application的優(yōu)化工作大概就這些,當(dāng)然因?yàn)楦鱾€(gè)app復(fù)雜程度不同,業(yè)務(wù)類型不盡相同,具體的優(yōu)化操作肯定會(huì)比較深入,這里我這是按照我在工作中進(jìn)行的優(yōu)化工作進(jìn)行的自我總結(jié),關(guān)于我的優(yōu)化過程重點(diǎn)也不在這里。
Activity創(chuàng)建過程的優(yōu)化
在這次對(duì)app的優(yōu)化過程中,效果最為明顯的操作是對(duì)啟動(dòng)頁(SplashActivity)的優(yōu)化。通過上面的基礎(chǔ)概念的總結(jié)可以得出結(jié)論:在Activity的 onCreate() 和 onResume() 生命周期中, app的第一個(gè)frame其實(shí)都還沒有繪制出來.但因?yàn)锳ctivity的創(chuàng)建過程和調(diào)用鏈相當(dāng)長,這里先不總結(jié)Activity的創(chuàng)建流程.
只需要先知道Activity的布局和渲染過程其實(shí)是在 onResume執(zhí)行之后才開始的.Activity在AMS置于resume狀態(tài)后,Activity所屬的Window會(huì)通過WindowManagerImpl,addView()將decorView放入到 ViewRoot中,然后ViewRoot發(fā)起 traversal遍歷整個(gè)view樹,進(jìn)行布局和渲染,最終將畫面繪制到屏幕上.
所以說在不影響業(yè)務(wù)流程的情況下,為了盡快將app的首幀繪制到屏幕上,我們最后將啟動(dòng)頁的耗時(shí)操作(例如下載廣告圖)放在首幀繪制完成之后進(jìn)行.
那么問題來了,如何盡可能捕捉到應(yīng)用完成首幀繪制的時(shí)間點(diǎn)呢?
在我的另外一篇總結(jié) GPU-Rendering Profile分析中分析了android是如何將view繪制到屏幕上的,基本流程就是 Measure - Layout - Draw - GPU渲染,所以可以選擇hook這幾個(gè)時(shí)間點(diǎn),選擇合適的時(shí)機(jī)插入耗時(shí)操作,使得耗時(shí)操作可以盡量延遲執(zhí)行.
- 首先列出未優(yōu)化前的代碼,直接在onResume方法中啟動(dòng)耗時(shí)操作.
@Override
protected void onResume() {
super.onResume();
getWindow().getDecorView().post(mGetSplashDataRunnable);
- 優(yōu)化方案選擇在ViewTreeObserver.onGlobalLayoutListener()的回調(diào)中插入耗時(shí)操作.
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");
}
});


然后列出兩種方案的啟動(dòng)耗時(shí)測(cè)試,發(fā)現(xiàn)使用優(yōu)化方案的平均啟動(dòng)時(shí)間要減少80ms左右.
自定義默認(rèn)窗口背景,優(yōu)化用戶體驗(yàn)
雖然通延時(shí)加載耗時(shí)任務(wù)能夠在一定程度上加快app首幀的顯示速度,但是縮短的80ms的啟動(dòng)時(shí)間相比較總的耗時(shí)(520ms左右)來說,用戶體驗(yàn)依舊沒有得到明顯地提升。
前面總結(jié)到,點(diǎn)擊桌面上的應(yīng)用圖標(biāo),首先為我們展示的是一個(gè)空白的默認(rèn)啟動(dòng)背景,為了避免白屏的出現(xiàn)而影響體驗(yàn),最有效的方法還是需要修改默認(rèn)的空白背景,替換為app的默認(rèn)啟動(dòng)頁畫面.
具體做法為新建一個(gè) shape.xml,背景使用app的默認(rèn)啟動(dòng)圖
<?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>
然后建立一個(gè)新的主題,并將該主題設(shè)為啟動(dòng)頁Activity的background。
<style name="AppSplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:background">@drawable/shape_launch</item>
</style>
這樣就從根本上解決了啟動(dòng)頁會(huì)產(chǎn)生白屏的情況,當(dāng)然對(duì)于啟動(dòng)速度的優(yōu)化仍然是必要的,否則長時(shí)間卡在啟動(dòng)界面,即使沒有了白屏情況,用戶體驗(yàn)也會(huì)很差.
以上。