Android啟動(dòng)優(yōu)化 :學(xué)會(huì)這些讓?xiě)?yīng)用啟動(dòng)速度提高10倍!

作者:胡飛洋
鏈接:https://juejin.im/post/5f183026f265da230739b7db

一、概述

手機(jī)桌面點(diǎn)擊一個(gè)應(yīng)用,用戶(hù)希望應(yīng)用能及時(shí)響應(yīng)、快速加載。啟動(dòng)時(shí)間過(guò)長(zhǎng)的應(yīng)用可能會(huì)令用戶(hù)失望。這種糟糕的體驗(yàn)可能會(huì)導(dǎo)致用戶(hù)在 Play 商店針對(duì)您的應(yīng)用給出很低的評(píng)分,甚至完全棄用您的應(yīng)用。

本篇就來(lái)講解如何分析和優(yōu)化應(yīng)用的啟動(dòng)時(shí)間。首先介紹啟動(dòng)過(guò)程機(jī)制,然后討論如何檢測(cè)啟動(dòng)時(shí)間以及分析工具,最后給出通用啟動(dòng)優(yōu)化方案。

二、應(yīng)用啟動(dòng)流程介紹

根據(jù)官方文檔,應(yīng)用有三種啟動(dòng)狀態(tài):冷啟動(dòng)、溫啟動(dòng)、熱啟動(dòng)

  • 冷啟動(dòng) 冷啟動(dòng)是指應(yīng)用從頭開(kāi)始啟動(dòng):系統(tǒng)進(jìn)程在冷啟動(dòng)后才創(chuàng)建應(yīng)用進(jìn)程。發(fā)生冷啟動(dòng)的情況包括應(yīng)用自設(shè)備啟動(dòng)后或系統(tǒng)終止應(yīng)用后首次啟動(dòng)。例如,通過(guò)任務(wù)列表手動(dòng)殺掉應(yīng)用進(jìn)程后,又重新啟動(dòng)應(yīng)用。

  • 熱啟動(dòng) 熱啟動(dòng)比冷啟動(dòng)簡(jiǎn)單得多,開(kāi)銷(xiāo)也更低。在熱啟動(dòng)中,系統(tǒng)的所有工作就是將您的 Activity 帶到前臺(tái)。只要應(yīng)用的所有 Activity 仍駐留在內(nèi)存中,應(yīng)用就不必重復(fù)執(zhí)行進(jìn)程、應(yīng)用、activity的創(chuàng)建。例如,按home鍵到桌面,然后又點(diǎn)圖標(biāo)啟動(dòng)應(yīng)用。

  • 溫啟動(dòng) 溫啟動(dòng)包含了在冷啟動(dòng)期間發(fā)生的部分操作;同時(shí),它的開(kāi)銷(xiāo)要比熱啟動(dòng)高。有許多潛在狀態(tài)可視為溫啟動(dòng)。例如:用戶(hù)按返回鍵退出應(yīng)用后又重新啟動(dòng)應(yīng)用。這時(shí)進(jìn)程已在運(yùn)行,但應(yīng)用必須通過(guò)調(diào)用 onCreate() 從頭開(kāi)始重新創(chuàng)建 Activity。

啟動(dòng)優(yōu)化是在 冷啟動(dòng) 的基礎(chǔ)上進(jìn)行優(yōu)化。要優(yōu)化應(yīng)用以實(shí)現(xiàn)快速啟動(dòng),了解系統(tǒng)和應(yīng)用層面的情況以及它們?cè)诟鱾€(gè)狀態(tài)中的互動(dòng)方式很有幫助。

在冷啟動(dòng)開(kāi)始時(shí),系統(tǒng)有三個(gè)任務(wù),它們是:

  • 加載并啟動(dòng)應(yīng)用。
  • 在啟動(dòng)后立即顯示應(yīng)用的空白啟動(dòng)窗口。
  • 創(chuàng)建應(yīng)用進(jìn)程。

系統(tǒng)一創(chuàng)建應(yīng)用進(jìn)程,應(yīng)用進(jìn)程就負(fù)責(zé)后續(xù)階段

  • 啟動(dòng)主線(xiàn)程。
  • 創(chuàng)建應(yīng)用對(duì)象。
  • 創(chuàng)建主 Activity。
  • 加載視圖。
  • 執(zhí)行初始繪制。

一旦應(yīng)用進(jìn)程完成第一次繪制,系統(tǒng)進(jìn)程就會(huì)換掉當(dāng)前顯示的后臺(tái)窗口(StartingWindow),替換為主 Activity。此時(shí),用戶(hù)可以開(kāi)始使用應(yīng)用。

下面是[官方文檔]中的啟動(dòng)過(guò)程流程圖,顯示系統(tǒng)進(jìn)程和應(yīng)用進(jìn)程之間如何交接工作。實(shí)際上對(duì)啟動(dòng)流程的簡(jiǎn)要概括。

三、優(yōu)化核心思想

