天天看點

談一談Android 啟動優化的一些了解和方案

前言

假如我們去到一家餐廳,叫了半天都沒有人過來點菜,那等不了多久就沒耐心想走了。

對于 App 也是一樣的,如果我們打開一個應用半天都打不開,那很快的我們也會失去耐心。

啟動速度是使用者對我們應用的第一體驗,使用者隻有啟動我們的應用才能使用我們應用中的功能。

就算我們應用内部設計得再精美,其他性能優化地再好,如果打開速度很慢的話,使用者對我們應用的第一印象還是很差。

你可以追求完美,要做到應用在 1 毫秒内啟動。

但是一般情況下, 我們隻要做到超越競品或者遠超競品,就能在啟動速度這一個點上讓使用者滿意。

使用者選擇 App 的時候會考慮各種因素,而我們 App 開發者能做的就是在争取通過各種技術讓我們的 App 從衆多競品中脫穎而出。

1. 三種啟動狀态

啟動速度對 App 的整體性能非常重要,是以谷歌官方給出了一篇啟動速度優化的文章。

在這篇文章中,把啟動分為了三種狀态:熱啟動、暖啟動和冷啟動。

下面我們來看下三種啟動狀态的特點。

1.1 熱啟動

熱啟動是三種啟動狀态中是最快的一種,因為熱啟動是從背景切到了前台,不需要再建立 Applicaiton,也不需要再進行渲染布局等操作。

1.2 暖啟動

暖啟動的啟動速度介于冷啟動和熱啟動之間,暖啟動隻會重走 Activity 的生命周期,不會重走程序建立和 Application 的建立和生命周期等。

1.3 冷啟動

冷啟動經曆了一系列流程,耗時也是最多的,了解冷啟動整體流程的了解,可以幫助我們尋找之後的一個優化方向。

冷啟動也是優化的衡量标準,一般線上上進行的啟動優化都是以冷啟動速度為名額的。

啟動速度的優化方向是 Application 和 Activity 生命周期階段,這是我們開發者能控制的時間,其他階段都是系統做的。

冷啟動流程可以分為三步:建立程序、啟動應用和繪制界面。

  1. 建立程序

    建立程序階段主要做了下面三件事,這三件事都是系統做的。

    • 啟動 App
    • 加載空白 Window
    • 建立程序
  2. 啟動應用

    啟動應用階段主要做了下面三件事,從這些開始,随後的任務和我們自己寫的代碼有一定的關系。

    • 建立 Application
    • 啟動主線程
    • 建立 MainActivity
  3. 繪制界面

    繪制界面階段主要做了下面三件事。

    • 加載布局
    • 布置螢幕
    • 首幀繪制

2. 兩種測量方法

上一節介紹了三種啟動狀态,這一節我們來看一下常用的兩種測量啟動時間的方法:指令測量和埋點測量。

2.1 指令測量

指令測量指的是用 adb 指令測量啟動時間,通過下面兩步就能實作 adb 指令測量應用啟動時間

  1. 輸入測量指令
  2. 分析測量結果

2.2.1 輸入測量指令

我們在終端中輸入一條 adb 指令打開我們要測量的應用,打開後系統會輸出應用的啟動時間。

下面就是測量啟動時間的 adb 指令。

談一談Android 啟動優化的一些了解和方案

首屏 Activity 也要加上包名,比如下面這樣的。

談一談Android 啟動優化的一些了解和方案

2.2.2 分析測量結果

談一談Android 啟動優化的一些了解和方案

上面是指令執行完成後顯示的内容,在輸出中可以看到三個值:ThisTime、TotalTime 和 WaitTime。

下面我們來看下這三個值分别代表什麼。

  • ThisTime

    ThisTime 代表最後一個 Activity 啟動所需要的時間,也就是最後一個 Activity 的啟動耗時。

  • TotalTime

    TotalTime 代表所有 Activity 啟動耗時,在上面的輸出中,TotalTime 和 ThisTime 是一樣的,因為這個 Demo 沒有寫 Splash 界面。

    也就是這個 App 打開了 Application 後就直接打開了 MainActivity 界面,沒有啟動其他頁面。

  • WaitTime

    WaitTime 是 AMS 啟動 Activity 的總耗時。

