Android App 冷啓動優化方案

App 啓動優化

啓動方式

  • 冷啓動html

    當啓動應用時,後臺沒有該應用的進程(常見如:進程被殺、首次啓動等),這時系統會從新建立一個新的進程分配給該應用java

  • 暖啓動android

    當啓動應用時,後臺已有該應用的進程(常見如:按back鍵、home鍵,應用雖然會退出,可是該應用的進程是依然會保留在後臺,可進入任務列表查看),因此在已有進程的狀況下,這種啓動會從已有的進程中來啓動應用git

  • 熱啓動github

    相比暖啓動,熱啓動時應用作的工做更少,啓動時間更短。熱啓動產生的場景不少,常見如:用戶使用返回鍵退出應用,而後立刻又從新啓動應用shell

熱啓動和暖啓動由於會從已有的進程中來啓動,不會再建立和初始化Application數據庫

平時咱們討論中基本都會將暖啓動和熱啓動合在一塊兒統稱爲熱啓動,由於暖啓動與熱啓動差別很小,若是不是特別留意啓動流程,那麼在用戶體驗和感官上沒有直接差別,可是在framework層執行時是有必定差別的。本次優化點也是圍繞冷啓動和熱啓動來作,將暖啓動與熱啓動統稱爲熱啓動緩存

另外有一點,從絕對時間上來看,app安裝後的首次啓動將會最耗時,由於首次啓動會新建數據庫,sp文件,各類緩存,配置等性能優化


白屏/黑屏問題

  • 白屏或黑屏,具體是哪個,取決於appTheme使用的是dark仍是light主題微信

  • Android Studio 引發的白屏

    2.x時代的AS開啓了instant run之後可能會致使白屏,但實際完整的apk包不會出現此問題

  • 冷啓動引發的白屏/黑屏

    點擊你app那一刻到系統調用Activity.onCreate()之間的時間段。在這個時間段內,WindowManager會先加載app主題樣式中的windowBackground做爲app的預覽元素,而後再真正去加載activitylayout佈局

  • 暖啓動/熱啓動引發的白屏/黑屏

    這點在配置較好,內存空間充足的手機上不是很明顯,但低端手機或者內存吃緊的狀況下依舊會出現」閃屏」效果,持續時間很短,一閃而過

優化

我將冷啓動優化分爲可控階段和不可控階段

  • 不可控階段

    點擊app之後到初始化Application之間這段時間,系統接管,從Zygote進程中fork建立新進程,GC回收等等一系列操做,和咱們app無關

  • 可控階段

    初始化Application開始,以下圖

    冷啓動應用程序工做流示意圖

    從上圖能夠看到,整個冷啓動流程中至少有兩處onCreate,分別是ApplicationActivity,整個流程都是可控的。因此,onCreate方法作的事情越多,冷啓動消耗的時間越長