問(wèn)題來(lái)了,啟動(dòng)優(yōu)化是對(duì) 啟動(dòng)流程的那些步驟進(jìn)行優(yōu)化呢?

這是一個(gè)好問(wèn)題。我們知道,用戶(hù)關(guān)心的是:點(diǎn)擊桌面圖標(biāo)后 要盡快的顯示第一個(gè)頁(yè)面,并且能夠進(jìn)行交互。 根據(jù)啟動(dòng)流程的分析,顯示頁(yè)面能和用戶(hù)交互,這是主線(xiàn)程做的事情。那么就要求 我們不能再主線(xiàn)程做耗時(shí)的操作。啟動(dòng)中的系統(tǒng)任務(wù)我們無(wú)法干預(yù),能干預(yù)的就是在創(chuàng)建應(yīng)用和創(chuàng)建 Activity 的過(guò)程中可能會(huì)出現(xiàn)的性能問(wèn)題。這一過(guò)程具體就是:

  • Application的attachBaseContext
  • Application的onCreate
  • activity的onCreate
  • activity的onStart
  • activity的onResume

activity的onResume方法完成后才開(kāi)始首幀的繪制。所以這些方法中的耗時(shí)操作我們是要極力避免的。

并且,通常情況下,一個(gè)應(yīng)用的主頁(yè)的數(shù)據(jù)是需要進(jìn)行網(wǎng)絡(luò)請(qǐng)求的,那么用戶(hù)啟動(dòng)應(yīng)用是希望快速進(jìn)入主頁(yè)以及看到主頁(yè)數(shù)據(jù),這也是我們計(jì)算啟動(dòng)結(jié)束時(shí)間的一個(gè)依據(jù)。

四、時(shí)間檢測(cè)

4.1 Displayed

在 Android 4.4(API 級(jí)別 19)及更高版本中,logcat 包含一個(gè)輸出行,其中包含名為 “Displayed” 的值。此值代表從啟動(dòng)進(jìn)程到在屏幕上完成對(duì)應(yīng) Activity 的繪制所用的時(shí)間。經(jīng)過(guò)的時(shí)間包括以下事件序列:

  • 啟動(dòng)進(jìn)程。
  • 初始化對(duì)象。
  • 創(chuàng)建并初始化 Activity。
  • 擴(kuò)充布局。
  • 首次繪制。

這是我的demo app 啟動(dòng)的日志打印,查看

2020-07-13 19:54:38.256 18137-18137/com.hfy.androidlearning I/hfy: onResume begin. 
2020-07-13 19:54:38.257 18137-18137/com.hfy.androidlearning I/hfy: onResume end. 
2020-07-13 19:54:38.269 1797-16782/? I/WindowManager: addWindow: Window{1402051 u0 com.hfy.androidlearning/com.hfy.demo01.MainActivity}
2020-07-13 19:54:38.391 1797-2017/? I/ActivityTaskManager: Displayed com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s251ms
復(fù)制代碼

可見(jiàn)“Displayed”的時(shí)間打印是在添加window之后,而添加window是在onResume方法之后。

4.2 adb shell

也可以使用adb命令運(yùn)行應(yīng)用來(lái)測(cè)量初步顯示所用時(shí)間:

adb shell am start -W [ApplicationId]/[根Activity的全路徑] 當(dāng)ApplicationId和package相同時(shí),根Activity全路徑可以省略前面的packageName。

Displayed 指標(biāo)和前面一樣出現(xiàn)在 logcat 輸出中:

2020-07-14 14:53:05.294 1797-2017/? I/ActivityTaskManager: Displayed com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s98ms
復(fù)制代碼

您的終端窗口在adb命令執(zhí)行后還應(yīng)顯示以下內(nèi)容:

hufeiyangdeMacBook-Pro:~ hufeiyang$ adb shell am start -W com.hfy.androidlearning/com.hfy.demo01.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.hfy.androidlearning/com.hfy.demo01.MainActivity }
Status: ok
LaunchState: COLD
Activity: com.hfy.androidlearning/com.hfy.demo01.MainActivity
TotalTime: 2098
WaitTime: 2100
Complete
復(fù)制代碼

我們關(guān)注TotalTime即可,即應(yīng)用的啟動(dòng)時(shí)間,包括 創(chuàng)建進(jìn)程 + Application初始化 + Activity初始化到界面顯示 的過(guò)程。

4.3 reportFullyDrawn()

可以使用 reportFullyDrawn() (API19及以上)方法測(cè)量從應(yīng)用啟動(dòng)到完全顯示所有資源和視圖層次結(jié)構(gòu)所用的時(shí)間。什么意思呢?前面核心思想中提到,主頁(yè)數(shù)據(jù)請(qǐng)求后完全呈現(xiàn)界面的過(guò)程也是一個(gè)優(yōu)化點(diǎn),而前面的“Displayed”、:“TotalTime”的時(shí)間統(tǒng)計(jì)都是啟動(dòng)到首幀繪制,那么如何獲取 從 啟動(dòng) 到 獲取網(wǎng)絡(luò)請(qǐng)求后再次完成刷新 的時(shí)間呢?