這三者之間的關系如下。

ThisTime <= TotalToime < WaitTime

2.2 埋點測量

埋點測量指的是我們在應用啟動階段埋一個點,在啟動結束時再埋一個點,兩者之間的內插補點就是 App 的啟動耗時。

通過下面三步可以實作埋點測量。

  1. 定義埋點工具類
  2. 記錄啟動時間
  3. 計算啟動耗時

2.2.1 定義埋點工具類

使用埋點測量的第一步是定義一個記錄埋點工具類。

在這裡要注意的是,除了 System.currentTimeMillis() 以外,我們還可以用 SystemClock.currentThreadTimeMillis() 記錄時間。

通過 SystemClock 拿到的是 CPU 真正執行的時間,這個時間與下一大節要講的 Systrace 上記錄的時間點是一樣的。

談一談Android 啟動優化的一些了解和方案

2.2.2 記錄啟動時間

使用埋點測量的第二步是記錄啟動時間。

開始記錄的位置放在 Application 的 attachBaseContext 方法中,attachBaseContext 是我們應用能接收到的最早的一個生命周期回調方法。

談一談Android 啟動優化的一些了解和方案

2.2.3 計算啟動耗時

計算啟動耗時的一個誤區就是在 onWindowFocusChanged 方法中計算啟動耗時。

onWindowFocusChanged 方法隻是 Activity 的首幀時間,是 Activity 首次進行繪制的時間,首幀時間和界面完整展示出來還有一段時間差,不能真正代表界面已經展現出來了。

按首幀時間計算啟動耗時并不準确,我們要的是使用者真正看到我們界面的時間。

正确的計算啟動耗時的時機是要等真實的資料展示出來,比如在清單第一項的展示時再計算啟動耗時。

在 Adapter 中記錄啟動耗時要加一個布爾值變量進行判斷,避免 onBindViewHolder 方法被多次調用導緻不必要的計算。

談一談Android 啟動優化的一些了解和方案

2.3 小結

2.3.1 指令測量優缺點

  • 指令測量優點
    • 線下使用友善

      adb 指令測量啟動速度的方式線上下使用比較友善,而且這種方式還能用于測量競品。

  • 指令測量缺點
    • 不能帶到線上

      如果一條 adb 指令帶到線上去,沒有 app 也沒有系統幫我們執行這一條 adb 指令,我們就拿不到這些資料,是以不能帶到線上。

    • 不嚴謹和精确

      不能精确控制啟動時間的開始和結束。

2.3.2 埋點測量的特點

  • 精确

    手動打點的方式比較精确,因為我們可以精确控制開始和結束的位置。

  • 可帶到線上

    使用埋點測量進行使用者資料的采集,可以很友善地帶到線上,把資料上報給伺服器。

    伺服器可以針對所有使用者上報的啟動資料,每天做一個整合,計算出一個平均值,然後對比不同版本的啟動速度。

3. 兩個分析工具

常用的分析方法耗時的工具有 Systrace 和 Traceview,它們兩個是互相補充的關系,我們要在不同的場景下使用不同的工具,這樣才能發揮工具的最大作用。

本節内容如下。

  • Traceview
  • Systrace
  • 小結

3.1 Traceview

Traceview 能以圖形的形式展示代碼的執行時間和調用棧資訊,而且 Traceview 提供的資訊非常全面,因為它包含了所有線程。

Traceview 的使用可以分為兩步:開始跟蹤、分析結果。

下面我們來看看這兩步的具體操作。

3.1.1 開始跟蹤

我們可以通過 Debug.startMethodTracing("輸出檔案") 就可以開始跟蹤方法,記錄一段時間内的 CPU 使用情況。

當我們調用了 Debug.stopMethodTracing() 停止跟蹤方法後,系統就會為我們生成一個檔案,我們可以通過 Traceview 檢視這個檔案記錄的内容。

檔案生成的位置在 Android/data/包名/files 下,下面我們來看一個示例。

