卡頓優(yōu)化(下)

1. 消息隊(duì)列

基于消息隊(duì)列實(shí)現(xiàn),通過替換 Looper 的 Printer 實(shí)現(xiàn)。

Looper

問題:線上開啟這個(gè)監(jiān)控模塊,快速滑動(dòng)時(shí)平均幀率起碼降低 5 幀,上圖中所示的大量字符串拼接導(dǎo)致性能損耗嚴(yán)重。

參考 BlockCanary

另一個(gè)方案:可以通過一個(gè)監(jiān)控線程,每隔 1 秒向主線程消息隊(duì)列的頭部插入一條空消息。假設(shè) 1 秒后這個(gè)消息沒有被主線程消費(fèi)掉,說明阻塞消息運(yùn)行的時(shí)間在 0 ~ 1 秒之間。如果我們需要監(jiān)控 3 秒卡頓,那么在第 4 次輪詢中頭部消息依然沒有被消費(fèi)的話,就可以確定主線程出現(xiàn)了一次 3 秒以上的卡頓。

監(jiān)控線程

2. 插樁

基于消息隊(duì)列的卡段監(jiān)控并不準(zhǔn)確,正在運(yùn)行的函數(shù)有可能并不是真正耗時(shí)的函數(shù)。

假設(shè)一個(gè)消息循環(huán)里面順序執(zhí)行了 A、B、C 三個(gè)函數(shù),當(dāng)整個(gè)消息執(zhí)行超過 3 秒時(shí),因?yàn)楹瘮?shù) A 和 B 已經(jīng)執(zhí)行完畢,只能得到的正在執(zhí)行的函數(shù) C 的堆棧,事實(shí)上它可能并不耗時(shí)。

卡頓函數(shù)

能否直接利用 Android Runtime 函數(shù)調(diào)用的回調(diào)事件,做一個(gè)自定義的 Traceview++ 呢?可以拿到整個(gè)卡頓過程所有運(yùn)行函數(shù)的耗時(shí),可以明確知道其實(shí)函數(shù) A 和 B 才是造成卡頓的主要原因。

Traceview++

可以在函數(shù)入口和出口加入耗時(shí)監(jiān)控代碼,但是需要考慮的細(xì)節(jié)很多。

  • 避免方法數(shù)暴增。
    在函數(shù)的入口和出口應(yīng)該插入相同的函數(shù),在編譯時(shí)提前給代碼中每個(gè)方法分配一個(gè)獨(dú)立的 ID 作為參數(shù)。

  • 過濾簡(jiǎn)單的函數(shù)
    過濾一些類似 return 、i++ 這樣的簡(jiǎn)單函數(shù),并且支持黑名單配置。對(duì)一些調(diào)用非常頻繁的函數(shù),需要添加到黑名單中來降低整個(gè)方案對(duì)性能的損耗。

插樁

參考騰訊開源庫 matrix

不管我們使用哪種卡頓監(jiān)控方法,最后都可以得到卡頓時(shí)的堆棧和當(dāng)時(shí) CPU 運(yùn)行的一些信息。大部分卡頓問題都比較好定位,例如主線程執(zhí)行一個(gè)耗時(shí)任務(wù),讀一個(gè)非常大的文件或者是執(zhí)行網(wǎng)絡(luò)請(qǐng)求等。

3. 其他監(jiān)控

  1. 幀率

業(yè)界都使用 Choreographer 來監(jiān)控應(yīng)用的幀率,需要排除掉沒有操作的情況,只在**界面存在繪制的時(shí)候才做統(tǒng)計(jì)。

可以通過 addOnDrawListener 實(shí)現(xiàn):

getWindow().getDecorView().getViewTreeObserver().addOnDrawListener

對(duì)用戶來說,感覺最明顯的是連續(xù)丟幀情況,將連續(xù)丟幀超過 700 毫秒定義為凍幀,也就是連續(xù)丟幀 42 幀以上。

因此可以統(tǒng)計(jì)凍幀率。凍幀率就是計(jì)算發(fā)生凍幀時(shí)間在所有時(shí)間的占比。

  1. 生命周期監(jiān)控

Activity、Service、Receiver 組件生命周期的耗時(shí)和調(diào)用次數(shù)也是我們重點(diǎn)關(guān)注的性能問題。例如,Acitivity 的 onCreate() 不應(yīng)該超過1秒。

life

每個(gè)組件各個(gè)生命周期的調(diào)用次數(shù)也是非常有參考價(jià)值指標(biāo)的,可以查看是否頻繁的拉起某個(gè)組件。

  1. 線程監(jiān)控

Java 線程管理令人頭疼,應(yīng)用啟動(dòng)已經(jīng)創(chuàng)建了大量線程,而且大部分線程都沒有經(jīng)過線程池管理,另一方面某些線程優(yōu)先級(jí)或者活躍程度比較高,占用了過多的 CPU,這會(huì)降低主線程 UI 響應(yīng)能力,我們需要對(duì)這些線程做重點(diǎn)的優(yōu)化。

對(duì)于 Java 線程,總的來說需要監(jiān)控以下兩點(diǎn)

  • 線程數(shù)量。需要監(jiān)控線程數(shù)量的多少,以及創(chuàng)建線程的方式。例如有沒有使用我們特有的線程池,這塊可以通過 got hook 線程的 nativeCreate() 函數(shù),主要用于進(jìn)行線程收斂、也就是減少線程數(shù)量。
  • 線程時(shí)間。監(jiān)控線程的用戶時(shí)間 utime、系統(tǒng)時(shí)間 stime 和優(yōu)先級(jí)。主要看哪些線程 utime + stime 時(shí)間比較多,占用了過多的 CPU。


    thread time

導(dǎo)致卡頓的原因會(huì)有很多,比如函數(shù)非常耗時(shí)、I/O 非常慢、線程間的競(jìng)爭(zhēng)或者鎖等。其實(shí)很多時(shí)候卡頓問題并不難解決,相比較解決來說,更困難的是如何快速發(fā)現(xiàn)這些卡頓點(diǎn),以及通過更多的輔助信息找到真正的卡頓原因。

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

相關(guān)閱讀更多精彩內(nèi)容

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