得物App安卓冷啟動(dòng)優(yōu)化-Application篇
前言
冷啟動(dòng)指標(biāo)是App體驗(yàn)中相當(dāng)重要的指標(biāo),在電商App中更是對(duì)用戶的留存意愿有著舉足輕重的影響。通常是指App進(jìn)程啟動(dòng)到首頁首幀出現(xiàn)的耗時(shí),但是在用戶體驗(yàn)的角度來看,應(yīng)當(dāng)是從用戶點(diǎn)擊App圖標(biāo),到首頁內(nèi)容完全展示結(jié)束。
將啟動(dòng)階段工作分配為任務(wù)并構(gòu)造出有向無環(huán)圖的設(shè)計(jì)已經(jīng)是現(xiàn)階段組件化App的啟動(dòng)框架標(biāo)配,但是受限于移動(dòng)端的性能瓶頸,高并發(fā)度的設(shè)計(jì)使用不當(dāng)往往會(huì)讓鎖競爭、磁盤IO阻塞等耗時(shí)問題頻繁出現(xiàn)。如何百尺竿頭更進(jìn)一步,在啟動(dòng)階段有限的時(shí)間里,將有限的資源最大化利用,在保障業(yè)務(wù)功能穩(wěn)定的前提下盡可能壓縮主線程耗時(shí),是本文將要探討的主題。
本文將介紹我們是如何通過對(duì)啟動(dòng)階段的系統(tǒng)資源做統(tǒng)一管控,按需分配和錯(cuò)峰加載等手段將得物App的線上啟動(dòng)指標(biāo)降低10%,線下指標(biāo)降低34%,并在同類型的電商App中提升至Top3。
一、指標(biāo)選擇
傳統(tǒng)的性能監(jiān)控指標(biāo),通常是以Application的attachBaseContext回調(diào)作為起點(diǎn),首頁decorView.postDraw任務(wù)執(zhí)行作為結(jié)束時(shí)間點(diǎn),但是這樣并不能統(tǒng)計(jì)到dex加載以及contentProvider初始化的耗時(shí)。
因此為了更貼近用戶真實(shí)體驗(yàn),在啟動(dòng)速度監(jiān)控指標(biāo)的基礎(chǔ)上,我們添加了一個(gè)線下的用戶體感指標(biāo),通過對(duì)錄屏文件逐幀分析,找到App圖標(biāo)點(diǎn)擊動(dòng)畫開始播放(圖標(biāo)變暗)作為起始幀,首頁內(nèi)容出現(xiàn)的第一幀作為結(jié)束幀,計(jì)算出結(jié)果作為啟動(dòng)耗時(shí)。
例:啟動(dòng)過程為03:00 - 03:88,故啟動(dòng)耗時(shí)為880ms。
圖片
圖片
二、Application優(yōu)化
App在不同的業(yè)務(wù)場景下可能會(huì)落到不同的首頁(社區(qū)/交易/H5),但是Application運(yùn)行的流程基本是固定的,且很少變更,因此Application優(yōu)化是我們的首要選擇。
得物App的啟動(dòng)框架任務(wù)在近幾年已經(jīng)先后做過多輪優(yōu)化,常規(guī)的抓trace尋找耗時(shí)點(diǎn)并異步化已經(jīng)不能帶來明顯的收益,得從鎖競爭,CPU利用率的角度去挖掘優(yōu)化點(diǎn),這類優(yōu)化可能短期收益不會(huì)特別明顯,但從長遠(yuǎn)來看能夠提前規(guī)避很多劣化問題。
1.WebView優(yōu)化
App在首次調(diào)用webview的構(gòu)造方法時(shí)會(huì)拉起系統(tǒng)對(duì)webview的初始化流程,一般會(huì)耗時(shí)200+ms,如此耗時(shí)的任務(wù)常規(guī)思路都是直接丟到子線程去執(zhí)行,但是chrome內(nèi)核中加入了非常多的線程檢查,使得webview只能在構(gòu)造它的線程中使用。
圖片
為了加速H5頁面的啟動(dòng),App通常會(huì)選擇在Application階段就初始化webview并緩存,但是webview的初始化涉及跨進(jìn)程交互和讀文件,因此CPU時(shí)間片,磁盤資源和binder線程池中任何一種不足都會(huì)導(dǎo)致其耗時(shí)膨脹,而Application階段任務(wù)繁多,恰恰很容易出現(xiàn)以上資源短缺的情況。
圖片
因此我們將webview拆分成三個(gè)步驟,分散到啟動(dòng)的不同階段來執(zhí)行,這樣可以降低因?yàn)楦偁庂Y源導(dǎo)致的耗時(shí)膨脹問題,同時(shí)還可以大幅度降低出現(xiàn)ANR的幾率。
圖片
1.1 任務(wù)拆分
a. provider預(yù)加載
WebViewFactoryProvider是用于和webview渲染進(jìn)程交互的接口類,webview初始化的第一步就是加載系統(tǒng)webview的apk文件,構(gòu)建出classloader并反射創(chuàng)建了WebViewFactoryProvider的靜態(tài)實(shí)例,這一操作并沒有涉及線程檢查,因此我們可以直接將其交給子線程執(zhí)行。
圖片
b. 初始化webview渲染進(jìn)程
這一步對(duì)應(yīng)著chrome內(nèi)核中的WebViewChromiumAwInit.ensureChromiumStartedLocked()方法,是webview初始化最耗時(shí)的部分,但是和第三步是連續(xù)執(zhí)行的。走碼分析發(fā)現(xiàn)WebViewFactoryProvider暴露給應(yīng)用的接口中,getStatics這個(gè)方法會(huì)正好會(huì)觸發(fā)ensureChromiumStartedLocked方法。
至此,我們就可以通過執(zhí)行WebSettings.getDefaultUserAgent()來達(dá)到僅初始化webview渲染進(jìn)程的目的。
圖片
圖片
圖片
c. 構(gòu)造webview
即new Webview()
1.2 任務(wù)分配
為了最大程度縮短主線程耗時(shí),我們的任務(wù)安排如下:
a. provider預(yù)加載,可以異步執(zhí)行,且沒有任何前置依賴,因此放在Application階段最早的時(shí)間點(diǎn)異步執(zhí)行即可。
b. 初始化webview渲染進(jìn)程,必須在主線程,因此放到首頁首幀結(jié)束之后。
c. 構(gòu)造webview,必須在主線程,在第二步完成時(shí)post到主線程執(zhí)行。這樣可以確保和第二步不在同一個(gè)消息中,降低ANR的幾率。
圖片
1.3 小結(jié)
盡管我們已經(jīng)將webview初始化拆分為了三個(gè)部分,但是耗時(shí)占比最高的第二步在低端機(jī)或者極端情況還是可能觸達(dá)ANR的閾值,因此我們做了一些限制,例如當(dāng)前設(shè)備會(huì)統(tǒng)計(jì)并記錄webview完整初始化的耗時(shí),僅當(dāng)耗時(shí)低于配置下發(fā)的閾值時(shí),開啟上述的分段執(zhí)行優(yōu)化。
App如果是通過推送、投放等渠道打開,一般打開的頁面大概率是H5營銷頁,因此這類場景不適用于上述的分段加載,所以需要hook主線程的messageQueue,解析出啟動(dòng)頁面的intent信息,再做判斷。
受限于開屏廣告功能,我們目前只能對(duì)無開屏廣告的啟動(dòng)場景開啟此優(yōu)化,后續(xù)將計(jì)劃利用廣告倒計(jì)時(shí)的間隙執(zhí)行步驟2,來覆蓋有開屏廣告的場景。
圖片
2.ARouter優(yōu)化
在當(dāng)下組件化流行的時(shí)代,路由組件已經(jīng)幾乎是所有大型安卓App必備的基礎(chǔ)組件,目前得物使用的是開源的ARouter框架。
ARouter 框架的設(shè)計(jì)是它默認(rèn)會(huì)將注解中注冊path路徑中第一個(gè)路由層級(jí) (例如 "/trade/homePage"中的trade)作為該路由信息所的Group, 相同Group路徑的路由信息會(huì)合并到最終生成的同一個(gè)類 的注冊函數(shù)中進(jìn)行同步注冊。在大型項(xiàng)目中,對(duì)于復(fù)雜業(yè)務(wù)線同一個(gè)Group下可能包含上百個(gè)注冊信息,注冊邏輯執(zhí)行過程耗時(shí)較長,以得物為例,路由最多的業(yè)務(wù)線在初始化路由上的耗時(shí)已經(jīng)來到了150+ms。
圖片
路由的注冊邏輯本身是懶加載的,即對(duì)應(yīng)Group之下的首個(gè)路由組件被調(diào)用時(shí)會(huì)觸發(fā)路由注冊操作。然而ARouter通過SPI(服務(wù)發(fā)現(xiàn))機(jī)制來幫助業(yè)務(wù)組件對(duì)外暴露一些接口,這樣不需要依賴業(yè)務(wù)組件就可以調(diào)用一些業(yè)務(wù)層的視線,在開發(fā)這些服務(wù)時(shí),開發(fā)者一般會(huì)習(xí)慣性的按照其所屬的組件為其設(shè)置路由path,這使得首次構(gòu)造這些服務(wù)的時(shí)候也會(huì)觸發(fā)同一個(gè)Group下的路由加載。
而在Application階段肯定需要用到業(yè)務(wù)模塊的服務(wù)中的一些接口,這就會(huì)提前觸發(fā)路由注冊操作,雖然這一操作可以在異步線程執(zhí)行,但是Application階段的絕大部分工作都需要訪問這些服務(wù),所以當(dāng)這些服務(wù)在首次構(gòu)造的耗時(shí)增大時(shí),整體的啟動(dòng)耗時(shí)勢必會(huì)隨之增長。
2.1 ARouter Service路由分離
ARouter采用SPI設(shè)計(jì)的本意是為了解耦,Service的作用也應(yīng)該只是提供接口,所以應(yīng)當(dāng)新增一個(gè)空實(shí)現(xiàn)的Service專門用于觸發(fā)路由加載,而原先的Service則需要更換一個(gè)Group,后續(xù)只用于提供接口,如此一來Application階段的其他任務(wù)就不需要等待路由加載任務(wù)的完成。
圖片
2.2 ARouter支持并發(fā)裝載路由
我們在實(shí)現(xiàn)了路由分離之后,發(fā)現(xiàn)現(xiàn)有的熱點(diǎn)路由裝載耗時(shí)總和是大于Application耗時(shí),而為了保證在進(jìn)入閃屏頁之前完成對(duì)路由的加載,主線程不得不sleep等待路由裝載完畢。
分析可知ARouter的路由裝載方法加了類鎖,因?yàn)樗枰獙⒙酚裳b載到倉庫類中的map,這些map是線程不安全的HashMap,相當(dāng)于所有的路由裝載操作其實(shí)都是在串行執(zhí)行,而且存在鎖競爭的情況,最終導(dǎo)致耗時(shí)累加大于Application耗時(shí)。
圖片
圖片
分析trace可知耗時(shí)主要來自頻繁調(diào)用裝載路由的loadInto操作,再分析這里鎖的作用,可知加類鎖是主要是為了確保對(duì)倉庫WareHouse中map操作的線程安全。
圖片
因此我們可以將類鎖降級(jí)對(duì)GroupMeta這個(gè)class對(duì)象加鎖(這個(gè)class是ARouter apt生成的類,對(duì)應(yīng)apk中的ARouter$$Provider$$xxx類),來確保路由裝載過程中的線程安全,至于在此之前對(duì)map操作的線程安全問題,則完全可以通過將這些map替換為concurrentHashMap解決,在極端并發(fā)情況下會(huì)有一些線程安全問題,也可以按照?qǐng)D中添加判空來解決。
圖片
圖片
至此,我們就實(shí)現(xiàn)了路由的并發(fā)裝載,隨后我們根據(jù)木桶效應(yīng)對(duì)要預(yù)載的service進(jìn)行合理分組,再放到協(xié)程中并發(fā)執(zhí)行,確保最終整體耗時(shí)最短。
圖片
圖片
3.鎖優(yōu)化
Application階段執(zhí)行的任務(wù)多為基礎(chǔ)SDK的初始化,其運(yùn)行的邏輯通常相對(duì)獨(dú)立,但是SDK之間會(huì)有依賴關(guān)系(例如埋點(diǎn)庫會(huì)依賴于網(wǎng)絡(luò)庫),且大部分都會(huì)涉及讀文件,加載so庫等操作,Application階段為了壓縮主線程的耗時(shí),會(huì)盡可能地將耗時(shí)操作放到子線程中并發(fā)運(yùn)行,充分利用CPU時(shí)間片,但是這也不可避免的會(huì)導(dǎo)致一些鎖競爭的問題。
3.1 Load so鎖
System.loadLibrary()方法用于加載當(dāng)前apk中的so庫,這個(gè)方法對(duì)Runtime對(duì)象加了鎖,相當(dāng)于一個(gè)類鎖。
基礎(chǔ)SDK在設(shè)計(jì)上通常會(huì)將load so的操作寫到類的靜態(tài)代碼塊中,確保在SDK初始化代碼執(zhí)行之前就準(zhǔn)備好了so庫。如果這個(gè)基礎(chǔ)SDK恰巧是網(wǎng)絡(luò)庫這類基礎(chǔ)庫,會(huì)被很多其他SDK調(diào)用,就會(huì)出現(xiàn)多個(gè)線程同時(shí)競爭這個(gè)鎖的情況。那么在最壞的情況下,此時(shí)IO資源緊張,讀so文件變慢,并且主線程是鎖等待隊(duì)列中最后一個(gè),那么啟動(dòng)耗時(shí)將遠(yuǎn)超預(yù)期。
圖片
為此,我們需要將loadSo的操作統(tǒng)一管控并收斂到一個(gè)線程中執(zhí)行,強(qiáng)制他們以串行的方式運(yùn)行,這樣就可以避免以上情況的出現(xiàn)。值得一提的是,前面webview的provider預(yù)加載的過程中也會(huì)加載webview.apk中的so文件,因此需要確保preloadProvider的操作也放到這個(gè)線程。
so的加載操作會(huì)觸發(fā)native層的JNI_onload方法,一些so可能會(huì)在其中執(zhí)行一些初始化工作,因此我們不能直接調(diào)用System.loadLibrary()方法來進(jìn)行so加載,否則可能會(huì)重復(fù)初始化出現(xiàn)問題。
我們最終采用了類加載的方式,即將這些so加載的代碼全部挪到相關(guān)類的靜態(tài)代碼塊中,然后再去觸發(fā)這些類的加載即可,利用類加載的機(jī)制確保這些so的加載操作不會(huì)重復(fù)執(zhí)行,同時(shí)這些類加載的順序也要按照這些so使用的順序來編排。
圖片
除此之外,so的加載任務(wù)不建議和其他需要IO資源的任務(wù)并發(fā)執(zhí)行,在得物App中實(shí)測這兩種情況下該任務(wù)的耗時(shí)相差巨大。
4.啟動(dòng)框架優(yōu)化
目前常見的啟動(dòng)框架設(shè)計(jì)是將啟動(dòng)階段的工作分配到一組任務(wù)節(jié)點(diǎn)中,再由這些任務(wù)節(jié)點(diǎn)的依賴關(guān)系構(gòu)造出一個(gè)有向無環(huán)圖,但是隨著業(yè)務(wù)迭代,一些歷史遺留的任務(wù)依賴已經(jīng)沒有存在的必要,但是他會(huì)拖累整體的啟動(dòng)速度。
啟動(dòng)階段大部分工作都是基礎(chǔ)SDK的初始化,他們之間往往有著復(fù)雜的依賴關(guān)系,而我們在做啟動(dòng)優(yōu)化時(shí)為了壓縮主線程的耗時(shí),通常都會(huì)找出主線程的耗時(shí)任務(wù)并丟到子線程去執(zhí)行,但是在依賴關(guān)系復(fù)雜的Application階段,如果只是將其丟到異步執(zhí)行未必能有預(yù)期的收益。
我們在做完webview優(yōu)化之后發(fā)現(xiàn)啟動(dòng)耗時(shí)并沒有和預(yù)期一樣直接減少了webview初始化的耗時(shí),而是只有預(yù)期的一半左右,經(jīng)分析發(fā)現(xiàn)我們的主線程任務(wù)依賴著子線程的任務(wù),所以當(dāng)子線程任務(wù)沒有執(zhí)行完時(shí),主線程會(huì)sleep等待。
并且webview之所以放在這個(gè)時(shí)間點(diǎn)初始化不是因?yàn)橛幸蕾囅拗七@它,而是因?yàn)檫@段時(shí)間主線程正好有一段比較長的sleep時(shí)間可以利用起來,但是異步的任務(wù)工作量是遠(yuǎn)大于主線程的,即便是七個(gè)子線程并發(fā)在跑,其耗時(shí)也是大于主線程的任務(wù)。
因此想進(jìn)一步擴(kuò)大收益,就得對(duì)啟動(dòng)框架中的任務(wù)依賴關(guān)系做優(yōu)化。
圖片
圖片
以上第一張圖為優(yōu)化之前得物App啟動(dòng)階段任務(wù)的有向無環(huán)圖,紅框表示該任務(wù)在主線程執(zhí)行。我們著重關(guān)注阻塞主線程任務(wù)執(zhí)行的任務(wù)。
可以觀察到主線程任務(wù)的依賴鏈路上存在幾個(gè)出口和入口特別多的任務(wù),出口多表明這類任務(wù)通常是非常重要的基礎(chǔ)庫(例如圖中的網(wǎng)絡(luò)庫),而入口多表明這個(gè)任務(wù)的前置依賴太多,他開始執(zhí)行的時(shí)間點(diǎn)波動(dòng)較大。這兩點(diǎn)結(jié)合起來就說明這個(gè)任務(wù)執(zhí)行結(jié)束的時(shí)間點(diǎn)很不穩(wěn)定,并且將直接影響到后續(xù)主線程的任務(wù)。
這類任務(wù)優(yōu)化的思路主要是:
拆解任務(wù)自身,將可以提前執(zhí)行或者延后執(zhí)行的操作分出去,但是分出去之前要考慮到對(duì)應(yīng)的時(shí)間段還有沒有時(shí)間片余量,或者會(huì)不會(huì)加重IO資源競爭的情況出現(xiàn);
優(yōu)化該任務(wù)的前置任務(wù),讓該任務(wù)執(zhí)行結(jié)束的時(shí)間點(diǎn)盡可能提早,就可以降低后續(xù)任務(wù)等待該任務(wù)的耗時(shí);
移除非必要的依賴關(guān)系,例如埋點(diǎn)庫初始化只是需要注冊一個(gè)監(jiān)聽器到網(wǎng)絡(luò)庫,并非發(fā)起網(wǎng)絡(luò)請(qǐng)求。(推薦)
可以看到我們在優(yōu)化之后的第二張有向無環(huán)圖里,任務(wù)的依賴層級(jí)明顯變少,入口和出口特別多的任務(wù)也都基本不再出現(xiàn)。
圖片
圖片
對(duì)比優(yōu)化前后的trace,也可以看到子線程的任務(wù)并發(fā)度明顯提高,但是任務(wù)并發(fā)度并不是越高越好,在時(shí)間片本身就不足的低端機(jī)上并發(fā)度越高表現(xiàn)可能會(huì)越差,因?yàn)楦菀壮鲦i競爭,IO等待之類的問題,因此要適當(dāng)留下一定空隙,并在中低端機(jī)上進(jìn)行充分的性能測試之后再上線,或者針對(duì)高中低端機(jī)器使用不同的任務(wù)編排。
三、首頁優(yōu)化
1.通用布局耗時(shí)優(yōu)化
系統(tǒng)解析布局是通過inflate方法讀取布局xml文件并解析構(gòu)建出view樹,這一過程涉及IO操作,很容易受到設(shè)備狀態(tài)影響,因此我們可以在編譯期通過apt解析布局文件生成對(duì)應(yīng)的view構(gòu)建類。然后在運(yùn)行時(shí)提前異步執(zhí)行這些類的方法來構(gòu)建并組裝好view樹,這樣可以直接優(yōu)化掉頁面inflate的耗時(shí)。
圖片
圖片
2.消息調(diào)度優(yōu)化
在啟動(dòng)階段我們通常會(huì)注冊一些ActivityLifecycleListener來監(jiān)聽頁面生命周期,或者是往主線程post了一些延時(shí)任務(wù),如果這些任務(wù)中有耗時(shí)操作,將會(huì)影響到啟動(dòng)速度,因此可以通過hook主線程的消息隊(duì)列,將頁面生命周期回調(diào)和頁面繪制相關(guān)的msg移動(dòng)到消息隊(duì)列的隊(duì)頭,這樣就可以加快首頁首幀內(nèi)容展示的速度。
圖片
詳情可期待本系列后續(xù)內(nèi)容。
四、穩(wěn)定性
性能優(yōu)化對(duì)App只能算作錦上添花,穩(wěn)定性才是生命紅線,而啟動(dòng)優(yōu)化改造的又都是執(zhí)行時(shí)機(jī)非常早的Application階段,穩(wěn)定性風(fēng)險(xiǎn)程度非常高,因此務(wù)必要在準(zhǔn)備好崩潰防護(hù)的前提下做優(yōu)化,即便有不可避免的穩(wěn)定性問題,也要將負(fù)面影響降到最低。
1.崩潰防護(hù)
由于啟動(dòng)階段執(zhí)行的任務(wù)都是重要的基礎(chǔ)庫初始化,因此發(fā)生崩潰時(shí)將異常識(shí)別并吃掉的意義不大,因?yàn)榇蟾怕蕰?huì)導(dǎo)致后續(xù)崩潰或功能異常,因此我們主要的防護(hù)工作都是發(fā)生問題之后的止血。
配置中心SDK的設(shè)計(jì)通常都是從本地文件中讀出緩存的配置使用,待接口請(qǐng)求成功后再刷新。所以如果當(dāng)啟動(dòng)階段命中了配置之后發(fā)生了crash,是拉不到新配置的。這種情況下只能清空App緩存或者卸載重裝,會(huì)造成非常嚴(yán)重的用戶流失。
圖片
- 崩潰回退
對(duì)所有改動(dòng)點(diǎn)加上try-catch保護(hù),捕捉到異常之后上報(bào)埋點(diǎn)并往MMKV中寫入崩潰標(biāo)記位,這樣該設(shè)備在當(dāng)前版本下都不會(huì)再開啟啟動(dòng)優(yōu)化相關(guān)的變更,隨后再拋出原異常讓他崩潰掉。至于native crash則是在Crash監(jiān)控的native崩潰回調(diào)里執(zhí)行同樣操作即可。
圖片
- 運(yùn)行狀態(tài)檢測
Java Crash我們可以通過注冊u(píng)nCaughtExceptionHandler來捕捉到,但是native crash則需要借助crash監(jiān)控SDK來捕捉,但是crash監(jiān)控未必能在啟動(dòng)最早的時(shí)間點(diǎn)初始化,例如Webview的Provider的預(yù)加載,以及so庫的預(yù)加載都是早于crash監(jiān)控,而這些操作都涉及native層的代碼。
為了規(guī)避這種場景下的崩潰風(fēng)險(xiǎn),我們可以在Application的起始點(diǎn)埋入MMKV標(biāo)記位,在結(jié)束點(diǎn)改為另一個(gè)狀態(tài),這樣一些執(zhí)行時(shí)間早于配置中心的代碼就可以通過獲取這個(gè)標(biāo)記位來判斷上一次運(yùn)行是否正常,如果上次啟動(dòng)發(fā)生了一些未知的崩潰(例如發(fā)生在crash監(jiān)控初始化之前的native崩潰),那么通過這個(gè)標(biāo)記位就可以及時(shí)關(guān)閉掉啟動(dòng)優(yōu)化的變更。
結(jié)合崩潰之后自動(dòng)重啟的操作,在用戶視角其實(shí)是觀察不到閃退的,只是會(huì)感覺到啟動(dòng)的耗時(shí)約是平時(shí)的1-2倍。
圖片
- 配置有效期
線上的技改變更通常都會(huì)配置采樣率,結(jié)合隨機(jī)數(shù)實(shí)現(xiàn)逐漸放量,但是配置下發(fā)SDK的設(shè)計(jì)通常都是默認(rèn)取上次的本地緩存,在發(fā)生線上崩潰等故障時(shí),盡管及時(shí)回滾了配置,但是緩存的設(shè)計(jì)會(huì)導(dǎo)致用戶還會(huì)因?yàn)榫彺嬖庥鲋辽僖淮蔚谋罎ⅰ?/p>
為此,我們可以為每一個(gè)開關(guān)配置加一個(gè)配套的過期時(shí)間戳,限制當(dāng)前放量的開關(guān)只在該時(shí)間戳之前生效,這樣在遇到線上崩潰等故障時(shí)確??梢约皶r(shí)止血,而且時(shí)間戳的設(shè)計(jì)也可以避免線上配置生效的滯后性導(dǎo)致的crash。
圖片
用戶視角下,添加配置有效期前后對(duì)比:
圖片
五、總結(jié)
至此,我們已經(jīng)對(duì)安卓App中比較通用的冷啟動(dòng)耗時(shí)案例做了分析,但是啟動(dòng)優(yōu)化最大的痛點(diǎn)往往還是App自身的業(yè)務(wù)代碼,應(yīng)當(dāng)結(jié)合業(yè)務(wù)需求合理的進(jìn)行任務(wù)分配,如果一味的靠預(yù)加載,延遲加載和異步加載是不能從根本上解決耗時(shí)問題的,因?yàn)楹臅r(shí)并沒有消失只是轉(zhuǎn)移,隨之而來的可能是低端機(jī)啟動(dòng)劣化或功能異常。
做性能優(yōu)化不僅需要站在用戶的視角,還要有全局觀,如果因?yàn)閱?dòng)指標(biāo)算是首頁首幀結(jié)束就把耗時(shí)任務(wù)都丟到首幀之后,勢必會(huì)造成用戶后續(xù)的體驗(yàn)有卡頓甚至ANR。所以在拆分任務(wù)時(shí)不僅需要考慮是否會(huì)和與其并發(fā)的任務(wù)競爭資源,還需要考慮啟動(dòng)各個(gè)階段以及啟動(dòng)后一段時(shí)間內(nèi)的功能穩(wěn)定性和性能是否會(huì)受之影響,并且需要在高中低端機(jī)器上都驗(yàn)證下,至少要確保都沒有劣化的表現(xiàn)。
1.防劣化
啟動(dòng)優(yōu)化絕不是一次性的工作,它需要長時(shí)間的維護(hù)和打磨,基礎(chǔ)庫的一次技改可能就會(huì)讓指標(biāo)一夜回到解放前,因此防劣化必須要盡早落地。
通過在關(guān)鍵點(diǎn)添加埋點(diǎn),可以做到在發(fā)現(xiàn)線上指標(biāo)劣化時(shí)迅速定位到劣化代碼大概位置(例如xxActivity的onCreate)并告警,這樣不僅可以幫助研發(fā)迅速定位問題,還可以避免線上特定場景指標(biāo)劣化線下無法復(fù)現(xiàn)的情況,因?yàn)閱未螁?dòng)的耗時(shí)波動(dòng)范圍最高能有20%,如果直接去抓trace分析可能連劣化的大概范圍都難以定位。
例如兩次啟動(dòng)做trace對(duì)比時(shí),其中一次因?yàn)橛龅絀O阻塞導(dǎo)致某次讀文件的操作都明顯變慢,而另一次IO正常,這就會(huì)誤導(dǎo)開發(fā)者去分析這些正常的代碼,而實(shí)際導(dǎo)致劣化的代碼可能因?yàn)椴▌?dòng)正好被掩蓋。
2.展望
對(duì)于通過點(diǎn)擊圖標(biāo)啟動(dòng)的普通場景,默認(rèn)會(huì)在Application執(zhí)行完整的初始化工作,但是一些層級(jí)比較深的功能,例如客服中心,編輯收貨地址這類,即使用戶以最快速度直接進(jìn)入這些頁面,也是需要至少1s以上的操作時(shí)間,所以這些功能相關(guān)的初始化工作也是可以推遲到Application之后的,甚至改為懶加載,視具體功能的重要性而定。
通過投放,push來做召回/拉新的啟動(dòng)場景通常占比較少,但是其業(yè)務(wù)價(jià)值要遠(yuǎn)大于普通場景。由于目前啟動(dòng)耗時(shí)主要來源于webview初始化以及一些首頁預(yù)載相關(guān)的任務(wù),如果啟動(dòng)落地頁并不需要所有基礎(chǔ)庫(例如H5頁面),那么這些我們就可以將它不需要的任務(wù)統(tǒng)統(tǒng)延遲加載,這樣啟動(dòng)速度可以得到大幅度增長,做到真正意義上的秒開。