我們在 Application 的 onCreate 方法的開頭開始追蹤方法,然後在結尾結束追蹤,在這裡隻是對 BlockCanary 卡頓監測架構進行初始化。

談一談Android 啟動優化的一些了解和方案

startMethodTracing 方法真正調用的其實是另一個重載方法,在這個重載方法可以傳入 bufferSize。

bufferSize 就是分析結果檔案的大小,預設是 8 兆。

我們可以進行擴充,比如擴充為 16 兆、32 兆等。

這個重載方法的第三個參數是标志位,這個标志位隻有一個選項,就是 TRACE_COUNT_ALLOCS。

談一談Android 啟動優化的一些了解和方案

3.1.2 分析結果

運作了程式後,有兩種方式可以擷取到跟蹤結果檔案。

第一種方式是通過下面的指令把檔案拉到項目根目錄。

談一談Android 啟動優化的一些了解和方案

第二種方式是在 AS 右下方的檔案資料總管中定位到 /sdcard/android/data/包名/files/ 目錄下,然後自己找個地方儲存。

談一談Android 啟動優化的一些了解和方案

我們在 AS 中打開跟蹤檔案 mytrace.trace 後,就可以用 Profiler 檢視跟蹤的分析結果。

談一談Android 啟動優化的一些了解和方案

在分析結果上比較重要的是 5 種資訊。

  • 代碼指定的時間範圍

    這個時間範圍是我們通過 Debug 類精确指定的

  • 選中的時間範圍

    我們可以拖動時間線,選擇檢視一段時間内某條線程的調用堆棧

  • 程序中存在的線程

    在這裡可以看到在指定時間範圍内程序中隻有主線程和 BlockCanary 的線程,一共有 4 條線程。

  • 調用堆棧

    在上面的跟蹤資訊中,我選中了 main,也就是主線程。

    還把時間範圍縮小到了特定時間區域内,放大了這個時間範圍内主線程的調用堆棧資訊

  • 方法耗時

    當我們把滑鼠放到某一個方法上的時候,我們可以看到這個方法的耗時,比如上面的 initBlockCanary 的耗時是 19 毫秒。

3.2 Systrace

Systrace 結合了 Android 核心資料,分析了線程活動後會給我們生成一個非常精确 HTML 格式的報告。

Systrace 提供的 Trace 工具類預設隻能 API 18 以上的項目中才能使用,如果我們的相容版本低于 API 18,我們可以使用 TraceCompat。

Systrace 的使用步驟和 Traceview 差不多,分為下面兩步。

  • 調用跟蹤方法
  • 檢視跟蹤結果

3.2.2 調用跟蹤方法

首先在 Application 中調用 Systrace 的跟蹤方法。

談一談Android 啟動優化的一些了解和方案

然後連接配接裝置,在終端中定位到 Android SDK 目錄下,比如我的 Android SDK 目錄在 /users/oushaoze/library/Android/sdk 。

這時候我打開 SDK 目錄下的 platform-tools/systrace 就能看到 systrace.py 的一個 python 檔案。

Systrace 是一個 Python 腳本,輸入下面指令,運作 systrace ,開始追蹤系統資訊。

談一談Android 啟動優化的一些了解和方案

這行指令附加了下面一些選項。

  • -t ...

    -t 後面表示的是跟蹤的時間,比如上面設定的是 10 秒就結束。

  • -o ...

    -o 後面表示把檔案輸出到指定目錄下。

  • -a ...

    -a 後面表示的是要啟動的應用包名

輸入完這行指令後,可以看到開始跟蹤的提示。看到 Starting tracing 後可以打開打開我們的應用。

10 秒後,會看到 Wrote trace HTML file: ....。

談一談Android 啟動優化的一些了解和方案

上面這段輸出就是說追蹤完畢,追蹤到的資訊都寫到 trace.html 檔案中了,接下來我們打開這個檔案。

3.2.3 檢視跟蹤結果

談一談Android 啟動優化的一些了解和方案