啓動時間

  • Logcat 自動打印

    Android 4.4(API 19)開始,Logcat自動幫咱們打印出應用的啓動時間。這個時間從應用啓動(建立進程)開始計算,到完成視圖的第一次繪製(即Activity內容對用戶可見)爲止

    • 冷啓動 :
    04-25 14:53:09.317 869-1214/? I/ActivityManager: Displayed cn.com.dhc.danlu/.shell.activity.InitializeActivity: +4s256ms
    04-25 14:53:11.077 869-1214/? I/ActivityManager: Displayed cn.com.dhc.danlu/.shell.main.MainActivity: +559ms
    複製代碼
    • 熱啓動:
    04-25 14:53:20.407 869-1214/? I/ActivityManager: Displayed cn.com.dhc.danlu/.shell.activity.InitializeActivity: +178ms
    04-25 14:53:22.447 869-1214/? I/ActivityManager: Displayed cn.com.dhc.danlu/.shell.main.MainActivity: +131ms
    複製代碼
    • 這個 log 信息是從com.android.server.am.ActivityRecord#reportLaunchTimeLocked(long curTime)中打印出來的
    private void reportLaunchTimeLocked(final long curTime) {
       final ActivityStack stack = task.stack;
       final long thisTime = curTime - displayStartTime;
       final long totalTime = stack.mLaunchStartTime != 0
               ? (curTime - stack.mLaunchStartTime) : thisTime;
       if (ActivityManagerService.SHOW_ACTIVITY_START_TIME) {
           Trace.asyncTraceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, "launching", 0);
           EventLog.writeEvent(EventLogTags.AM_ACTIVITY_LAUNCH_TIME,
                   userId, System.identityHashCode(this), shortComponentName,
                   thisTime, totalTime);
           StringBuilder sb = service.mStringBuilder;
           sb.setLength(0);
           sb.append("Displayed ");
           sb.append(shortComponentName);
           sb.append(": ");
           TimeUtils.formatDuration(thisTime, sb);
           if (thisTime != totalTime) {
               sb.append(" (total ");
               TimeUtils.formatDuration(totalTime, sb);
               sb.append(")");
           }
           Log.i(ActivityManagerService.TAG, sb.toString());
       }
       mStackSupervisor.reportActivityLaunchedLocked(false, this, thisTime, totalTime);
       if (totalTime > 0) {
           //service.mUsageStatsService.noteLaunchTime(realActivity, (int)totalTime);
       }
       displayStartTime = 0;
       stack.mLaunchStartTime = 0;
    }
    
    // normal time:統計的是 Activity 從啓動到界面繪製完畢的時間
    // total time :統計的是 normal time + Activity 棧創建完畢的時間
    複製代碼
  • 測量 Activity 啓動時間

    ActivityreportFullyDrawn()

    咱們能夠在Activity的任意位置調用此方法已打印你想看到的、執行完某個方法的最終時間。它會在Logcat裏打印從apk初始化(和前面Displayed的時間是同樣的)到reportFullyDrawn()方法被調用用了多長時間

    ActivityManager: Displayed com.Android.myexample/.StartupTiming: +768ms
    複製代碼

    4.4上調用reportFullyDrawn()方法會崩潰(可是log仍是能正常打印),提示須要UPDATE_DEVICE_STATS權限 ,可是這個權限沒法拿到

    try {
    reportFullyDrawn();
    } catch (SecurityException e) {
    }
    複製代碼
  • 本地調試啓動時間

    本地調試

    上述命令能夠直接啓動對應包名的對應activity,但要注意不是所有activity都能使用這個命令直接啓動

    • 熱啓動 :

    熱啓動

    • 冷啓動:

    冷啓動

    • 只須要關注TotalTime便可
  • adb screenrecord 命令

    • 首先啓動帶bugreport選項(它能夠在frames中添加時間戳-應該是L中的特性)的screenrecord命令

    錄屏

    • 而後點擊app圖標,等待app顯示,ctrl-C中止screenrecord命令,在手機存儲中會生 成aunch.mp4視頻文件,而後pull到電腦
    • 打開視頻逐幀播放,注意視頻的上方有一個frame時間戳。一直往前直到你發現app圖標高亮了爲止。這個時候系統已經處理了圖標上的點擊事件,開始啓動app了,記錄下這一幀的時間。繼續播放幀直到你看到了app整個UI的第一幀爲止。根據不一樣狀況(是否有啓動窗口,是否有啓動畫面等等)。事件和窗口發生的實際順序可能會有不一樣。對於一個簡單的app來講,你會首先見到啓動窗口,而後漸變出app真實的UI。在你看到UI上的任何內容以後,再記錄下第一幀,這時app完成了佈局和繪製,準備開始顯示出來了。同時也記錄下這一幀所發生的時間
    • 如今把這兩個時間相減 ((UI displayed) - (icon tapped)) 獲得app從點擊到繪製就緒的全部時間。雖然這個時間包含了進程啓動以前的時間,可是至少它能夠用於跟其餘app比較

因而可知,app冷啓動時間大約爲4s,熱啓動時間大約爲132ms.

優化方案(僅針對可控區範圍)

  • 從啓動流程分析

    減小兩處onCreate()中的初始化操做,將部分初始化移動到IntentService中進行

  • 從用戶體驗分析

    app首頁的按返回鍵響應修改成響應Home鍵,曲線救國。讓用戶覺得app確實退出了,可是其實是點了Home鍵。如此一來,下次點擊app圖標的時候,直接喚起,不須要進行初始化操做,主要能夠避免再次走閃屏頁,參考美團,微信,QQ,淘寶等(實現的效果同樣,可是實現方式就不得而知了)

    • 微博:啓動後點擊返回鍵和Home鍵的操做同樣,底部選中tab沒有作自切換
    • 美團:啓動後點擊返回鍵和Home鍵的操做不同,底部選中tab作了自切換
    • QQ:同微博
    • 淘寶:啓動後在首頁按返回鍵,會先回到第一個tab,而後再退出