要解決此問(wèn)題,您可以手動(dòng)調(diào)用Activity的 reportFullyDrawn()方法,讓系統(tǒng)知道您的 Activity 已完成延遲加載。當(dāng)您使用此方法時(shí),logcat 顯示的值為從創(chuàng)建應(yīng)用對(duì)象到調(diào)用 reportFullyDrawn() 時(shí)所用的時(shí)間。使用示例如下:

    @Override
    protected void onResume() {
        super.onResume();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                            reportFullyDrawn();
                        }
                    }
                });

            }
        }).start();
    }
復(fù)制代碼

使用子線(xiàn)程睡1秒來(lái)模擬數(shù)據(jù)加載,然后調(diào)用reportFullyDrawn(),以下是 logcat 的輸出。

2020-07-14 15:26:00.979 1797-2017/? I/ActivityTaskManager: Displayed com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s133ms
2020-07-14 15:26:01.788 1797-2017/? I/ActivityTaskManager: Fully drawn com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s943ms
復(fù)制代碼

4.4 代碼打點(diǎn)

寫(xiě)一個(gè)打點(diǎn)工具類(lèi),開(kāi)始結(jié)束時(shí)分別記錄,把時(shí)間上報(bào)到服務(wù)器。

此方法可帶到線(xiàn)上,但代碼有侵入性。

開(kāi)始記錄的位置放在 Application 的 attachBaseContext 方法中,attachBaseContext 是我們應(yīng)用能接收到的最早的一個(gè)生命周期回調(diào)方法。

計(jì)算啟動(dòng)結(jié)束時(shí)間的兩種方式

  • 一種是在 onWindowFocusChanged 方法中計(jì)算啟動(dòng)耗時(shí)。 onWindowFocusChanged 方法只是 Activity 的首幀時(shí)間,是 Activity 首次進(jìn)行繪制的時(shí)間,首幀時(shí)間和界面完整展示出來(lái)還有一段時(shí)間差,不能真正代表界面已經(jīng)展現(xiàn)出來(lái)了。

  • 按首幀時(shí)間計(jì)算啟動(dòng)耗時(shí)并不準(zhǔn)確,我們要的是用戶(hù)真正看到我們界面的時(shí)間。 正確的計(jì)算啟動(dòng)耗時(shí)的時(shí)機(jī)是要等真實(shí)的數(shù)據(jù)展示出來(lái),比如在列表第一項(xiàng)的展示時(shí)再計(jì)算啟動(dòng)耗時(shí)。 (在 Adapter 中記錄啟動(dòng)耗時(shí)要加一個(gè)布爾值變量進(jìn)行判斷,避免 onBindViewHolder 方法被多次調(diào)用導(dǎo)致不必要的計(jì)算。)

//第一個(gè)item 且沒(méi)有記錄過(guò),就結(jié)束打點(diǎn)
  if (helper.getLayoutPosition() == 1 && !mHasRecorded) {
      mHasRecorded = true;
      helper.getView(R.id.xxx).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
          @Override
          public boolean onPreDraw() {
              helper.getView(R.id.xxx).getViewTreeObserver().removeOnPreDrawListener(this);
              LogHelper.i("結(jié)束打點(diǎn)!");
              return true;
          }
      });
  }

復(fù)制代碼

4.5 AOP(Aspect Oriented Programming) 打點(diǎn)

面向切面編程,可以使用AspectJ。例如可以切Application的onCreate方法來(lái)計(jì)算其耗時(shí)。 特點(diǎn)是是對(duì)代碼無(wú)侵入性、可帶到線(xiàn)上。

五、分析工具介紹

分析方法耗時(shí)的工具: Systrace 、 Traceview,兩個(gè)是相互補(bǔ)充的關(guān)系,我們要在不同的場(chǎng)景下使用不同的工具,這樣才能發(fā)揮工具的最大作用。

5.1 Traceview

Traceview 能以圖形的形式展示代碼的執(zhí)行時(shí)間和調(diào)用棧信息,而且 Traceview 提供的信息非常全面,因?yàn)樗怂芯€(xiàn)程。

Traceview 的使用可以分為兩步:開(kāi)始跟蹤、分析結(jié)果。我們來(lái)看看具體操作。

通過(guò) Debug.startMethodTracing(tracepath) 開(kāi)始跟蹤方法,記錄一段時(shí)間內(nèi)的 CPU 使用情況。調(diào)用 Debug.stopMethodTracing() 停止跟蹤方法,然后系統(tǒng)就會(huì)為我們生成一個(gè).trace文件,我們可以通過(guò) Traceview 查看這個(gè)文件記錄的內(nèi)容。