打開檔案後我們可以看到上面這樣的一個視圖,在這裡有幾個需要特别關注的地方。

  • 8 核

    我運作 Systrace 的裝置是 8 核的,是以這裡的 Kernel 下面是 8 個 CPU。

  • 縮放

    當我們選中縮放後,縮放的方式是上下移動,不是左右移動。

  • 移動

    選擇移動後,我們可以拖動我們往下檢視其它程序的分析資訊。

  • 時間片使用情況

    時間片使用情況指的是各個 CPU 在特定時間内的時間片使用情況,當我們用縮放把特定時間段内的時間片資訊放大,我們就可以看到時間片是被哪個線程占用了。

  • 運作中的程序

    左側一欄除了各個核心外,還會顯示運作中的程序。

我們往下移動,可以看到 MyAppplication 程序的線程活動情況。

談一談Android 啟動優化的一些了解和方案

在這個視圖上我們主要關注三個點。

  • 主線程

    在這裡我們主要關注主線程的運作了哪些方法

  • 跟蹤的時間段

    剛才在代碼中設定的标簽是 AppOnCreate,在這裡就顯示了這個跟蹤時間段的标簽

  • 耗時

    我們選中 AppOnCreate 标簽後,就可以看到這個方法的耗時。

    在 Slice 标簽下的耗時資訊包括 Wall Duration 和 CPU Duration,下面是它們的差別。

    • Wall Duration

      Wall Time 是執行這段代碼耗費的時間,不能作為優化名額。

      假如我們的代碼要進入鎖的臨界區,如果鎖被其他線程持有,目前線程就進入了阻塞狀态,而等待的時間是會被計算到 Wall Time 中的。

    • CPU Duration

      CPU Duration 是 CPU 真正花在這段代碼上的時間,是我們關心的優化名額。

      在上面的例子中 Wall Duration 是 84 毫秒,CPU Duration 是 34 毫秒,也就是在這段時間内一共有 50 毫秒 CPU 是處于休息狀态的,真正執行代碼的時間隻花了 34 毫秒。

3.3 小結

3.3.1 Traceview 的兩個特點

Traceview 有兩個特點:可埋點、開銷大。

  • 可埋點

    Traceview 的好處之一是可以在代碼中埋點,埋點後可以用 CPU Profiler 進行分析。

    因為我們現在優化的是啟動階段的代碼,如果我們打開 App 後直接通過 CPU Profiler 進行記錄的話,就要求你有單身三十年的手速,點選開始記錄的時間要和應用的啟動時間完全一緻。

    有了 Traceview,哪怕你是老年人手速也可以記錄啟動過程涉及的調用棧資訊。

  • 開銷大

    Traceview 的運作時開銷非常大,它會導緻我們程式的運作變慢。

    之是以會變慢,是因為它會通過虛拟機的 Profiler 抓取我們目前所有線程的所有調用堆棧。

    因為這個問題,Traceview 也可能會帶偏我們的優化方向。

    比如我們有一個方法,這個方法在正常情況下的耗時不大,但是加上了 Traceview 之後可能會發現它的耗時變成了原來的十倍甚至更多。

3.3.2 Systrace 的兩個特點

Systrace 的兩個特點:開銷小、直覺。

  • 開銷小

    Systrace 開銷非常小,不像 Traceview,因為它隻會在我們埋點區間進行記錄。

    而 Traceview 是會把所有的線程的堆棧調用情況都記錄下來。

  • 直覺

    在 Systrace 中我們可以很直覺地看到 CPU 使用率的情況。

    當我們發現 CPU 使用率低的時候,我們可以考慮讓更多代碼以異步的方式執行,以提高 CPU 使用率。

3.3.3 Traceview 與 Systrace 的兩個差別

  • 檢視工具

    Traceview 分析結果要使用 Profiler 檢視。

    Systrace 分析結果是在浏覽器檢視 HTML 檔案。

  • 埋點工具類

    Traceview 使用的是 Debug.startMethodTracing()。

    Systrace 用的是 Trace.beginSection() 和 TraceCompat.beginSection()。

4. 兩種優化方法

常用的兩種優化方法有兩種,這兩種是可以結合使用的。

第一種是閃屏頁,在視覺上讓使用者感覺啟動速度快,第二種是異步初始化。

4.1 閃屏頁