開始優化

  • 利用Google官方文檔推薦的方式,咱們將啓動頁界面的主題設置爲SplashTheme。此界面是冷啓動後首先加載的界面:

    mainifest

    主題內的代碼以下:

    style

    這個主題至關於丟了一張圖片做爲背景,也就是紅色背景LogoSlogan圖片,無版本號

    此時咱們已經「消除了」白屏/黑屏頁,將冷啓動的白屏/黑屏單調的純色背景替換爲咱們即將展現給用戶的 InitializeActivity界面的圖片,從系統的Window到咱們本身App跳轉過程,使用了全屏屬性,以達到無縫跳轉

    須要說明的是,這一步作了以後,對總體啓動時間並無任何的減小,時間不變,只是說給用戶的體驗要友好不少,再也不顯示一個突兀的白屏/黑屏界面 ------將鍋甩給本身appapp太卡,竟然在InitializeActivity要等這麼久(固然用戶不知道的是:系統window界面和InitializeActivity不是同一個界面)

    固然也能夠採用另外一種方式,那就是將上面的主題中的backgroud設置爲透明用戶點擊了圖標開始啓動的時候,界面上沒有任何變化,由於此時系統啓動的那個白屏/黑屏界面背景透明的 ------將鍋甩給系統,太卡,點了圖標竟然隔了這麼久才顯示InitializeActivity

  • 接下來咱們查看ApplicationInitializeActivityonCreate()是否有能夠遷移到IntentService中的代碼:

    • BaseApplication:

    BaseApplication

    ​ 能夠看到,其實當中的邏輯不是不少,而且都是須要在Application中初始化完畢的,不能單獨提出來進行初始化,其中只有GrowingIO能夠考慮提出來

    • MainApplication:

    MainApplication

    ​ 推送初始化能夠提出來

    • InitializeActivity:

    ActivityonCreate

    ​ onCreate 中乍看沒有什麼耗時操做,內部的幾個方法也都是必需要的業務邏輯,惟一能動的就是內部針對界面停留作的延時時間,目前是2s,能夠減小到1s左右。

  • 最後一步,在MainActivity中處理返回鍵邏輯。

    將確實是退出的邏輯替換爲按Home鍵:

    activity返回鍵

    這種作法給用戶一個假象:用戶按返回鍵退出,可是實際上並沒退出,app處於後臺,下次點擊圖標時直接喚起

    針對這種操做,須要注意幾個點

    • onRestart中須要判斷tab狀態
    • onSaveInstanceStateonRestoreInstanceState中須要保存和恢復數據,用於判斷用戶是點了 home仍是back,這兩種操做須要區分開,同時須要保存tab狀態
    • 經過廣播監聽Home鍵事件
    • ......
  • 結果

    冷啓動:

    優化後冷啓動

    熱啓動:

    優化後熱啓動

    由上圖可知,優化後的冷啓動時間大約爲3544ms,熱啓動時間大約爲127ms,相比以前的4121ms以及151ms來講有必定的提高,白屏效果也被消除了。

    可是其實提高最大的點不是白屏優化,由於咱們沒有把ApplicationActivityonCreate的邏輯減小並提到IntentService中。

    最大的提高點是,咱們讓用戶退出app時,形成假象,讓用戶覺得他確實退出了app,但實際上咱們是藏在後臺,當用戶熱啓動或者溫啓動時,咱們不用再通過InitializeActivity的流程進入首頁。


總結

  • 白屏/黑屏界面使用圖片替換
  • onCreate中儘可能避免作過多的初始化動做,若是必須,那麼考慮IntentService
  • 首頁中對BackHome鍵的動做作一些假象,使用戶按Back鍵時覺得他退出了,以減小下次啓動的沒必要要動做(建議:非即時消息類和社交類app,這種作法慎用,由於可能有流氓之嫌。。。(逃)
  • Activity#moveTaskToBack(true)

參考資料


TODO

  • [ ] 探究:爲什麼新建的 Hello World 工程冷啓動白屏時間比線上工程短一些

代碼傳送門

相關文章
相關標籤/搜索