文件生成的位置默認(rèn)在 Android/data/包名/files 下,下面來(lái)看一個(gè)例子。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.i(TAG, "onCreate begin. ");
        super.onCreate(savedInstanceState);
        //默認(rèn)生成路徑:Android/data/包名/files/dmtrace.trace
        Debug.startMethodTracing();
        //也可以自定義路徑
       //Debug.startMethodTracing(getExternalFilesDir(null)+"test.trace");

        setContentView(R.layout.activity_main);
        Intent intent = getIntent();
        String name = intent.getStringExtra("name");
        Log.i(TAG, "onCreate: name = " + name);

        initConfig();
        initView();
        initData();
        ...

        Debug.stopMethodTracing();
    }
復(fù)制代碼

在MainActivity的onCreate前后方法中分別調(diào)用開(kāi)始停止記錄方法,運(yùn)行打開(kāi)應(yīng)用進(jìn)入首頁(yè)后,我們定位到 /sdcard/android/data/包名/files/ 目錄下查看文件管理器確實(shí)是有.trace文件:

然后雙擊打開(kāi):


以圖形來(lái)呈現(xiàn)方法跟蹤數(shù)據(jù)或函數(shù)跟蹤數(shù)據(jù),其中調(diào)用的時(shí)間段和時(shí)間在橫軸上表示,而其被調(diào)用方則在縱軸上顯示。 所以我們可以看到具體的方法及其耗時(shí)。

詳細(xì)介紹參考官方文檔《使用 CPU Profiler 檢查 CPU 活動(dòng)》。

可以看到在onCreate方法中,最耗時(shí)的是testHandler方法,它里面睡了一覺(jué)。

5.2 Systrace

Systrace 結(jié)合了 Android 內(nèi)核數(shù)據(jù),分析了線(xiàn)程活動(dòng)后會(huì)給我們生成一個(gè)非常精確 HTML 格式的報(bào)告。

Systrace原理:在系統(tǒng)的一些關(guān)鍵鏈路(如SystemServcie、虛擬機(jī)、Binder驅(qū)動(dòng))插入一些信息(Label)。然后,通過(guò)Label的開(kāi)始和結(jié)束來(lái)確定某個(gè)核心過(guò)程的執(zhí)行時(shí)間,并把這些Label信息收集起來(lái)得到系統(tǒng)關(guān)鍵路徑的運(yùn)行時(shí)間信息,最后得到整個(gè)系統(tǒng)的運(yùn)行性能信息。其中,Android Framework 里面一些重要的模塊都插入了label信息,用戶(hù)App中也可以添加自定義的Lable。

Systrace 提供的 Trace 工具類(lèi)默認(rèn)只能 API 18 以上的項(xiàng)目中才能使用,如果我們的兼容版本低于 API 18,我們可以使用 TraceCompat。 Systrace 的使用步驟和 Traceview 差不多,分為下面兩步。

  • 調(diào)用跟蹤方法
  • 查看跟蹤結(jié)果

來(lái)看示例,在onCreate前后分別使用TraceCompat.beginSection、TraceCompat.endSection方法:

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        Log.i(TAG, "onCreate begin. ");

        super.onCreate(savedInstanceState);

        TraceCompat.beginSection("MainActivity onCreate");

        Debug.startMethodTracing();//dmtrace.trace
//        Debug.startMethodTracing(getExternalFilesDir(null)+"test.trace");

        setContentView(R.layout.activity_main);

        initConfig();
        initView();
        initData();

        Debug.stopMethodTracing();

        TraceCompat.endSection();
    }
復(fù)制代碼

運(yùn)行app后,手動(dòng)殺掉。然后cd 到SDK 目錄下的 platform-tools/systrace 下,使用命令:

python systrace.py -t 10 -o /Users/hufeiyang/trace.html -a com.hfy.androidlearning

其中:-t 10是指跟蹤10秒,-o 表示把文件輸出到指定目錄下,-a 是指定應(yīng)用包名。

輸入完這行命令后,可以看到開(kāi)始跟蹤的提示。看到 “Starting tracing ”后,手動(dòng)打開(kāi)我們的應(yīng)用。

示例如下:

hufeiyangdeMacBook-Pro:~ hufeiyang$ cd  /Users/hufeiyang/Library/Android/sdk/platform-tools/systrace

hufeiyangdeMacBook-Pro:systrace hufeiyang$ python systrace.py -t 10 -o /Users/hufeiyang/trace.html  -a com.hfy.androidlearning

Starting tracing (10 seconds)
Tracing completed. Collecting output...
Outputting Systrace results...
Tracing complete, writing results

Wrote trace HTML file: file:///Users/hufeiyang/trace.html
復(fù)制代碼

跟蹤10秒,然后就在指定目錄生成了html文件,我們打開(kāi)看看:

這里我們同樣可以看到具體的耗時(shí),以及每一幀渲染耗費(fèi)的時(shí)間。具體參考官方文檔《Systrace 概覽》