閃屏頁是優化啟動速度的一個小技巧,雖然對實際的啟動速度沒有任何幫助,但是能讓使用者感覺比啟動的速度要快一些。

閃屏頁就是在 App 打開首屏 Activity 前,首先顯示一張圖檔,這張圖檔可以是 Logo 頁,等 Activity 展示出來後,再把 Theme 變回來。

冷啟動的其中一步是建立一個空白 Window,閃屏頁就是利用這個空白 Window 顯示占位圖。

通過下面四個步驟可以實作閃屏頁。

  1. 定義閃屏圖
  2. 定義閃屏主題
  3. 設定主題
  4. 換回主題

4.1.1 定義閃屏圖

第一步是在 drawable 目錄下建立一個 splash.xml 檔案。

談一談Android 啟動優化的一些了解和方案

4.1.2 定義閃屏主題

第二步是在 values/styles.xml 中定義一個 Splash 主題。

談一談Android 啟動優化的一些了解和方案

4.1.3 設定主題

第三步是在清單檔案中設定 Theme。

談一談Android 啟動優化的一些了解和方案

4.1.4 換回主題

第四步是在調用 super.onCreate 方法前切換回來

談一談Android 啟動優化的一些了解和方案

4.2 異步初始化

我們這一節來看一下怎麼用線程池進行異步初始化。

本節内容包括如下部分,

  • 異步初始化簡介
  • 線程池大小
  • 線程池基本用法

4.2.1 異步初始化簡介

異步優化就是把初始化的工作分細分成幾個子任務,然後讓子線程分别執行這些子任務,加快初始化過程。

如果你對怎麼在 Android 中實作多線程不了解,可以看一下我的上一篇文章:探索 Android 多線程優化,在這篇文章中我對在 Android 使用多線程的方法做了一個簡單的介紹。

有些初始化代碼在子線程執行的時候可能會出現問題,比如要求在 onCreate 結束前執行完成。

這種情況我們可以考慮使用 CountDownLatch 實作,實在不行的時候就保留這段初始化代碼在主線程中執行。

4.2.2 線程池大小

我們可以使用線程池來實作異步初始化,使用線程池需要注意的是線程池大小的設定。

線程池大小要根據不同的裝置設定不同的大小,有的手機是四核的,有的是八核的,如果把線程池大小設為固定數值的話是不合理的。

我們可以參考 AsyncTask 中設定的線程池大小,在 AsyncTask 中有 CPU_COUNT 和 CORE_POOL_SIZE。

  • CPU_COUNT

    CPU_COUNT 的值是裝置的 CPU 核數。

  • CORE_POOL_SIZE

    CORE_POOL_SIZE 是線程池核心大小,這個值的最小值是 2,最大值是 Math.min(CPU_COUNT - 1, 4)。

    當裝置的核數為 8 時,CORE_POOL_SIZE 的值為 4,當裝置核數為 4 時,這個值是 3,也就是 CORE_POOL_SIZE 的最大值是 4。

4.2.3 線程池基本用法

在這裡我們可以參考 AsyncTask 的做法來設定線程池的大小,并把初始化的工作送出到線程池中。

談一談Android 啟動優化的一些了解和方案

6. 改進優化方案

上一節介紹了怎麼通過線程池處理初始化任務,這一節我們看一下改進的異步初始化工具:啟動器(LaunchStarter)。

這一節的内容包括如下部分。

  • 線程池實作的不足
  • 啟動器簡介
  • 啟動器工作流程
  • 實作任務等待執行
  • 實作任務依賴關系

6.1 線程池實作的不足

通過線程池處理初始化任務的方式存在三個問題。

  • 代碼不夠優雅

    假如我們有 100 個初始化任務,那像上面這樣的代碼就要寫 100 遍,送出 100 次任務。

  • 無法限制在 onCreate 中完成

    有的第三方庫的初始化任務需要在 Application 的 onCreate 方法中執行完成,雖然可以用 CountDownLatch 實作等待,但是還是有點繁瑣。

  • 無法實作存在依賴關系

    有的初始化任務之間存在依賴關系,比如極光推送需要裝置 ID,而 initDeviceId() 這個方法也是一個初始化任務。

