應(yīng)用啟動(dòng)速度(Launch-Time)的優(yōu)化


序言

應(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)頁.

c21d30343d6a5e2fdbce614b359a86eb.gif

工欲善其事,必先利其器。如果想要對(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é)
image.png

通過上面這張官方文檔提供的流程圖可以看出,冷啟動(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日志

image.png

上圖是查看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");
            }
        });
onResume.jpg

onGlobalLayout.jpg

然后列出兩種方案的啟動(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ì)很差.

以上。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,733評(píng)論 25 709
  • 請(qǐng)保持淡定,分析代碼,記住:性能很重要。 啟動(dòng)時(shí)間優(yōu)化 毫無疑問,應(yīng)用的啟動(dòng)速度越快越好。 本文可以幫助你優(yōu)化應(yīng)用...
    Mupceet閱讀 11,918評(píng)論 5 19
  • 先講點(diǎn)題外話 簡(jiǎn)述Activity的幾種啟動(dòng)模式 standard標(biāo)準(zhǔn)啟動(dòng)模式,也是Activity的啟動(dòng)模式,以...
    大大大大大先生閱讀 6,893評(píng)論 0 3
  • 文/無語 一些瑣碎的小事總會(huì)牽動(dòng)我們的神經(jīng),讓我們?cè)谏钪谐8衅v。 怎樣才能從繁瑣中抽身?我們需要清醒地活著,遇...
    無語_c7b8閱讀 367評(píng)論 2 2
  • 用一生執(zhí)念等待一季芳華。 每一個(gè)季節(jié)都有一個(gè)故事。 春光燦爛,春水微漾,春花嬌美 有關(guān)于春的一切 似乎都 很美,很...
    瑤YY閱讀 241評(píng)論 0 1

友情鏈接更多精彩內(nèi)容