昨天被问到一个内存泄漏问题,正好近期在录制性能优化相关的课程,用我讲的方法来分析问题😯
有时候内存泄漏真是措不及防,潜伏的很深,不用工具的话压根感知不到,往往只有在错误日志里才看得到,不过那个时候也已经晚了。
先上个LeakCanary的warning图
Paste_Image.png
从图上来看,很直观,MainActivity泄漏了,持有的是MyLinkMovementMethod,还是个static变量。
这么看起来很容易解嘛,在MyLinkMovementMethod里把context引用去掉不就好了么,有什么好问的。
然而看了代码,我也有点懵逼,大家也来瞅瞅。
Paste_Image.png
这个MyLinkMovementMethod类中只有一个成员变量TouchableSpan,并没有持有context呀,我被误导的去翻了它的父类LinkMovementMethod,发现并没有任何成员变量。那这个锅只能是TouchableSpan来背了。
说来也是诡异的很,当我要TouchableSpan类的截图时,对方给我的是这样的:
Paste_Image.png
wtf, 这span没有持有context呀,为什么会泄漏?这不科学。onClick(View)也没有泄漏的可能。于是我又让对方发TouchableSpan类给我
Paste_Image.png
好吧,我也不知道问题出在哪了,被对方牵着走了一路,没线索了。后来他一直在纠结widget.getContext().startActivity(intent)要不要换成applicationContext,这又是另外一个故事了。。。不管用什么context,这里都不存在泄漏。
最后没辙,好歹我也撸了相关课程,连个内存泄漏的原因都找不到,我不甘心啊。。
于是我让他把相关代码都发给我。。。看了代码内心都是崩溃的
Paste_Image.png
内部类。。。内部类。。。非静态内部类能持有外部的引用啊。心塞。
我们再把整个泄漏的过程梳理下。
Paste_Image.png
需求是textview能局部点击,整体的onClick事件无法满足,所以需要ClickableSpan来标记。
于是自定义一个TagTextView,在TagTextView中有个内部类Span来监听局部点击,但Span是如何知道自己被点击的呢?用MyLinkMovementMethod来处理textview的onTouch事件。解决方案是对的,但是代码写的有点渣。
MyLinkMovementMethod为单例,TouchableSpan一旦被实例化出来,除非手动置为null,否则不会释放,而由于它是TagTextView的内部类,所以持有这个TextView的引用,TextView是被MainActivity创建出来的,最终导致MainActivity无法被回收。
MyLinkMovementMethod->TagTextView.TouchableSpan->TagTextView->MainActivity
现在再来看LeakCanary的warning
Paste_Image.png
刚好能对应上了吧,有个工具能帮你做分析挺好的,不然review代码都不一定能找出来。
至于怎么解决?方法倒是很简单的:
TagTextView.Span变为static,静态内部类不持有外部类的引用,就像Handler要用static一样。
MyLinkMovementMethod中不能有Span这个成员变量,让Span引用只存在于某个方法的作用域中。
当TagTextView的TouchEvent为UP时,应手动将Span置为null。
这算是一个比较有代表性的内存泄漏了,如果你对内存泄漏的概念与成因还不够熟悉,可以参考Stay录制的性能优化合辑,从java角度去讲内存泄漏你肯定能看懂。
另外有一点需要改进的,tag的点击事件应该传递到外部去,不应该由Span自己处理跳转到另外一个activity中去,这不符合设计原则,如果某个Tag跳转的activity不是固定的怎么办呢?自定义View的职责应该是单一的,只负责接收事件,解析事件,具体的解决事件应该由上层来处理,不然就没法复用了。