小結(jié) Traceview 的兩個(gè)特點(diǎn)

  • 可埋點(diǎn) Traceview 的好處之一是可以在代碼中埋點(diǎn),埋點(diǎn)后可以用 CPU Profiler 進(jìn)行分析。 因?yàn)槲覀儸F(xiàn)在優(yōu)化的是啟動(dòng)階段的代碼,如果我們打開(kāi) App 后直接通過(guò) CPU Profiler 進(jìn)行記錄的話(huà),就要求你有單身三十年的手速,點(diǎn)擊開(kāi)始記錄的時(shí)間要和應(yīng)用的啟動(dòng)時(shí)間完全一致。 有了 Traceview,哪怕你是老年人手速也可以記錄啟動(dòng)過(guò)程涉及的調(diào)用棧信息。
  • 開(kāi)銷(xiāo)大 Traceview 的運(yùn)行時(shí)開(kāi)銷(xiāo)非常大,它會(huì)導(dǎo)致我們程序的運(yùn)行變慢。 之所以會(huì)變慢,是因?yàn)樗鼤?huì)通過(guò)虛擬機(jī)的 Profiler 抓取我們當(dāng)前所有線(xiàn)程的所有調(diào)用堆棧。 因?yàn)檫@個(gè)問(wèn)題,Traceview 也可能會(huì)帶偏我們的優(yōu)化方向。 比如我們有一個(gè)方法,這個(gè)方法在正常情況下的耗時(shí)不大,但是加上了 Traceview 之后可能會(huì)發(fā)現(xiàn)它的耗時(shí)變成了原來(lái)的十倍甚至更多。

Systrace 的兩個(gè)特點(diǎn)

  • 開(kāi)銷(xiāo)小 Systrace 開(kāi)銷(xiāo)非常小,不像 Traceview,因?yàn)樗粫?huì)在我們埋點(diǎn)區(qū)間進(jìn)行記錄。 而 Traceview 是會(huì)把所有的線(xiàn)程的堆棧調(diào)用情況都記錄下來(lái)。
  • 直觀 在 Systrace 中我們可以很直觀地看到 CPU 利用率的情況。 當(dāng)我們發(fā)現(xiàn) CPU 利用率低的時(shí)候,我們可以考慮讓更多代碼以異步的方式執(zhí)行,以提高 CPU 利用率。

Traceview 與 Systrace 的兩個(gè)區(qū)別

  • 查看工具 Traceview 分析結(jié)果要使用 Profiler 查看。 Systrace 分析結(jié)果是在瀏覽器查看 HTML 文件。
  • 埋點(diǎn)工具類(lèi) Traceview 使用的是 Debug.startMethodTracing()。 Systrace 用的是 Trace.beginSection() 和 TraceCompat.beginSection()。

六、啟動(dòng)優(yōu)化方案

優(yōu)化方案有兩個(gè)方向:

  • 視覺(jué)優(yōu)化,啟動(dòng)耗時(shí)沒(méi)有變少,但是啟動(dòng)過(guò)程中給用戶(hù)更好的體驗(yàn)。
  • 速度優(yōu)化,減少主線(xiàn)程的耗時(shí),真實(shí)做到快速啟動(dòng)。

6.1 視覺(jué)優(yōu)化

《Activity的啟動(dòng)》中提到,在Activity啟動(dòng)前會(huì)展示一個(gè)名字叫StartingWindow的window,這個(gè)window的背景是取要啟動(dòng)Activity的Theme中配置的WindowBackground。

因?yàn)閱?dòng)根activity前是需要?jiǎng)?chuàng)建進(jìn)程等一系列操作,需要一定時(shí)間,而展示StartingWindow的目的是 告訴用戶(hù)你點(diǎn)擊是有反應(yīng)的,只是在處理中,然后Activity啟動(dòng)后,Activity的window就替換掉這個(gè)StartingWindow了。如果沒(méi)有這個(gè)StartingWindow,那么點(diǎn)擊后就會(huì)一段時(shí)間沒(méi)有反應(yīng),給用戶(hù)誤解。

而這,就是應(yīng)用啟動(dòng)開(kāi)始時(shí) 會(huì)展示白屏的原因了。

那么視覺(jué)優(yōu)化的方案 也就有了:替換第一個(gè)activity(通常是閃屏頁(yè))的Theme,把白色背景換成Logot圖,然后再Activity的onCreate中換回來(lái)。 這樣啟動(dòng)時(shí)看到的就是你配置的logo圖了。

具體操作一下:

        <activity android:name=".MainActivity" android:theme="@style/SplashTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
復(fù)制代碼

這里我的而第一個(gè)activity是MainActivity,配置了theme是R.style.SplashTheme,來(lái)看下:

    <style name="SplashTheme" parent="AppNoActionBarAlphaAnimTheme">
        <item name="android:windowBackground">@drawable/splash_background</item>
    </style>
復(fù)制代碼

看到 android:windowBackground已經(jīng)配置成了自定義的drawable,這個(gè)就是關(guān)鍵點(diǎn)了,而默認(rèn)是windowBackground是白色??纯醋远x的drawable:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:opacity="opaque">