6.2 啟動器簡介

啟動器的核心思想是充分利用多核 CPU ,自動梳理任務順序。

第一步是我們要對代碼進行任務化,任務化是一個簡稱,比如把啟動邏輯抽象成一個任務。

第二步是根據所有任務的依賴關系排序生成一個有向無環圖,這個圖是自動生成的,也就是對所有任務進行排序。

比如我們有個任務 A 和任務 B,任務 B 執行前需要任務 A 執行完,這樣才能拿到特定的資料,比如上面提到的 initDeviceId。

第三步是多線程根據排序後的優先級依次執行,比如我們現在有三個任務 A、B、C。

假如任務 B 依賴于任務 A,這時候生成的有向無環圖就是 ACB,A 和 C 可以提前執行,B 一定要排在 A 之後執行。

6.3 啟動器工作流程

談一談Android 啟動優化的一些了解和方案
  • Head Task

    Head Task 就是所有任務執行前要做的事情,在這裡初始化一些其他任務依賴的資源,也可以隻是打個 Log。

  • Tail Task

    Tail Task 可用于執行所有任務結束後列印一個 Log,或者是上報資料等任務。

  • Idle Task

    Idle Task 是在程式空閑時執行的任務。

如果我們不使用異步的方案,所有的任務都會在主線程執行。

為了讓其他線程分擔主線程的工作,我們可以把初始化的工作拆分成一個個的子任務,采用并發的方式,使用多個線程同時執行這些子任務。

6.4 實作任務等待執行

啟動器(LaunchStarter)使用了有向無環圖實作任務之間的依賴關系,具體的代碼可以在本文最下方找到。

使用啟動器需要完成 3 個步驟。

  • 添加依賴
  • 定義任務
  • 開始任務

下面我們來看下這 3 個步驟的具體操作。

6.4.1 添加依賴

首先在項目根目錄的 build.gradle 中添加 jitpack 倉庫。

allprojects {
  repositories {
    // ...
    maven { url 'https://jitpack.io' }
  }
}
           

然後在 app 子產品的 build.gradle 中添加依賴

dependencies {
  // 啟動器
  implementation 'com.github.zeshaoaaa:LaunchStarter:0.0.1'
}  
           

6.4.2 定義任務

定義任務這個步驟涉及了幾個概念:MainTask、Task、needWait 和 run。

  • MainTask

    MainTask 是需要在主線程執行的任務

  • Task

    Task 就是在工作線程執行的任務。

  • needWait

    InitWeexTask 中重寫了 needWait 方法,這個方法傳回 true 表示 onCreate 的執行需要等待這個任務完成。

  • run

    run() 方法中的代碼就是需要做的初始化工作

談一談Android 啟動優化的一些了解和方案

6.4.3 開始任務

定義好了任務後,我們就可以開始任務了。

這裡需要注意的是,如果我們的任務中有需要等待完成的任務,我們可以調用 TaskDispatcher 的 await() 方法等待這個任務完成,比如 InitWeexTask。

使用 await() 方法要注意的是這個方法要在 start() 方法調用後才能使用。

談一談Android 啟動優化的一些了解和方案

6.5 實作任務依賴關系

除了上面提到的等待功能以外,啟動器還支援任務之間存在依賴關系,下面我們來看一個極光推送初始化任務的例子。

在這一節會講實作任務依賴關系的兩個步驟。

  • 定義任務
  • 開始任務

6.5.1 定義任務

在這裡我們定義兩個存在依賴關系的任務:GetDeviceIdTask 和 InitJPush Task。

首先定義 GetDeviceIdTask ,這個任務負責初始化裝置 ID 。

談一談Android 啟動優化的一些了解和方案

然後定義InitJPushTask,這個任務負責初始化極光推送 SDK,InitJPushTask 在啟動器中是尾部任務 Tail Task。

InitJPushTask 依賴于 GetDeviceIdTask,是以需要重寫 dependsOn 方法,在 dependsOn 方法中建立一個 Class 清單,把想依賴的任務的 Class 添加到清單中并傳回。

談一談Android 啟動優化的一些了解和方案

