背景
在上線 ANR 監(jiān)控平臺(tái)后,線上收集到了較多的ANR日志 ,從火焰圖信息上看,函數(shù)阻塞在了QueuedWork 相關(guān)函數(shù)上 ,本文主要介紹的這一現(xiàn)象的原因以及如何解決這一問(wèn)題。
本文介紹的解決方案,已放到github 上https://github.com/Knight-ZXW/SpWaitKiller , 供參考實(shí)現(xiàn)
SP任務(wù) 阻塞主線程導(dǎo)致ANR的原理
首先簡(jiǎn)單介紹下 QueuedWork這個(gè)類,QueuedWork主要是用來(lái)執(zhí)行和跟蹤一些進(jìn)程全局的工作,但目前主要調(diào)度的SP相關(guān)的異步任務(wù) ,當(dāng)調(diào)用 SharedPreferences的 applay方法時(shí),其所要執(zhí)行的SP文件變更操作會(huì)被轉(zhuǎn)化成對(duì)應(yīng)的任務(wù) 并調(diào)用QueuedWork.queue 方法發(fā)送到QueuedWork類中進(jìn)行執(zhí)行。
同時(shí),系統(tǒng)為了保證這些任務(wù)在一些關(guān)鍵動(dòng)作觸發(fā)前(如 頁(yè)面跳轉(zhuǎn)啟動(dòng)Activity) 已經(jīng)被執(zhí)行完成, 設(shè)計(jì)了一個(gè)等待機(jī)制。簡(jiǎn)單描述一下這個(gè)機(jī)制
?SP的apply操作 會(huì)產(chǎn)生2個(gè)Runnable對(duì)象,其實(shí)一個(gè)為具體文件修改的工作任務(wù)(Work Runnable),另一個(gè)為 等待任務(wù)(awitCommit), 當(dāng)工作任務(wù)被執(zhí)行完成時(shí),會(huì)通過(guò)一個(gè)CountDownLatch[1]對(duì)象通知 等待任務(wù),而awitCommit內(nèi)部主要就是等待這個(gè)CountDownLatch計(jì)數(shù)器
?工作任務(wù)最終會(huì)通過(guò) QueueWork的queue被發(fā)送到異步線程執(zhí)行
?等待任務(wù)(awitCommit)會(huì)通過(guò)QueueWork的addFinisher函數(shù)被添加到 QueueWork內(nèi)部的等待隊(duì)列中
?最后,在系統(tǒng)的一些關(guān)鍵流程,比如ActivityThread執(zhí)行handleStopActivity時(shí)會(huì)通過(guò)waitToFinish保證這些異步任務(wù)都已經(jīng)被執(zhí)行完成
而此時(shí)如果系統(tǒng)資源(cpu、io)比較緊張、或者是提交的異步任務(wù)較多,則可能導(dǎo)致onStop執(zhí)行時(shí)間較長(zhǎng),從而導(dǎo)致ANR。
另外 在Android 8.0.0 以上的版本,google 在 waitToFinish的實(shí)現(xiàn)中做了一些改動(dòng),在原有的等待所有異步任務(wù)執(zhí)行完成的基礎(chǔ)上,會(huì)通過(guò)調(diào)用 processPendingWork 將QueueWork中未執(zhí)行的任務(wù)直接取出在當(dāng)前線程直接執(zhí)行。這個(gè)變更的原因是waitToFinish調(diào)用的時(shí)機(jī)一般是主線程, 主線程的優(yōu)先級(jí)會(huì)比QueueWork內(nèi)部線程的優(yōu)先級(jí)更高,因此未執(zhí)行的任務(wù)重新分發(fā)到主線程直接執(zhí)行,提高執(zhí)行效率。
SP阻塞問(wèn)題解決
反射替換 finishers隊(duì)列對(duì)象
解決SP 造成的阻塞問(wèn)題,有很多方式,比如將應(yīng)用內(nèi)使用SP的代碼 通過(guò)字節(jié)碼插樁改為MMKV或其他更高效鍵值存儲(chǔ)庫(kù)實(shí)現(xiàn)。另一種方式是 字節(jié)跳動(dòng)在一篇分享的文章[2]中提出的 通過(guò)代理替換Queuework類內(nèi)部的sFinishers對(duì)象,保證執(zhí)行 waitToFinish時(shí) 隊(duì)列長(zhǎng)度為空實(shí)現(xiàn)的。
這里 sFinishers.poll 函數(shù)的調(diào)用在整個(gè)類中,只有這一個(gè)地方調(diào)用,因此 通過(guò)動(dòng)態(tài)代理替換該對(duì)象,重寫(xiě)poll函數(shù)實(shí)現(xiàn) 使其總是返回null對(duì)象,并不會(huì)對(duì)其他流程造成影響
sFinishers對(duì)象在不同的版本具體使用的類不同
?android 8.0以下版本使用的是 ConcurrentLinkedQueue
?android 8.0 之后 使用的是LinkedList
以8.0以上版本為例,創(chuàng)建一個(gè)代理類,修改poll的實(shí)現(xiàn)
再通過(guò)反射替換掉該實(shí)現(xiàn)類。
解決processPendingWork調(diào)用
之前介紹過(guò)在 8.0及以上版本 調(diào)用waitToFinish 時(shí),除了在執(zhí)行等待finishers隊(duì)列之前,會(huì)在當(dāng)前線程直接調(diào)用processPendingWork函數(shù)。以下是程序運(yùn)行時(shí)主線程 和 異步工作線程之間的關(guān)系圖。
因此processPendingWork可能在主線程執(zhí)行 也可能在異步線程中執(zhí)行, 在 8.0~11.0下 processPendingWork的調(diào)用可能存在兩個(gè)block點(diǎn)
1.異步線程正在執(zhí)行 processPendingWork函數(shù),異步工作線程持有 sProcessingWork鎖,因此主線程執(zhí)行 processPendingWork時(shí) ,因?yàn)楂@取不到 sProcessingWork鎖 ,出現(xiàn)鎖等待
2.當(dāng)主線程成功獲取到 sProcessingWork鎖,調(diào)用clone函數(shù)時(shí),sWork隊(duì)列中 確實(shí)存在未執(zhí)行的任務(wù),這部分任務(wù)將在主線程直接執(zhí)行,如果此時(shí)IO操作較慢,則主線程因?yàn)槁齀O出現(xiàn)阻塞甚至ANR
由于這兩個(gè)原因,因此只代理clone函數(shù)是不可行的,因?yàn)槿绻惒骄€程正在執(zhí)行processPendingWork函數(shù),并且執(zhí)行得比較慢,那么主線程還是會(huì)出現(xiàn)等待的情況。最終 采取的方式是,無(wú)論是在哪個(gè)線程執(zhí)行,代理的clone函數(shù)都返回空隊(duì)列,這樣保證了processPendingWork的調(diào)用不會(huì)出現(xiàn)互相阻塞,相當(dāng)于processPendingWork實(shí)際上沒(méi)有執(zhí)行任何操作, 并且通過(guò)反射獲取QueuedWork的mHandler的Looper對(duì)象,創(chuàng)建一個(gè)新的Hander,并將sWork中的任務(wù)提交到這個(gè)Handler去執(zhí)行,從而實(shí)現(xiàn)了無(wú)阻塞運(yùn)行。
需要注意的是,由于hidden API的限制, sWork成員變量 只能在 target sdk version小于以下的app中被反射得到,因此如果希望在target大于28 的app正常工作,還需要 突破系統(tǒng)hidden api的限制,這里可以使用 LSPosed 提供 hiddenApiBypass[3]庫(kù)。
另外 在Android 12 版本,這部分代碼又發(fā)生了變更, 不再使用clone 和 clear的方式 拷貝集合副本,而是直接替換 sWork的引用來(lái)實(shí)現(xiàn).
這樣,替換clone函數(shù)的方案就不可行了,并且由于 sWork變量指向的對(duì)象在每次調(diào)用processPendingWork 都會(huì)發(fā)生變更,因此動(dòng)態(tài)代理替換sWork對(duì)象的操作不能只執(zhí)行一次。繼續(xù)尋找可以hook的點(diǎn), 對(duì)于
for (Runnable v: work)
這個(gè)代碼 在字節(jié)碼層面其實(shí)會(huì)被轉(zhuǎn)換為迭代器的調(diào)用,因此 可以將之前的操作 轉(zhuǎn)換到 iterator函數(shù)中執(zhí)行,返回一個(gè)空的迭代器對(duì)象,因此將之前的方案從 代理 clone函數(shù) 改為代理 iterator函數(shù),并且需要保證 每次調(diào)用獲取迭代器函數(shù)后 再次將sWork對(duì)象重新代理掉。
最后
上述 方案代碼量其實(shí)不多,因此我 在github上建了一個(gè)工程用來(lái)模擬并解決QueueWork任務(wù)阻塞造成的ANR問(wèn)題, 可供參考 https://github.com/Knight-ZXW/SpWaitKiller . 在上線時(shí),應(yīng)當(dāng)對(duì)使用到SP的業(yè)務(wù)進(jìn)行相應(yīng)的測(cè)試,比如如果存在跨進(jìn)程組件依賴同一個(gè)SP文件的情況,由于我們?nèi)∠薃ctivity 在Stop時(shí)的 SP文件變更的刷盤行為,因此如果跳轉(zhuǎn)到其他進(jìn)程的組件,而該組件又依賴于跳轉(zhuǎn)前的SP變更的最新配置值,那么可能會(huì)出現(xiàn)問(wèn)題。另外事實(shí)上,從收集ANR的其他上下文信息來(lái)看,雖然SP的操作阻塞導(dǎo)致了ANR操作,但是并不能說(shuō)明真正的原因是因?yàn)镾P導(dǎo)致的,比如可能由于物理內(nèi)存緊張、頻繁發(fā)生swa 操作影響了正常的io操作,影響了SP的刷盤速度,最終導(dǎo)致了ANR出現(xiàn).
為了幫助到大家更好的全面清晰的掌握好性能優(yōu)化,準(zhǔn)備了相關(guān)的核心筆記(還該底層邏輯):https://qr18.cn/FVlo89
性能優(yōu)化核心筆記:https://qr18.cn/FVlo89
啟動(dòng)優(yōu)化
內(nèi)存優(yōu)化
UI優(yōu)化
網(wǎng)絡(luò)優(yōu)化
Bitmap優(yōu)化與圖片壓縮優(yōu)化:https://qr18.cn/FVlo89
多線程并發(fā)優(yōu)化與數(shù)據(jù)傳輸效率優(yōu)化
體積包優(yōu)化
《Android 性能監(jiān)控框架》:https://qr18.cn/FVlo89
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-600407.html
《Android Framework學(xué)習(xí)手冊(cè)》:https://qr18.cn/AQpN4J
- 開(kāi)機(jī)Init 進(jìn)程
- 開(kāi)機(jī)啟動(dòng) Zygote 進(jìn)程
- 開(kāi)機(jī)啟動(dòng) SystemServer 進(jìn)程
- Binder 驅(qū)動(dòng)
- AMS 的啟動(dòng)過(guò)程
- PMS 的啟動(dòng)過(guò)程
- Launcher 的啟動(dòng)過(guò)程
- Android 四大組件
- Android 系統(tǒng)服務(wù) - Input 事件的分發(fā)過(guò)程
- Android 底層渲染 - 屏幕刷新機(jī)制源碼分析
- Android 源碼分析實(shí)戰(zhàn)
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-600407.html
到了這里,關(guān)于性能優(yōu)化:如何徹底解決SharedPreferences造成的卡頓的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!