昨天被問到一個內(nèi)存泄漏問題,正好近期在錄制性能優(yōu)化相關(guān)的課程,用我講的方法來分析問題??
有時候內(nèi)存泄漏真是措不及防,潛伏的很深,不用工具的話壓根感知不到,往往只有在錯誤日志里才看得到,不過那個時候也已經(jīng)晚了。
先上個LeakCanary的warning圖

從圖上來看,很直觀,MainActivity泄漏了,持有的是MyLinkMovementMethod,還是個static變量。
這么看起來很容易解嘛,在MyLinkMovementMethod里把context引用去掉不就好了么,有什么好問的。
然而看了代碼,我也有點懵逼,大家也來瞅瞅。

這個MyLinkMovementMethod類中只有一個成員變量TouchableSpan,并沒有持有context呀,我被誤導(dǎo)的去翻了它的父類LinkMovementMethod,發(fā)現(xiàn)并沒有任何成員變量。那這個鍋只能是TouchableSpan來背了。
說來也是詭異的很,當(dāng)我要TouchableSpan類的截圖時,對方給我的是這樣的:

wtf, 這span沒有持有context呀,為什么會泄漏?這不科學(xué)。onClick(View)也沒有泄漏的可能。于是我又讓對方發(fā)TouchableSpan類給我

好吧,我也不知道問題出在哪了,被對方牽著走了一路,沒線索了。后來他一直在糾結(jié)widget.getContext().startActivity(intent)要不要換成applicationContext,這又是另外一個故事了。。。不管用什么context,這里都不存在泄漏。
最后沒轍,好歹我也擼了相關(guān)課程,連個內(nèi)存泄漏的原因都找不到,我不甘心啊。。
于是我讓他把相關(guān)代碼都發(fā)給我。。??戳舜a內(nèi)心都是崩潰的

內(nèi)部類。。。內(nèi)部類。。。非靜態(tài)內(nèi)部類能持有外部的引用啊。心塞。
我們再把整個泄漏的過程梳理下。

需求是textview能局部點擊,整體的onClick事件無法滿足,所以需要ClickableSpan來標(biāo)記。
于是自定義一個TagTextView,在TagTextView中有個內(nèi)部類Span來監(jiān)聽局部點擊,但Span是如何知道自己被點擊的呢?用MyLinkMovementMethod來處理textview的onTouch事件。解決方案是對的,但是代碼寫的有點渣。
MyLinkMovementMethod為單例,TouchableSpan一旦被實例化出來,除非手動置為null,否則不會釋放,而由于它是TagTextView的內(nèi)部類,所以持有這個TextView的引用,TextView是被MainActivity創(chuàng)建出來的,最終導(dǎo)致MainActivity無法被回收。
MyLinkMovementMethod->TagTextView.TouchableSpan->TagTextView->MainActivity
現(xiàn)在再來看LeakCanary的warning

剛好能對應(yīng)上了吧,有個工具能幫你做分析挺好的,不然review代碼都不一定能找出來。
至于怎么解決?方法倒是很簡單的:
- TagTextView.Span變?yōu)閟tatic,靜態(tài)內(nèi)部類不持有外部類的引用,就像Handler要用static一樣。
- MyLinkMovementMethod中不能有Span這個成員變量,讓Span引用只存在于某個方法的作用域中。
- 當(dāng)TagTextView的TouchEvent為UP時,應(yīng)手動將Span置為null。
這算是一個比較有代表性的內(nèi)存泄漏了,如果你對內(nèi)存泄漏的概念與成因還不夠熟悉,可以參考Stay錄制的性能優(yōu)化合輯,從java角度去講內(nèi)存泄漏你肯定能看懂。
另外有一點需要改進的,tag的點擊事件應(yīng)該傳遞到外部去,不應(yīng)該由Span自己處理跳轉(zhuǎn)到另外一個activity中去,這不符合設(shè)計原則,如果某個Tag跳轉(zhuǎn)的activity不是固定的怎么辦呢?自定義View的職責(zé)應(yīng)該是單一的,只負(fù)責(zé)接收事件,解析事件,具體的解決事件應(yīng)該由上層來處理,不然就沒法復(fù)用了。