<!--兩層-->
    <item android:drawable="@android:color/white"/>
    <item>
        <bitmap
            android:src="@drawable/dog"
            android:gravity="center"/>
    </item>

</layer-list>
復(fù)制代碼

drawable的根節(jié)點(diǎn)是<layer-list>,然后一層是白色底,一層就是我們的logo圖片了。

最后,在activity的onCreate中把Theme換回R.style.AppTheme即可(要在super.onCreate之前)。

    protected void onCreate(Bundle savedInstanceState) {
        setTheme(R.style.AppTheme);
        super.onCreate(savedInstanceState);
    }
復(fù)制代碼

效果如下:

可以看到,確實(shí)視覺(jué)上體驗(yàn)比白屏好很多。

但實(shí)際上啟動(dòng)速度并沒(méi)有變快,下面就來(lái)看看可以真實(shí)提高啟動(dòng)速度的方案有哪些。

6.2 異步初始化

前面提到 提高啟動(dòng)速度,核心思想就是 減少主線(xiàn)程的耗時(shí)操作。啟動(dòng)過(guò)程中 可控住耗時(shí)的主線(xiàn)程 主要是Application的onCreate方法、Activity的onCreate、onStart、onResume方法。

通常我們會(huì)在Application的onCreate方法中進(jìn)行較多的初始化操作,例如第三方庫(kù)初始化,那么這一過(guò)程是就需要重點(diǎn)關(guān)注。

減少主線(xiàn)程耗時(shí)的方法,又可細(xì)分為異步初始化、延遲初始化,即把 主線(xiàn)程任務(wù) 放到子線(xiàn)程執(zhí)行 或 延后執(zhí)行。 下面就先來(lái)看看異步初始化是如何實(shí)現(xiàn)的。

執(zhí)行異步請(qǐng)求,一般是使用線(xiàn)程池,例如:

        Runnable initTask = new Runnable() {
            @Override
            public void run() {
                //init task
            }
        };

        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadCount);
        fixedThreadPool.execute(initTask);
復(fù)制代碼

但是通過(guò)線(xiàn)程池處理初始化任務(wù)的方式存在三個(gè)問(wèn)題:

  • 代碼不夠優(yōu)雅 假如我們有 100 個(gè)初始化任務(wù),那像上面這樣的代碼就要寫(xiě) 100 遍,提交 100 次任務(wù)。
  • 無(wú)法限制在 onCreate 中完成 有的第三方庫(kù)的初始化任務(wù)需要在 Application 的 onCreate 方法中執(zhí)行完成,雖然可以用 CountDownLatch 實(shí)現(xiàn)等待,但是還是有點(diǎn)繁瑣。
  • 無(wú)法實(shí)現(xiàn)存在依賴(lài)關(guān)系 有的初始化任務(wù)之間存在依賴(lài)關(guān)系,比如極光推送需要設(shè)備 ID,而 initDeviceId() 這個(gè)方法也是一個(gè)初始化任務(wù)。

那么解決方案是啥?啟動(dòng)器

LauncherStarter,即啟動(dòng)器,是針對(duì)這三個(gè)問(wèn)題的解決方案,結(jié)合CountDownLatch對(duì)線(xiàn)程池的再封裝,充分利用CPU多核,自動(dòng)梳理任務(wù)順序。

使用方式:

  • 引入依賴(lài)
  • 劃分任務(wù),確認(rèn)依賴(lài)和限制關(guān)系
  • 添加任務(wù),執(zhí)行啟動(dòng)

首先依賴(lài)引入:

implementation 'com.github.zeshaoaaa:LaunchStarter:0.0.1'
復(fù)制代碼

然后把初始化任務(wù)劃分成一個(gè)個(gè)任務(wù);厘清依賴(lài)關(guān)系,例如任務(wù)2要依賴(lài)任務(wù)1完成后才能開(kāi)始;還有例如3任務(wù)需要在onCreate方法結(jié)束前完成;任務(wù)4要在主線(xiàn)程執(zhí)行。

然后添加這些任務(wù),開(kāi)始任務(wù),設(shè)置等待。

具體使用也比較簡(jiǎn)單,代碼如下:

public class MyApplication extends Application {

    private static final String TAG = "MyApplication";

    @Override
    public void onCreate() {
        super.onCreate();

        TaskDispatcher.init(getBaseContext());
        TaskDispatcher taskDispatcher = TaskDispatcher.createInstance();

        // task2依賴(lài)task1;
        // task3未完成時(shí)taskDispatcher.await()處需要等待;
        // test4在主線(xiàn)程執(zhí)行
        //每個(gè)任務(wù)都耗時(shí)一秒
        Task1 task1 = new Task1();
        Task2 task2 = new Task2();
        Task3 task3 = new Task3();
        Task4 task4Main = new Task4();

        taskDispatcher.addTask(task1);
        taskDispatcher.addTask(task2);
        taskDispatcher.addTask(task3);
        taskDispatcher.addTask(task4Main);

        Log.i(TAG, "onCreate: taskDispatcher.start()");
        taskDispatcher.start();//開(kāi)始

        taskDispatcher.await();//等task3完成后才會(huì)往下走
        Log.i(TAG, "onCreate: end.");
    }

