一次不講武德的 Android 線(xiàn)上 OOM 的排查過(guò)程

作者:王晨彥

開(kāi)篇

一天,后臺(tái)統(tǒng)計(jì)到線(xiàn)上有大量 OOM 崩潰,小王收到老板的緊急指令,立即排查!

小王心想,這還不簡(jiǎn)單,待我看看崩潰堆棧,分分鐘解決。

于是小王不慌不忙的打開(kāi)崩潰后臺(tái),一看傻眼了,同樣的 OOM,卻有幾十種不同的堆棧,大到創(chuàng)建 View,小到 new 一個(gè) String。

小王差點(diǎn)罵了出來(lái):這 OOM 不講武德啊!

罵完之后,還是得解決問(wèn)題啊,否則怎么面對(duì)老板啊。

心路歷程

正郁悶著,小王突然想起曾經(jīng)看過(guò)性能優(yōu)化的文章,里面介紹了 Android Studio 中集成的 Profiler 可以分析 APP 內(nèi)存。

既然堆??床怀鍪裁磫?wèn)題,那就只能照著文章的方法,碰碰運(yùn)氣了。

于是小王點(diǎn)開(kāi)了 IDE 底部那個(gè)毫不起眼的「Profiler」面板,映入眼簾的是:

小王一眼就看到了 MEMORY 欄,這不就是內(nèi)存使用嘛。

嗯,數(shù)據(jù)倒是挺全,可是怎么知道哪里導(dǎo)致 OOM 了啊,小王又開(kāi)始懷疑人生了…

“放著不動(dòng)肯定看不出什么啊,內(nèi)存是動(dòng)態(tài)申請(qǐng)的嘛?!?/p>

小王心想,既然這么多 OOM,那么肯定是 APP 內(nèi)的常用頁(yè)面導(dǎo)致的,于是小王開(kāi)始一邊來(lái)回切換常用頁(yè)面,一邊觀察內(nèi)存走勢(shì)。

經(jīng)過(guò)多次嘗試,小王發(fā)現(xiàn)應(yīng)用的內(nèi)存占用確實(shí)在不斷升高,即使手動(dòng) GC 之后,仍然居高不下。

小王想起面試寶典中「無(wú)法被 GC 回收的對(duì)象,會(huì)導(dǎo)致內(nèi)存泄露」,于是手動(dòng)點(diǎn)了下 GC,避免數(shù)據(jù)不準(zhǔn)確。

Java 堆從 15.7MB 漲到 19.3MB,好像問(wèn)題不大,而 Native 就離譜了,好家伙,竟然從 56.1MB 漲到了 97.5MB,分分鐘就漲了 40MB+。

小王喜出望外,終于發(fā)現(xiàn)內(nèi)存問(wèn)題了!看來(lái)平時(shí)摸魚(yú)的時(shí)候多看看文章真是沒(méi)壞處啊。

可是,就算知道內(nèi)存不正常,但還是不能定位是哪段代碼導(dǎo)致了…

小王平復(fù)了一下心情,繼續(xù)觀察規(guī)律,終于發(fā)現(xiàn),每次從A頁(yè)面跳轉(zhuǎn)出去,內(nèi)存都會(huì)增加幾M,而且 GC 無(wú)法回收,那肯定是這個(gè)頁(yè)面有問(wèn)題了!

于是小王罵罵咧咧的開(kāi)始閱讀這個(gè)頁(yè)面的代碼,希望能夠發(fā)現(xiàn)內(nèi)存泄露的元兇。心里嘀咕著,讓我看看是哪個(gè) ** 寫(xiě)出了內(nèi)存泄露的代碼。

小王逐字逐句看完了代碼:可是并沒(méi)有什么問(wèn)題啊,就是一個(gè)普通的列表頁(yè),還是用 RecyclerView 實(shí)現(xiàn)的,沒(méi)啥毛病啊。

這下又把小王難住了,小王心想,不能在黎明前倒下啊,于是又想起文章中關(guān)于 Profiler 的介紹,可以使用 Dump 功能方便的查看當(dāng)前的內(nèi)存快照,興許能發(fā)現(xiàn)什么端倪呢。

