作者:王晨彥
開篇
一天,后臺(tái)統(tǒng)計(jì)到線上有大量 OOM 崩潰犹赖,小王收到老板的緊急指令青灼,立即排查!
小王心想姐浮,這還不簡單,待我看看崩潰堆棧葬馋,分分鐘解決卖鲤。
于是小王不慌不忙的打開崩潰后臺(tái),一看傻眼了畴嘶,同樣的 OOM蛋逾,卻有幾十種不同的堆棧,大到創(chuàng)建 View窗悯,小到 new 一個(gè) String区匣。
小王差點(diǎn)罵了出來:這 OOM 不講武德啊蒋院!
罵完之后亏钩,還是得解決問題啊,否則怎么面對(duì)老板啊欺旧。
心路歷程
正郁悶著姑丑,小王突然想起曾經(jīng)看過性能優(yōu)化的文章,里面介紹了 Android Studio 中集成的 Profiler 可以分析 APP 內(nèi)存辞友。
既然堆椪ぐВ看不出什么問題,那就只能照著文章的方法称龙,碰碰運(yùn)氣了昌屉。
于是小王點(diǎn)開了 IDE 底部那個(gè)毫不起眼的「Profiler」面板,映入眼簾的是:
小王一眼就看到了 MEMORY 欄茵瀑,這不就是內(nèi)存使用嘛间驮。
嗯,數(shù)據(jù)倒是挺全马昨,可是怎么知道哪里導(dǎo)致 OOM 了啊竞帽,小王又開始懷疑人生了…
“放著不動(dòng)肯定看不出什么啊,內(nèi)存是動(dòng)態(tài)申請(qǐng)的嘛鸿捧∫俾ǎ”
小王心想,既然這么多 OOM匙奴,那么肯定是 APP 內(nèi)的常用頁面導(dǎo)致的堆巧,于是小王開始一邊來回切換常用頁面,一邊觀察內(nèi)存走勢(shì)。
經(jīng)過多次嘗試谍肤,小王發(fā)現(xiàn)應(yīng)用的內(nèi)存占用確實(shí)在不斷升高啦租,即使手動(dòng) GC 之后,仍然居高不下荒揣。
小王想起面試寶典中「無法被 GC 回收的對(duì)象篷角,會(huì)導(dǎo)致內(nèi)存泄露」,于是手動(dòng)點(diǎn)了下 GC系任,避免數(shù)據(jù)不準(zhǔn)確恳蹲。
Java 堆從 15.7MB 漲到 19.3MB,好像問題不大俩滥,而 Native 就離譜了嘉蕾,好家伙,竟然從 56.1MB 漲到了 97.5MB霜旧,分分鐘就漲了 40MB+错忱。
小王喜出望外,終于發(fā)現(xiàn)內(nèi)存問題了颁糟!看來平時(shí)摸魚的時(shí)候多看看文章真是沒壞處啊航背。
可是喉悴,就算知道內(nèi)存不正常棱貌,但還是不能定位是哪段代碼導(dǎo)致了…
小王平復(fù)了一下心情,繼續(xù)觀察規(guī)律箕肃,終于發(fā)現(xiàn)婚脱,每次從A頁面跳轉(zhuǎn)出去,內(nèi)存都會(huì)增加幾M勺像,而且 GC 無法回收障贸,那肯定是這個(gè)頁面有問題了!
于是小王罵罵咧咧的開始閱讀這個(gè)頁面的代碼吟宦,希望能夠發(fā)現(xiàn)內(nèi)存泄露的元兇篮洁。心里嘀咕著,讓我看看是哪個(gè) ** 寫出了內(nèi)存泄露的代碼殃姓。
小王逐字逐句看完了代碼:可是并沒有什么問題啊袁波,就是一個(gè)普通的列表頁,還是用 RecyclerView 實(shí)現(xiàn)的蜗侈,沒啥毛病啊篷牌。
這下又把小王難住了,小王心想踏幻,不能在黎明前倒下啊枷颊,于是又想起文章中關(guān)于 Profiler 的介紹,可以使用 Dump 功能方便的查看當(dāng)前的內(nèi)存快照,興許能發(fā)現(xiàn)什么端倪呢夭苗。
好家伙信卡,原來是 Bitmap 占了這么大內(nèi)存,于是小王又想起面試寶典听诸。
Android 2.3.3(API level 10) 和更早的版本坐求,Bitmap 對(duì)象和對(duì)象里對(duì)應(yīng)的像素?cái)?shù)據(jù)是分開存儲(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) 開始泛领,Bitmap 對(duì)象存儲(chǔ)在虛擬機(jī)的堆里,而對(duì)應(yīng)的像素?cái)?shù)據(jù)存儲(chǔ)在 Native 堆里敛惊。
小王測試的手機(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è)非骋凼祝可疑的引用鏈。
這不是 Coil 的 Memory Cache 嘛显拜,可是這里明明是有緩存的嘛衡奥,怎么還會(huì)泄露,難不成是這個(gè)開源庫有 bug远荠?
https://github.com/coil-kt/coil
小王懷著忐忑的心情打開了 RealMemoryCache 這個(gè)類矮固。
這不就是一個(gè)基于 LRU 實(shí)現(xiàn)的內(nèi)存緩存嘛,乍一看好像沒什么毛病譬淳。
沒時(shí)間仔細(xì)研究了档址,小王心想,先看看開源社區(qū)有沒有人反饋過這個(gè)問題瘦赫,小王過濾了一下包含 "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
看起來問題已經(jīng)被修復(fù)且已經(jīng)發(fā)布了新版本,于是小王立馬升級(jí)版本再次測試确虱,果然沒有泄露了含友。
于是立馬提交代碼,興沖沖的去找老板炫耀了!>轿省辆童!
追根溯源
回過頭來,小王心想惠赫,作為一個(gè)“有上進(jìn)心”的程序員把鉴,我得看看是什么原因?qū)е碌男孤栋 ?/p>
于是再次打開 PR,在諸多改動(dòng)中儿咱,終于找到一個(gè)真正的代碼改動(dòng)庭砍,其他都是測試用例。
小王不禁感慨混埠,歪果仁就是專業(yè)呀怠缸,改了兩行代碼就要寫一堆測試用例。
小王終于弄清了導(dǎo)致泄露的原因钳宪,原來是在快速切換頁面時(shí)揭北,有時(shí)頁面已經(jīng)銷毀了,才開始加載圖片吏颖,此時(shí) Coil 會(huì)把這個(gè) View 對(duì)象保存起來搔体,等待 View detach 的時(shí)候釋放,然而此時(shí) View 已經(jīng)是 detach 的狀態(tài)了半醉,因此永遠(yuǎn)不會(huì)被釋放了疚俱,而 Bitmap 又被 View 持有,而我們都知道 Bitmap 是內(nèi)存占用大戶奉呛,因此就出現(xiàn)了上面 Bitmap 占用大量內(nèi)存的情況计螺。
而解決方案就是再判斷一下 View 是否已經(jīng) Detach夯尽,是的話就直接釋放了瞧壮,避免造成泄露。
最后小王高高興興的下班了匙握。