6.5.2 開始任務

GetDeviceIdTask 和 InitJPushTask 這兩個任務都不需要等待 Application 的 onCreate 方法執行完成,是以我們這裡不需要調用 TaskDispatcher 的 await 方法。

談一談Android 啟動優化的一些了解和方案

6.5.3 小結

上面這兩個步驟就能實作通過啟動器實作任務之間的依賴關系。

7. 延遲執行任務

在我們應用的 Application 和 Activity 中可能存在部分優先級不高的初始化任務,我們可以考慮把這些任務進行延遲初始化,比如放在清單的第一項顯示出來後再進行初始化。

正常的延遲初始化方法有兩種:onPreDraw 和 postDelayed。

除了正常方法外,還有一種改進的延遲初始化方案:延遲啟動器。

本節包括如下内容。

  • onPreDraw

    onPreDraw 指的是在清單第一項顯示後,在 onPreDraw 回調中執行初始化任務

  • postDelayed

    通過 Handler 的 postDelayed 方法延遲執行初始化任務

  • 延遲啟動器

7.1 onPreDraw

這一節我們來看下怎麼通過 OnPreDrawListener 把任務延遲到清單顯示後再執行。

下面是 onPreDraw 方式實作延遲初始化的 3 個步驟。

  • 聲明回調接口
  • 調用接口方法
  • 在 Activity 中監聽
  • 小結

7.1.1 聲明回調接口

第一步先聲明一個 OnFeedShowCallback。

談一談Android 啟動優化的一些了解和方案

7.1.2 調用接口方法

第二步是在 Adapter 中的第一條顯示的時候調用 onFeedShow() 方法。

談一談Android 啟動優化的一些了解和方案

7.1.3 在 Activity 中監聽

第三步是在 Activity 中調用 setOnFeedCallback 方法。

談一談Android 啟動優化的一些了解和方案

7.1.4 小結

直接在 onFeedShow 中執行初始化任務的弊端是有可能導緻滑動卡頓。

如果我們 onPreDraw 的方式延遲執行初始化任務,假如這個任務耗時是 2 秒,那就意味着在清單顯示第一條後的 2 秒内,清單是無法滑動的,使用者體驗很差。

7.2 postDelayed

還有一種方式就是通過 Handler.postDelayed 方法發送一個延遲消息,比如延遲到 100 毫秒後執行。

假如在 Activity 中有 1 個 100 行的初始化方法,我們把前 10 行代碼放在 postDelayed 中延遲 100 毫秒執行,把前 20 行代碼放在 postDelayed 中延遲 200 毫秒執行。

這種實作的确緩解了卡頓的情況,但是這種實作存在兩個問題

  • 不夠優雅

    假如按上面的例子,可以分出 10 個初始化任務,每一個都放在 不同的 postDelayed 中執行,這樣寫出來的代碼不夠優雅。

  • 依舊卡頓

    假如把任務延遲 200 毫秒後執行,而 200 後使用者還在滑動清單,那還是會發生卡頓。

7.3 延遲啟動器

7.3.1 延遲啟動器基本用法

除了上面說到的方式外,現在我們來說一個更好的解決方案:延遲啟動器。

延遲啟動器利用了 IdleHandler 實作主線程空閑時才執行任務,IdleHandler 是 Android 提供的一個類,IdleHandler 會在目前消息隊列空閑時才執行任務,這樣就不會影響使用者的操作了。

假如現在 MessageQueue 中有兩條消息,在這兩條消息處理完成後,MessageQueue 會通知 IdleHandler 現在是空閑狀态,然後 IdleHandler 就會開始處理它接收到的任務。

DelayInitDispatcher 配合 onFeedShow 回調來使用效果更好。

下面是一段使用延遲啟動器 DelayInitDispatcher 執行初始化任務的示例代碼。

談一談Android 啟動優化的一些了解和方案

結語

看完了上面提到的一些啟動優化技巧,你有沒有得到一些啟發呢?

又或者是你有沒有自己的一些啟動優化技巧,不妨在評論區給大家說說。

可能你覺得不值一提的技巧,能解決了其他同學的一個大麻煩。