    private static class Task1 extends Task {
        @Override
        public void run() {
            Log.i(TAG, Thread.currentThread().getName()+" run start: task1");
            doTask();
            Log.i(TAG, Thread.currentThread().getName()+" run end: task1");
        }
    }

    private static class Task2 extends Task {
        @Override
        public void run() {
            Log.i(TAG, Thread.currentThread().getName()+" run start: task2");
            doTask();
            Log.i(TAG, Thread.currentThread().getName()+" run end: task2");
        }

        @Override
        public List<Class<? extends Task>> dependsOn() {
            //依賴(lài)task1,等task1執(zhí)行完再執(zhí)行
            ArrayList<Class<? extends Task>> classes = new ArrayList<>();
            classes.add(Task1.class);
            return classes;
        }
    }

    private static class Task3 extends Task {
        @Override
        public void run() {
            Log.i(TAG, Thread.currentThread().getName()+" run start: task3");
            doTask();
            Log.i(TAG, Thread.currentThread().getName()+" run end: task3");
        }

        @Override
        public boolean needWait() {
            //task3未完成時(shí),在taskDispatcher.await()處需要等待。這里就是保證在onCreate結(jié)束前完成。
            return true;
        }
    }

    private static class Task4 extends MainTask {
    //繼承自MainTask,即保證在主線(xiàn)程執(zhí)行
        @Override
        public void run() {
            Log.i(TAG, Thread.currentThread().getName()+" run start: task4");
            doTask();
            Log.i(TAG, Thread.currentThread().getName()+" run end: task4");
        }
    }

    private static void doTask() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
復(fù)制代碼

有4個(gè)初始化任務(wù),都耗時(shí)1秒,若都在主線(xiàn)程執(zhí)行,那么會(huì)耗時(shí)4秒。這里使用啟動(dòng)器執(zhí)行,并且保證了上面描述的任務(wù)要求限制。執(zhí)行完成后日志如下:

2020-07-17 12:06:20.648 26324-26324/com.hfy.androidlearning I/MyApplication: onCreate: taskDispatcher.start()
2020-07-17 12:06:20.650 26324-26324/com.hfy.androidlearning I/MyApplication: main run start: task4
2020-07-17 12:06:20.651 26324-26427/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-1 run start: task1
2020-07-17 12:06:20.657 26324-26428/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-2 run start: task3

2020-07-17 12:06:21.689 26324-26324/com.hfy.androidlearning I/MyApplication: main run end: task4
2020-07-17 12:06:21.689 26324-26427/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-1 run end: task1
2020-07-17 12:06:21.690 26324-26429/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-3 run start: task2
2020-07-17 12:06:21.697 26324-26428/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-2 run end: task3
2020-07-17 12:06:21.697 26324-26324/com.hfy.androidlearning I/MyApplication: onCreate: end.

2020-07-17 12:06:22.729 26324-26429/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-3 run end: task2
復(fù)制代碼

可見(jiàn)主線(xiàn)程耗時(shí)只有1秒。 另外,要注意的是,task3、task4一定是在onCreate內(nèi)完成了,task1、task2都可能是在onCreate結(jié)束后一段時(shí)間才完成,所以在Activity中就不能使用task1、task2相關(guān)的庫(kù)了。那么 在劃分任務(wù),確認(rèn)依賴(lài)和限制關(guān)系時(shí)就要注意了。

異步初始化就說(shuō)這么多,原理部分可直接閱讀源碼,很容易理解。接著看延遲初始化。

6.3 延遲初始化

在 Application 和 Activity 中可能存在優(yōu)先級(jí)不高的初始化任務(wù),可以考慮把這些任務(wù)進(jìn)行 延遲初始化。延遲初始化并不是減少了主線(xiàn)程耗時(shí),而是讓耗時(shí)操作讓位、讓資源給UI繪制,將耗時(shí)的操作延遲到UI加載完畢后。

那么問(wèn)題來(lái)了,如何延遲呢?

  • 使用new Handler().postDelay()方法、或者view.postDelay()——但是延遲時(shí)間不好把握,不知道啥時(shí)候UI加載完畢。
  • 使用View.getViewTreeObserver().addOnPreDrawListener()監(jiān)聽(tīng)——可以保證view繪制完成,但是此時(shí)發(fā)生交互呢,例如用戶(hù)在滑動(dòng)列表,那么就會(huì)造成卡頓了。

那么解決方案是啥?延遲啟動(dòng)器!

延遲啟動(dòng)器,利用IdleHandler特性,在CPU空閑時(shí)執(zhí)行,對(duì)延遲任務(wù)進(jìn)行分批初始化, 這樣 執(zhí)行時(shí)機(jī)明確、也緩解界面UI卡頓。 延遲啟動(dòng)器就是上面的LauncherStarter中的一個(gè)類(lèi)。

public class DelayInitDispatcher {

