1. 啟動優(yōu)化概述
Android 啟動優(yōu)化指的是 App 冷啟動速度的優(yōu)化
喉前,相關知識包括 Android 應用啟動相關知識
把将、啟動分析工具
以及啟動優(yōu)化方案
轻专。
啟動優(yōu)化相關知識
有 App 啟動原理
、App 啟動的三種狀態(tài)
察蹲、以及啟動問題的三種表現(xiàn)
请垛。
啟動分析工具
可以分為啟動速度測量工具
和啟動速度分析工具
。啟動速度測量工具有ActivityManager 測量
和埋點測量
洽议,啟動速度分析工具有Debug
和Trace
宗收。
啟動優(yōu)化方案
可以分為視覺優(yōu)化
、onCreate() 優(yōu)化
以及MultiDex 優(yōu)化
亚兄。
App 啟動時可以分為熱啟動
混稽、暖啟動
和冷啟動
三種,熱啟動是三種啟動速度中最快的一種
,暖啟動只會重走 Activity 的生命周期
匈勋,而冷啟動
要經歷創(chuàng)建進程礼旅、啟動應用和繪制界面等一系列操作,是耗時最多一種啟動狀態(tài)洽洁,也是常見的啟動優(yōu)化衡量標準
各淀。
啟動問題有點擊桌面圖標響應慢
、首頁顯示慢
以及首頁顯示后無法操作
三個诡挂。
點擊圖標響應慢
在把主題設為透明的時候才會發(fā)生碎浇,這時如果啟動初始化流程要 2 秒,那用戶就要 2 秒才能看到界面璃俗,就像是點擊無效
一樣奴璃。
首頁顯示慢
指的是閃屏廣告和各種 SDK 的初始化工作越來越復雜,在中低端機上可能要十幾秒才能啟動城豁。
首頁顯示后無法操作
指的是把初始化的工作延遲后苟穆,有可能出現(xiàn)首頁雖然顯示了,但是無法操作唱星,無法滑動的情況雳旅,這樣優(yōu)化的意義就很小了,所以從啟動到首頁可操作這整個過程都是啟動優(yōu)化要解決的問題间聊。
ActivityManager 測量
指的是通過 adb shell
讓 ActivityManager 在應用啟動完成后攒盈,輸出最后一個 Activity 啟動的耗時
、所有 Activity 啟動的耗時
以及AMS 啟動 Activity 的耗時
哎榴。ActivityManager 測量只能在線下使用
型豁。
埋點測量
指的是在 attachBaseContext()
方法中記錄啟動的時間,然后在列表第一項展示
或其他 View 繪制完成
的時候記錄啟動結束的時間尚蝌。這里要注意不要在 onWindowFocusChanged() 方法中記錄啟動結束的時間迎变,因為這個只是首幀時間,界面還未完整顯示出來飘言。埋點測量可以精確記錄啟動耗時衣形,而且能帶到線上
。
Debug
啟動速度分析使用的是 Android SDK 提供的一個 Debug 類
姿鸿,調用 Debug 的 startMethodTracing()
可以跟蹤接下來的一段時間內 CPU 的使用情況
谆吴,在調用 stopMethodTracing()
方法后,會生成一個文件般妙,我們可以通過 CPU Profiler
查看該文件中記錄的在啟動過程中都有哪些線程執(zhí)行了哪些方法
纪铺,包括各個方法的耗時
。Debug 的缺點是開銷非常大碟渺,有可能影響啟動測試結果
鲜锚。
Trace
也是 Android SDK 中的一個類突诬,與 Debug 相比,Trace 除了能看到啟動過程中各個方法的耗時芜繁,還能看到各個 CPU 的時間片使用情況
旺隙。Trace 的優(yōu)點是開銷小,只會在埋點區(qū)間記錄
骏令,而且可以直觀地看到 CPU 利用率
蔬捷,我們可以根據報告分析一下要不要開更多線程提升程序執(zhí)行性能,提升 CPU 利用率榔袋。
視覺優(yōu)化
指的是給閃屏頁設置圖片背景
周拐,這樣能讓用戶在視覺上感覺應用啟動很快,冷啟動過程中的第一步是創(chuàng)建一個空白 Window
凰兑,通過自定義一個主題 Theme
并設置給閃屏頁妥粟,就能讓這個空白 Window 顯示占位圖。
onCreate() 優(yōu)化
有異步初始化
吏够、啟動器
以及延遲初始化
和延遲啟動器
這 4 個方案勾给。
異步初始化
指的是把初始化工作細分為幾個子任務并放到子線程中,然后把這些子線程提交到線程池中锅知,減少主線程的工作播急。有的初始化代碼可能需要在 Application 的 onCreate() 方法結束前執(zhí)行完成,這時可以考慮用 CountDownLatch
等待該任務執(zhí)行完成售睹。異步初始化方案的缺點是無法建立任務依賴關系
桩警,比如極光推送需要用到設備 ID ,而設備 ID 初始化在另一個線程中執(zhí)行侣姆。
啟動器
優(yōu)化方案就是生真,首先把初始化代碼拆分為不同的任務
沉噩,然后根據任務的依賴關系排序生成一個有向無環(huán)圖
捺宗,這個圖是自動生成的,最后根據排序后的優(yōu)先級依次執(zhí)行任務
川蒙。任務可以分為 初始化前執(zhí)行的任務
蚜厉、初始化后執(zhí)行的任務
、初始化任務
以及空閑時執(zhí)行的任務
畜眨。初始化任務
又可分為在主線程執(zhí)行的任務
和在子線程執(zhí)行的任務
昼牛。
延遲初始化
優(yōu)化方案,就是把優(yōu)先級不高康聂,可以在啟動完成后執(zhí)行的初始化任務延遲到啟動完成后執(zhí)行贰健。比如放在列表第一項的 View 的 ViewTreeObserver
的 onPreDraw()
回調或用 Handler 的 postDelayed()
方法提交到消息隊列中延遲執(zhí)行。延遲啟動器的缺點是延遲執(zhí)行后頁面無法滑動
恬汁。
延遲啟動器
優(yōu)化方案就是利用 IdleHandler
實現(xiàn)在主線程空閑時再執(zhí)行任務伶椿,這樣一方面任務延遲執(zhí)行了,另一方面也不會影響用戶操作。
MultiDex 優(yōu)化
有 BoostMultidex
脊另。
2. 三種啟動狀態(tài)
谷歌官方把 App 的啟動分為了熱啟動
导狡、暖啟動
和冷啟動
三種狀態(tài)。
熱啟動是三種啟動狀態(tài)中是最快的一種
偎痛,因為熱啟動是從后臺切到了前臺旱捧,應用的 Activity 還駐留在內存中,應用不需要重復執(zhí)行對象初始化操作踩麦,不需要再創(chuàng)建 Applicaiton枚赡,也不需要再進行渲染布局等操作。
暖啟動只會重走 Activity 的生命周期
谓谦,啟動速度介于冷啟動和熱啟動之間标锄,暖啟動只會重走 Activity 的生命周期,不需要重新創(chuàng)建進程和 Application茁计。
冷啟動經歷了創(chuàng)建進程料皇、啟動應用和繪制界面一系列流程,是耗時最多的星压,也是常見的啟動優(yōu)化的衡量標準
践剂,一般在線上進行的啟動優(yōu)化都是以冷啟動速度為指標的。
啟動速度的優(yōu)化方向是 Application 和 Activity 生命周期階段娜膘,比如 Application 的 onCreate()
和 attachBaseContext()
這兩個生命周期回調方法的執(zhí)行時間逊脯,在 Application 和 Activity 的回調方法中做的事情是我們開發(fā)者能控制,其他階段都是系統(tǒng)做的竣贪。
冷啟動流程可以分為三步:創(chuàng)建進程
军洼、啟動應用
和繪制界面
,創(chuàng)建進程階段的三件事都是系統(tǒng)做的演怎,從啟動應用階段開始匕争,隨后的任務和我們自己寫的代碼有一定的關系。
3. 三個啟動問題
啟動速度慢具體的表現(xiàn)有點擊圖標響應慢
爷耀、首頁顯示慢
和顯示后無法操作
三個甘桑。
1. 點擊圖標響應慢
如果我們指定了透明的主題,那用戶點擊桌面圖標后歹叮,需要在 Application 創(chuàng)建和閃屏頁創(chuàng)建完成后才能看到商品跑杭,從用戶的角度來看,就是點擊了圖標咆耿,結果過了幾秒還是停在桌面德谅,就像是點擊無效一樣。
2. 首頁顯示慢
現(xiàn)在應用啟動流程越來越復雜萨螺,閃屏廣告窄做、熱修復宅荤、插件化框架等,所有的準備工作都要在啟動階段完成浸策,在中低端機上可能要十幾秒才能啟動冯键,難以忍受。
3. 首頁顯示后無法操作
假如把初始化工作通過不恰當的方式延遲執(zhí)行庸汗,就有可能出現(xiàn)首頁出來后根本無法操作惫确,假如用戶看到了首頁,但是要等過十幾秒后才能滑動蚯舱,那這個優(yōu)化的意義就非常小了改化,啟動優(yōu)化不能把注意力都放在首幀繪制時間上,而是要從用戶的真實體驗觸發(fā)枉昏,從用戶點擊圖標到可操作的整個過程的執(zhí)行速度陈肛,都是啟動優(yōu)化要解決的問題。
4. 兩種測量方法
下面來看一下常用的兩種測量啟動時間的方法:命令測量
和埋點測量
兄裂。
1. 命令測量
命令測量指的是用 adb 命令測量啟動時間句旱,在輸入測量命令后就能看到測量結果。
首先打開終端晰奖,輸入 adb shell am start -W packagename/首屏 Activity
打開我們要測量的應用谈撒,打開后系統(tǒng)會輸出應用的啟動時間,-W
選項表示等待啟動完成匾南。
首屏 Activity 也要加上包名啃匿,比如下面這樣的。
在該命令執(zhí)行后蛆楞,可以在輸出中可以看到 ThisTime
溯乒、TotalTime
和 WaitTime
三個值。
ThisTime 代表最后一個 Activity 啟動所需要的時間
豹爹,也就是最后一個 Activity 的啟動耗時裆悄。
TotalTime 代表所有 Activity 啟動耗時
,在上面的輸出中帅戒,TotalTime 和 ThisTime 是一樣的灯帮,因為這個 Demo 沒有寫 Splash 界面。
也就是這個 App 打開了 Application 后就直接打開了 MainActivity 界面逻住,沒有啟動其他頁面。
WaitTime 是 AMS 啟動 Activity 的總耗時迎献。
這三者之間的關系為 ThisTime <= TotalToime < WaitTime
瞎访。
除了 -W ,start 還支持下面這些選項吁恍。
-
-D
扒秸, 啟動調試功能 -
--start-profiler
file播演,啟動性能分析器并把結果發(fā)送到指定文件 -
-P
file,類似于啟動性能分析器伴奥,但是會在應用進入空閑狀態(tài)時停止分析 -
-R
count写烤,重復啟動 Activitycount
次數,在每次重復前拾徙,會完成頂層 Activity -
-S
洲炊,在啟動 Activity 前,強行停止目標應用 -
--opengl-trace
尼啡,啟動 OpenGL 函數的跟蹤 -
--user
user_id | current 暂衡,指定要為哪個用戶運行,如果未指定崖瞭,則作為當前用戶運行
2. 埋點測量
埋點測量指的是我們在應用啟動階段埋一個點狂巢,在啟動結束時再埋一個點,兩者之間的差值就是 App 的啟動耗時书聚。
使用埋點測量的第一步是定義一個記錄埋點工具類唧领。
在這里要注意的是,除了 System.currentTimeMillis() 以外雌续,我們還可以用 SystemClock.currentThreadTimeMillis() 記錄時間疹吃。
通過 SystemClock 拿到的是 CPU 真正執(zhí)行的時間,這個時間與下一大節(jié)要講的 Systrace 上記錄的時間點是一樣的西雀。
使用埋點測量的第二步是記錄啟動時間萨驶。
開始記錄的位置放在 Application 的 attachBaseContext 方法中,attachBaseContext 是我們應用能接收到的最早的一個生命周期回調方法艇肴。
計算啟動耗時的一個誤區(qū)就是在 onWindowFocusChanged()
方法中計算啟動耗時腔呜。
onWindowFocusChanged()
方法只是 Activity 的首幀時間,是 Activity 首次進行繪制的時間再悼,首幀時間和界面完整展示出來還有一段時間差核畴,不能真正代表界面已經展現(xiàn)出來了。
按首幀時間計算啟動耗時并不準確冲九,我們要的是用戶真正看到我們界面的時間谤草。
正確的計算啟動耗時的時機是要等真實的數據展示出來,比如在列表第一項的展示時再計算啟動耗時莺奸。
在 Adapter 中記錄啟動耗時要加一個布爾值變量進行判斷丑孩,避免 onBindViewHolder 方法被多次調用導致不必要的計算。
3. 小結
adb 命令測量啟動速度的方式在線下使用比較方便灭贷,而且這種方式還能用于測量競品温学。
adb 命令測量的缺點是不能帶到線上而且不能精確控制啟動時間的開始和結束
,如果一條 adb 命令帶到線上去甚疟,沒有 app 也沒有系統(tǒng)幫我們執(zhí)行這一條 adb 命令仗岖,我們就拿不到這個數據逃延,所以不能帶到線上。
埋點測量可以精確控制開始和結束的位置轧拄,而且可以帶到線上
揽祥,使用埋點測量進行用戶數據的采集,可以很方便地帶到線上檩电,把數據上報給服務器拄丰,服務器可以針對所有用戶上報的啟動數據,每天做一個整合是嗜,計算出一個平均值嫌套,然后對比不同版本的啟動速度乍桂。
5. 兩個分析工具
常用的分析方法耗時的工具有 Debug
和 Trace
,它們兩個是相互補充的關系,我們要在不同的場景下使用不同的工具币励,這樣才能發(fā)揮工具的最大作用乳幸,下面就來看下這兩個工具的用法飞袋。
5.1 Debug
CPU Profiler 能以圖形的形式展示代碼的執(zhí)行時間和調用棧信息蔗包,而且提供的信息非常全面,包含了啟動過程中涉及的所有線程甫题。下面我們來看看具體用法馁筐。
首先通過 Debug.startMethodTracing("輸出文件")
開始跟蹤方法,記錄一段時間內的 CPU 使用情況坠非。當我們調用了 Debug.stopMethodTracing()
停止跟蹤后敏沉,系統(tǒng)就會為我們生成一個文件,我們可以通過 CPU Profiler 查看這個文件記錄的內容炎码。
文件生成的位置在 Android/data/包名/files
下盟迟,下面我們來看一個示例。
我們在 Application 的 onCreate() 方法的開頭開始追蹤方法潦闲,然后在結尾結束追蹤攒菠,在這里只是對 BlockCanary
卡頓監(jiān)測框架進行初始化。
startMethodTracing() 方法真正調用的其實是另一個重載方法歉闰,在這個重載方法可以傳入 bufferSize辖众。
bufferSize 就是分析結果文件的大小,默認是 8m
和敬,我們可以進行擴充凹炸,比如擴充為 16m、32m 等概龄。
這個重載方法的第三個參數是標志位还惠,這個標志位只有一個選項,就是 TRACE_COUNT_ALLOCS
私杜。
2. 分析結果
運行程序后蚕键,有兩種方式可以獲取到跟蹤結果文件。第一種方式是通過下面的命令把文件拉到項目根目錄衰粹。
第二種方式是在 AS 右下方的文件資源管理器中定位到 /sdcard/android/data/包名/files/
目錄下锣光,然后自己找個地方保存。
在 Profiler 的 CPU 板塊的左上角的 Session 中點擊加號誊爹,然后選擇 Load From File 后瓢捉,就能看到啟動過程中都做了哪些事情。
在分析結果上比較重要的是 5 種信息泡态。
-
代碼指定的時間范圍
這個時間范圍是我們通過 Debug 類精確指定的
-
選中的時間范圍
我們可以拖動時間線搂漠,選擇查看一段時間內某條線程的調用堆棧
-
進程中存在的線程
在這里可以看到在指定時間范圍內進程中只有主線程和 BlockCanary 的線程桐汤,一共有 4 條線程。
-
調用堆棧
在上面的跟蹤信息中靶壮,我選中了 main怔毛,也就是主線程拣度。
還把時間范圍縮小到了特定時間區(qū)域內螃壤,放大了這個時間范圍內主線程的調用堆棧信息
-
方法耗時
當我們把鼠標放到某一個方法上的時候映穗,我們可以看到這個方法的耗時,比如上面的 initBlockCanary 的耗時是 19 毫秒宿接。
5.2 Trace
Systrace 結合了 Android 內核數據辕录,分析了線程活動后會給我們生成一個非常精確 HTML 格式的報告走诞。
Systrace 提供的 Trace 工具類默認只能 API 18 以上的項目中才能使用,如果我們的兼容版本低于 API 18碑幅,我們可以使用 TraceCompat沟涨。下面來看看具體用法。
首先在 Application 中調用 Systrace 的 beginSection() 方法喜庞。
然后連接設備延都,在終端中定位到 Android SDK 目錄下睛竣,比如我的 Android SDK 目錄在 /users/oushaoze/library/Android/sdk
。
然后打開 SDK 目錄下的 platform-tools/systrace
目錄嫉你,就能看到一個叫 systrace.py
的 python 腳本幽污,執(zhí)行下面這個命令簿姨,就可以開始追蹤系統(tǒng)信息扁位。
這行命令附加了下面一些選項域仇。
-
-t ...
-t 后面表示的是跟蹤的時間,比如上面設定的是 10 秒就結束泼掠。
-
-o ...
-o 后面表示把文件輸出到指定目錄下择镇。
-
-a ...
-a 后面表示的是要啟動的應用包名
輸入完這行命令后腻豌,可以看到開始跟蹤的提示∈瑁看到 Starting tracing 后可以打開打開我們的應用订框。
10 秒后兜叨,會看到 Wrote trace HTML file: ....
衩侥。
上面這段輸出就是說追蹤完畢,追蹤到的信息都寫到 trace.html
文件中了跪但,接下來我們打開這個文件屡久。
打開文件后我們可以看到上面這樣的一個視圖爱榔,在這里有幾個需要特別關注的地方筛欢。
-
8 核
我運行 Systrace 的設備是 8 核的唇聘,所以這里的 Kernel 下面是 8 個 CPU。
-
縮放
當我們選中縮放后剥险,縮放的方式是上下移動宪肖,不是左右移動匈庭。
-
移動
選擇移動后阱持,我們可以拖動我們往下查看其它進程的分析信息。
-
時間片使用情況
時間片使用情況指的是各個 CPU 在特定時間內的時間片使用情況,當我們用縮放把特定時間段內的時間片信息放大蒜绽,我們就可以看到時間片是被哪個線程占用了桶现。
-
運行中的進程
左側一欄除了各個內核外骡和,還會顯示運行中的進程慰于。
我們往下移動,可以看到 MyAppplication 進程的線程活動情況绵脯。
在這個視圖上我們主要關注三個點休里。
-
主線程
在這里我們主要關注主線程的運行了哪些方法
-
跟蹤的時間段
剛才在代碼中設置的標簽是 AppOnCreate,在這里就顯示了這個跟蹤時間段的標簽
-
耗時
我們選中 AppOnCreate 標簽后悴侵,就可以看到這個方法的耗時畜挨。
在 Slice 標簽下的耗時信息包括 Wall Duration 和 CPU Duration巴元,下面是它們的區(qū)別驮宴。
Wall Time 是執(zhí)行這段代碼耗費的時間
,不能作為優(yōu)化指標堵泽,假如我們的代碼要進入鎖的臨界區(qū)迎罗,如果鎖被其他線程持有纹安,當前線程就進入了阻塞狀態(tài),而等待的時間是會被計算到 Wall Time 中的光督。CPU Duration 是 CPU 真正花在這段代碼上的時間
,是我們關心的優(yōu)化指標筐摘。在上面的例子中 Wall Duration 是 84 毫秒咖熟,CPU Duration 是 34 毫秒努隙,也就是在這段時間內一共有 50 毫秒 CPU 是處于休息狀態(tài)的荸镊,真正執(zhí)行代碼的時間只花了 34 毫秒堪置。
4.3 小結
1. Traceview
Traceview 的優(yōu)點是可以在代碼中埋點
,埋點后可以用 CPU Profiler 進行分析岭洲,因為我們現(xiàn)在優(yōu)化的是啟動階段的代碼盾剩,如果我們打開 App 后直接通過 CPU Profiler 進行記錄的話告私,就要求你有單身三十年的手速承桥,點擊開始記錄的時間要和應用的啟動時間完全一致,而有了 Traceview蜀撑,哪怕你是老年人手速也可以記錄啟動過程涉及的調用棧信息剩彬。
Traceview 的缺點是運行時開銷非常大喉恋,它會導致我們程序的運行變慢
,之所以會變慢徘意,是因為它會通過虛擬機的 Profiler 抓取我們當前所有線程的所有調用堆棧轩褐。
因為這個問題,Traceview 也可能會帶偏我們的優(yōu)化方向勤讽,比如我們有一個方法脚牍,這個方法在正常情況下的耗時不大巢墅,但是加上了 Traceview 之后可能會發(fā)現(xiàn)它的耗時變成了原來的十倍甚至更多君纫。
2. Systrace
Systrace 的第一個優(yōu)點是開銷非常小
,因為它只會在我們埋點區(qū)間進行記錄叉庐,而 Traceview 是會把所有的線程的堆棧調用情況都記錄下來会喝。
Systrace 的第二個優(yōu)點是直觀
肢执,在 Systrace 中我們可以很直觀地看到 CPU 利用率的情況蔚万,當我們發(fā)現(xiàn) CPU 利用率低的時候反璃,我們可以考慮讓更多代碼以異步的方式執(zhí)行,以提高 CPU 利用率淮蜈。
3. Traceview 與 Systrace 的兩個區(qū)別
-
查看工具
Traceview 分析結果要使用 Profiler 查看梧田。
Systrace 分析結果是在瀏覽器查看 HTML 文件。
-
埋點工具類
Traceview 使用的是 Debug.startMethodTracing()讳癌。
Systrace 用的是 Trace.beginSection() 和 TraceCompat.beginSection()存皂。
5. 兩種優(yōu)化方法
常用的兩種優(yōu)化方法有兩種旦袋,這兩種是可以結合使用的。
第一種是閃屏頁商乎,在視覺上讓用戶感覺啟動速度快祭阀,第二種是異步初始化柬讨。
5.1 閃屏頁
閃屏頁是優(yōu)化啟動速度的一個小技巧踩官,雖然對實際的啟動速度沒有任何幫助境输,但是能讓用戶感覺比啟動的速度要快一些。
閃屏頁就是在 App 打開首屏 Activity 前辩越,首先顯示一張圖片黔攒,這張圖片可以是 Logo 頁督惰,等 Activity 展示出來后旅掂,再把 Theme 變回來。
冷啟動的其中一步是創(chuàng)建一個空白 Window觉阅,閃屏頁就是利用這個空白 Window 顯示占位圖典勇。
通過下面四個步驟可以實現(xiàn)閃屏頁。
- 定義閃屏圖
- 定義閃屏主題
- 設置主題
- 換回主題
1. 定義閃屏圖
第一步是在 drawable 目錄下創(chuàng)建一個 splash.xml 文件。
2. 定義閃屏主題
第二步是在 values/styles.xml 中定義一個 Splash 主題豪嚎。
3. 設置主題
第三步是在清單文件中設置 Theme谈火。
4. 換回主題
第四步是在調用 super.onCreate 方法前切換回來
5.2 異步初始化
我們這一節(jié)來看一下怎么用線程池進行異步初始化。
本節(jié)內容包括如下部分扔字,
- 異步初始化簡介
- 線程池大小
- 線程池基本用法
4.2.1 異步初始化簡介
異步優(yōu)化就是把初始化的工作分細分成幾個子任務革为,然后讓子線程分別執(zhí)行這些子任務震檩,加快初始化過程抛虏。
如果你對怎么在 Android 中實現(xiàn)多線程不了解套才,可以看一下我的另一篇文章:探索 Android 多線程優(yōu)化,在這篇文章中我對在 Android 使用多線程的方法做了一個簡單的介紹沸毁。
有些初始化代碼在子線程執(zhí)行的時候可能會出現(xiàn)問題息尺,比如要求在 onCreate() 結束前執(zhí)行完成掷倔。
這種情況我們可以考慮使用 CountDownLatch
實現(xiàn)勒葱,實在不行的時候就保留這段初始化代碼在主線程中執(zhí)行。
4.2.2 線程池大小
我們可以使用線程池來實現(xiàn)異步初始化死遭,使用線程池需要注意的是線程池大小的設置呀潭。
線程池大小要根據不同的設備設置不同的大小钠署,有的手機是 4 核的谐鼎,有的是 8 核的狸棍,如果把線程池大小設為固定數值的話是不合理的草戈。
我們可以參考 AsyncTask 中設置的線程池大小唐片,在 AsyncTask 中有 CPU_COUNT 和 CORE_POOL_SIZE牵触,CPU_COUNT 的值是設備的 CPU 核數
,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 的做法來設置線程池的大小尼摹,并把初始化的工作提交到線程池中蠢涝。
6. 改進優(yōu)化方案
上一節(jié)介紹了怎么通過線程池處理初始化任務和二,這一節(jié)我們看一下改進的異步初始化工具:啟動器(LaunchStarter)惯吕。
這一節(jié)的內容包括如下部分废登。
- 線程池實現(xiàn)的不足
- 啟動器簡介
- 啟動器工作流程
- 實現(xiàn)任務等待執(zhí)行
- 實現(xiàn)任務依賴關系
6.1 線程池實現(xiàn)的不足
通過線程池處理初始化任務的方式存在三個問題堡距。
-
代碼不夠優(yōu)雅
假如我們有 100 個初始化任務羽戒,那像上面這樣的代碼就要寫 100 遍易稠,提交 100 次任務。
-
無法限制在 onCreate() 中完成
有的第三方庫的初始化任務需要在 Application 的
onCreate()
方法中執(zhí)行完成测萎,雖然可以用CountDownLatch
實現(xiàn)等待绳泉,但是還是有點繁瑣零酪。 -
無法實現(xiàn)存在依賴關系
有的初始化任務之間存在依賴關系,比如極光推送需要設備 ID月腋,而 initDeviceId() 這個方法也是一個初始化任務榆骚。
6.2 啟動器簡介
啟動器的核心思想是充分利用多核 CPU 煌集,自動梳理任務順序碉钠。
第一步是對代碼進行任務化喊废,任務化是一個簡稱污筷,比如把啟動邏輯抽象成一個任務颓屑。
第二步是根據所有任務的依賴關系排序生成一個有向無環(huán)圖,這個圖是自動生成的罗侯,也就是對所有任務進行排序。
比如我們有個任務 A 和任務 B溪猿,任務 B 執(zhí)行前需要任務 A 執(zhí)行完钩杰,這樣才能拿到特定的數據纫塌,比如上面提到的 initDeviceId
。
第三步是多線程根據排序后的優(yōu)先級依次執(zhí)行讲弄,比如我們現(xiàn)在有三個任務 A措左、B、C怎披。
假如任務 B 依賴于任務 A,這時候生成的有向無環(huán)圖就是 ACB瓶摆,A 和 C 可以提前執(zhí)行凉逛,B 一定要排在 A 之后執(zhí)行。
6.3 啟動器工作流程
Head Task 就是所有任務執(zhí)行前要做的事情
群井,在這里初始化一些其他任務依賴的資源状飞,也可以只是打個 Log。
Tail Task 可用于執(zhí)行所有任務結束后要做的事情书斜,
诬辈,比如打印某些日志或上報數據等任務。
Idle Task 是在程序空閑時執(zhí)行的任務
荐吉。
如果我們不使用異步的方案焙糟,所有的任務都會在主線程執(zhí)行,為了讓其他線程分擔主線程的工作稍坯,我們可以把初始化的工作拆分成一個個的子任務酬荞,采用并發(fā)的方式,使用多個線程同時執(zhí)行這些子任務瞧哟。
6.4 實現(xiàn)任務等待執(zhí)行
啟動器(LaunchStarter)使用了有向無環(huán)圖實現(xiàn)任務之間的依賴關系混巧,具體的代碼可以在本文最下方找到。
使用啟動器需要完成 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 是需要在主線程執(zhí)行的任務
-
Task
Task 就是在工作線程執(zhí)行的任務负蠕。
-
needWait
InitWeexTask 中重寫了 needWait 方法蛙埂,這個方法返回 true 表示 onCreate 的執(zhí)行需要等待這個任務完成。
-
run
run() 方法中的代碼就是需要做的初始化工作
6.4.3 開始任務
定義好了任務后遮糖,我們就可以開始任務了绣的。
這里需要注意的是,如果我們的任務中有需要等待完成的任務,我們可以調用 TaskDispatcher 的 await() 方法等待這個任務完成屡江,比如 InitWeexTask芭概。
使用 await() 方法要注意的是這個方法要在 start() 方法調用后才能使用。
6.5 實現(xiàn)任務依賴關系
除了上面提到的等待功能以外惩嘉,啟動器還支持任務之間存在依賴關系罢洲,下面我們來看一個極光推送初始化任務的例子。
在這一節(jié)會講實現(xiàn)任務依賴關系的兩個步驟文黎。
- 定義任務
- 開始任務
6.5.1 定義任務
在這里我們定義兩個存在依賴關系的任務:GetDeviceIdTask 和 InitJPushTask惹苗。
首先定義 GetDeviceIdTask ,這個任務負責初始化設備 ID 臊诊。
然后定義InitJPushTask鸽粉,這個任務負責初始化極光推送 SDK,InitJPushTask 在啟動器中是尾部任務 Tail Task抓艳。
InitJPushTask
依賴于 GetDeviceIdTask
触机,所以需要重寫 dependsOn()
方法,在 dependsOn 方法中創(chuàng)建一個 Class 列表玷或,把想依賴的任務的 Class 添加到列表中并返回儡首。
6.5.2 開始任務
GetDeviceIdTask
和 InitJPushTask
這兩個任務都不需要等待 Application
的 onCreate()
方法執(zhí)行完成,所以我們這里不需要調用 TaskDispatcher
的 await()
方法偏友。
通過上面這兩個步驟就能實現(xiàn)通過啟動器實現(xiàn)任務之間的依賴關系蔬胯。
7. 延遲執(zhí)行任務
在我們應用的 Application
和 Activity
中可能存在部分優(yōu)先級不高的初始化任務,我們可以考慮把這些任務進行延遲初始化位他,比如放在列表的第一項顯示出來后再進行初始化氛濒。
常規(guī)的延遲初始化方法有兩種:onPreDraw 和 postDelayed。
除了常規(guī)方法外鹅髓,還有一種改進的延遲初始化方案:延遲啟動器舞竿。
本節(jié)包括如下內容。
-
onPreDraw
onPreDraw 指的是在列表第一項顯示后窿冯,在 onPreDraw 回調中執(zhí)行初始化任務
-
postDelayed
通過 Handler 的 postDelayed 方法延遲執(zhí)行初始化任務
延遲啟動器
7.1 onPreDraw
這一節(jié)我們來看下怎么通過 OnPreDrawListener 把任務延遲到列表顯示后再執(zhí)行骗奖。
下面是 onPreDraw 方式實現(xiàn)延遲初始化的 3 個步驟。
- 聲明回調接口
- 調用接口方法
- 在 Activity 中監(jiān)聽
第一步先聲明一個 OnFeedShowCallback
醒串。
第二步是在 Adapter 中的第一條顯示的時候調用 onFeedShow() 方法执桌。
第三步是在 Activity 中調用 setOnFeedCallback 方法。
直接在 onFeedShow 中執(zhí)行初始化任務的弊端是有可能導致滑動卡頓芜赌。
如果我們 onPreDraw 的方式延遲執(zhí)行初始化任務仰挣,假如這個任務耗時是 2 秒,那就意味著在列表顯示第一條后的 2 秒內缠沈,列表是無法滑動的椎木,用戶體驗很差违柏。
7.2 postDelayed
還有一種方式就是通過 Handler.postDelayed 方法發(fā)送一個延遲消息,比如延遲到 100 毫秒后執(zhí)行香椎。
假如在 Activity 中有 1 個 100 行的初始化方法,我們把前 10 行代碼放在 postDelayed 中延遲 100 毫秒執(zhí)行禽篱,把前 20 行代碼放在 postDelayed 中延遲 200 毫秒執(zhí)行畜伐。
這種實現(xiàn)的確緩解了卡頓的情況,但是這種實現(xiàn)存在兩個問題
-
不夠優(yōu)雅
假如按上面的例子躺率,可以分出 10 個初始化任務玛界,每一個都放在 不同的 postDelayed 中執(zhí)行,這樣寫出來的代碼不夠優(yōu)雅悼吱。
-
依舊卡頓
假如把任務延遲 200 毫秒后執(zhí)行慎框,而 200 后用戶還在滑動列表,那還是會發(fā)生卡頓后添。
7.3 延遲啟動器
7.3.1 延遲啟動器基本用法
除了上面說到的方式外笨枯,現(xiàn)在我們來說一個更好的解決方案:延遲啟動器
。
延遲啟動器利用了 IdleHandler 實現(xiàn)主線程空閑時才執(zhí)行任務遇西,IdleHandler 是 Android 提供的一個類馅精,IdleHandler 會在當前消息隊列空閑時才執(zhí)行任務,這樣就不會影響用戶的操作了粱檀。
假如現(xiàn)在 MessageQueue 中有兩條消息洲敢,在這兩條消息處理完成后,MessageQueue 會通知 IdleHandler 現(xiàn)在是空閑狀態(tài)茄蚯,然后 IdleHandler 就會開始處理它接收到的任務压彭。
DelayInitDispatcher
配合 onFeedShow 回調來使用效果更好。
下面是一段使用延遲啟動器 DelayInitDispatcher
執(zhí)行初始化任務的示例代碼渗常。
參考文獻
1. 視頻
2. 文章
- App startup time
- Android App 冷啟動優(yōu)化方案
- 使用 CPU Profiler 檢查 CPU Activity 和函數跟蹤
- Overview of Systrace