好家伙,原來(lái)是 Bitmap 占了這么大內(nèi)存,于是小王又想起面試寶典。

Android 2.3.3(API level 10) 和更早的版本,Bitmap 對(duì)象和對(duì)象里對(duì)應(yīng)的像素?cái)?shù)據(jù)是分開(kāi)存儲(chǔ)的,Bitmap 存在虛擬機(jī)的堆里,而像素?cái)?shù)據(jù)存儲(chǔ)在 Native 內(nèi)存里。

從 Android 3.0(API level 11) 到 Android 7.1(API level 25),Bitmap 對(duì)象及其像素?cái)?shù)據(jù)都存儲(chǔ)在虛擬機(jī)的堆里。

從 Android 8.0(API level 26) 開(kāi)始,Bitmap 對(duì)象存儲(chǔ)在虛擬機(jī)的堆里,而對(duì)應(yīng)的像素?cái)?shù)據(jù)存儲(chǔ)在 Native 堆里。

小王測(cè)試的手機(jī)是 Android 10,Bitmap 數(shù)據(jù)存儲(chǔ)在 Native 堆,所以基本上可以確定就是 Bitmap 導(dǎo)致內(nèi)存泄露了。雖然又前進(jìn)了一大步,但還是找不到原因。

小王發(fā)現(xiàn),點(diǎn)擊對(duì)象,可以查看所有實(shí)例的引用鏈,這下可把小王高興壞了,而且小王還發(fā)現(xiàn)了一個(gè)非??梢傻囊面湣?/p>

這不是 Coil 的 Memory Cache 嘛,可是這里明明是有緩存的嘛,怎么還會(huì)泄露,難不成是這個(gè)開(kāi)源庫(kù)有 bug?

https://github.com/coil-kt/coil

小王懷著忐忑的心情打開(kāi)了 RealMemoryCache 這個(gè)類(lèi)。

這不就是一個(gè)基于 LRU 實(shí)現(xiàn)的內(nèi)存緩存嘛,乍一看好像沒(méi)什么毛病。

沒(méi)時(shí)間仔細(xì)研究了,小王心想,先看看開(kāi)源社區(qū)有沒(méi)有人反饋過(guò)這個(gè)問(wèn)題,小王過(guò)濾了一下包含 "memory leak" 關(guān)鍵字的 issue。

果然有一個(gè) PR 的標(biāo)題非常接近 Fix memory leak if request is started on detached view.

https://github.com/coil-kt/coil/pull/518

看起來(lái)問(wèn)題已經(jīng)被修復(fù)且已經(jīng)發(fā)布了新版本,于是小王立馬升級(jí)版本再次測(cè)試,果然沒(méi)有泄露了。

于是立馬提交代碼,興沖沖的去找老板炫耀了?。?!

追根溯源

回過(guò)頭來(lái),小王心想,作為一個(gè)“有上進(jìn)心”的程序員,我得看看是什么原因?qū)е碌男孤栋 ?/p>

于是再次打開(kāi) PR,在諸多改動(dòng)中,終于找到一個(gè)真正的代碼改動(dòng),其他都是測(cè)試用例。

小王不禁感慨,歪果仁就是專(zhuān)業(yè)呀,改了兩行代碼就要寫(xiě)一堆測(cè)試用例。

小王終于弄清了導(dǎo)致泄露的原因,原來(lái)是在快速切換頁(yè)面時(shí),有時(shí)頁(yè)面已經(jīng)銷(xiāo)毀了,才開(kāi)始加載圖片,此時(shí) Coil 會(huì)把這個(gè) View 對(duì)象保存起來(lái),等待 View detach 的時(shí)候釋放,然而此時(shí) View 已經(jīng)是 detach 的狀態(tài)了,因此永遠(yuǎn)不會(huì)被釋放了,而 Bitmap 又被 View 持有,而我們都知道 Bitmap 是內(nèi)存占用大戶(hù),因此就出現(xiàn)了上面 Bitmap 占用大量?jī)?nèi)存的情況。

而解決方案就是再判斷一下 View 是否已經(jīng) Detach,是的話(huà)就直接釋放了,避免造成泄露。

最后小王高高興興的下班了。

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

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

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