    private Queue<Task> mDelayTasks = new LinkedList<>();

    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            if(mDelayTasks.size()>0){
                Task task = mDelayTasks.poll();
                new DispatchRunnable(task).run();
            }
            return !mDelayTasks.isEmpty();
        }
    };

    public DelayInitDispatcher addTask(Task task){
        mDelayTasks.add(task);
        return this;
    }

    public void start(){
        Looper.myQueue().addIdleHandler(mIdleHandler);
    }

}
復(fù)制代碼

使用也很簡(jiǎn)單,例如在閃屏頁(yè)中添加任務(wù)開(kāi)始即可:

//SpalshActivity

DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();

protected void onCreate(Bundle savedInstanceState) {
        delayInitDispatcher.addTask(new Task() {
            @Override
            public void run() {
                Log.i(TAG, "run: delay task begin");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.i(TAG, "run: delay task end");
            }
        });
        delayInitDispatcher.start();
}
復(fù)制代碼

經(jīng)測(cè)試,確實(shí)是在是在布局展示后開(kāi)始任務(wù)。但是如果耗時(shí)較長(zhǎng)(例子中是3秒),過(guò)程中滑動(dòng)屏幕,是不能及時(shí)響應(yīng)的,會(huì)感覺(jué)到明顯的卡頓。

所以,能異步的task優(yōu)先使用異步啟動(dòng)器在Application的onCreate方法中加載,對(duì)于不能異步且耗時(shí)較少的task,我們可以利用延遲啟動(dòng)器進(jìn)行加載。如果任務(wù)可以到用時(shí)再加載,可以使用懶加載的方式。

IdleHandler原理分析:

//MessageQueue.java
    Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }
復(fù)制代碼

從消息隊(duì)列取消息時(shí),如果沒(méi)有取到消息,就執(zhí)行 空閑IdleHandler,執(zhí)行完就remove。

6.4 Multidex預(yù)加載優(yōu)化

安裝或者升級(jí)后 首次 MultiDex 花費(fèi)的時(shí)間過(guò)于漫長(zhǎng),我們需要進(jìn)行Multidex的預(yù)加載優(yōu)化。

5.0以上默認(rèn)使用ART,在安裝時(shí)已將Class.dex轉(zhuǎn)換為oat文件了,無(wú)需優(yōu)化,所以應(yīng)判斷只有在主進(jìn)程及SDK 5.0以下才進(jìn)行Multidex的預(yù)加載

抖音BoostMultiDex優(yōu)化實(shí)踐:

抖音BoostMultiDex優(yōu)化實(shí)踐:Android低版本上APP首次啟動(dòng)時(shí)間減少80%(一)

Github地址:BoostMultiDex

快速接入:

  • build.gradle的dependencies中添加依賴(lài):
dependencies {
    // For specific version number, please refer to app demo
    implementation 'com.bytedance.boost_multidex:boost_multidex:1.0.1'
}
復(fù)制代碼
  • 與官方MultiDex類(lèi)似,在Application.attachBaseContext的最前面進(jìn)行初始化即可:
public class YourApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        BoostMultiDex.install(base);
    }
復(fù)制代碼

今日頭條5.0以下,BoostMultiDex、MultiDex啟動(dòng)速度對(duì)比

6.5 頁(yè)面數(shù)據(jù)預(yù)加載

閃屏頁(yè)、首頁(yè)的數(shù)據(jù)預(yù)加載:閃屏廣告、首頁(yè)數(shù)據(jù) 加載后緩存到本地,下次進(jìn)入時(shí)直接讀取緩存。 首頁(yè)讀取緩存到內(nèi)存的操作還可以提前到閃屏頁(yè)。

6.6 頁(yè)面繪制優(yōu)化

閃屏頁(yè)與主頁(yè)的繪制優(yōu)化,這里涉及到繪制優(yōu)化相關(guān)知識(shí)了,例如減少布局層級(jí)等。

七、總結(jié)

我們先介紹了啟動(dòng)流程、優(yōu)化思想、耗時(shí)檢測(cè)、分析工具,然后給出了常用優(yōu)化方案:異步初始化、延遲初始化。涉及了很多新知識(shí)和工具,一些地方文章中沒(méi)有展開(kāi),可以參考給出的連接詳細(xì)學(xué)習(xí)。畢竟性能優(yōu)化是多樣技術(shù)知識(shí)的綜合使用,需要系統(tǒng)掌握對(duì)應(yīng)工作流程被、分析工具、解決方案,才能對(duì)性能進(jìn)行深層次的優(yōu)化。

好了,今天就到這里,歡迎留言討論~

點(diǎn)關(guān)注,更多Android開(kāi)發(fā)技能~~

?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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