一切从android的handler说起(七)之内存泄露

阅读本文大概需要 7 分钟。

 

 

作为一个客户端,UI无疑是非常重要的,因此主线程承载了非常多的任务,例如生命周期,View操作,包括Toast,View绘制,动画,等等,而这些的实现,都依赖于Android的消息机制模型。

 

可见Handler在Android的地位是非常核心的,在源码中随处可见它的存在。另一方面,在开发中Handler也可以作为线程间通信的重要手段,比如在子线程进行逻辑计算,通过向主线程handler发送message,从而达到更新UI的目的。

 

因此,对于Handler的使用,也是我作为面试官经常考察候选人的经典面试题。

 

我:在开发过程中我们经常在Activity中使用匿名handler对象来接收和处理线程中handler抛出来的message,但是Android Studio会提示有内存泄露的风险,你有碰到吗?

 

小张:是的,我最开始学习Android的时候就经常碰到这样的提示,一开始没当一会儿事,提示的次数多了,就觉得很奇怪IDE为何如此智能。

 

 

我:嗯,那你知道Android Studio为什么会有这样的提示吗?

 

小张回答道:因为匿名内部类默认会持有外部类Activity的引用,这样当Activity被销毁时,由于被匿名handler对象所持有而不能被释放,Activity所占用的内存就会泄露。

 

我:在聊内存泄露之前,我先问一下,你知道为什么匿名内部类默认会持有外部类的引用吗?

小张楞了一下:这个说多了就熟烂于心了,至于为什么,倒是没怎么想过呢...

 

我提示道:你有看过包含匿名内部类的java类编译过后的.class文件吗?

小张摇了摇头。

 

我说道:如果你看过的话,你会发现编译器为匿名内部类也单独生成了一份.class文件,而且其类名为Outer$1,并为其构造函数添加了一个参数,这个参数就是Outer类的实例,这就是为什么说匿名内部类默认会持有外部类的引用。

 

小张说道:哦,原来如此。那非静态内部类也默认会持有外部类的引用,是不是也是这个原因?

我说道:没错,但是静态内部类就不是这样了,可以认为它是一个独立的类,只不过写在了Outer类里,表示这2个类的关系非常紧密。

 

小张说道:看来每天挂在嘴边的常识,常常容易忽略背后的原因呢。

 

我继续说道:好了,回到之前的正题来,你说因为handler这个匿名内部类持有外部Activity的引用,导致Activity销毁时无法释放其内存是吗?

小张答道:是的。

 

我又问:那为什么Activity被引用了就无法释放Activity的内存呢?

 

小张见我继续问下去,只好答下去:因为匿名handler实例引用了Activity,handler又被其messsage.target所引用,如果当这个message是以sendMessageDelayed的方式放入message queue的,那么这个message可能在queue里存活较长时间,而此期间内如果用户销毁了Activity,但由于Activity一直被message引用链所引用而得不到释放。

 

我再问:如果不是用sendMessageDelayed,而是用postMessageDelayed呢?

 

小张答道:一样,不过这种情况下,不仅messsage.target会持有Activity的引用,同时匿名runnable还会直接引用Activity,而runnable又被message.callback所引用,因此无论用哪种情况下,message都会间接持有Activity的引用。

 

其实对于小张能回答到这一步,在众多面试者里已经算是不错的表现了,不过我并不打算就此轻易放过。

 

我继续反问道:这样为什么就不能被释放呢?

小张被问得有点发懵,好像问题答到这里算是结束了,就说道:我好像不是很理解你想问什么...

 

基本上在我多年的面试当中,到了这一步还能够回答得完美的少之又少,可以说不到5%!

 

我见势,就提示道:java里的对象之间的引用非常常见,难道只要被引用的对象都不能被内存回收吗?

小张这才回过神来:哦,你是想问JVM的垃圾回收机制。

 

我说道:没错,你还记得JVM是在什么情况下才不能释放一个对象的内存吗?

小张答道:如果GC Root到这个对象是引用链可达的话,那么此时就不能被GC垃圾回收掉此对象的内存。

 

我说道:嗯,是的,那你觉得在这个案例里,GC Root的引用链能够到达Activity吗?你先尽量把上游的引用链给列出来。

小张听我如此一说,开始捋了起来:Activity -> 匿名handler/runnable对象 -> message -> mQueue -> sMainLooper -> sThreadLocal -> 活着的UI线程。

 

我又问道:JVM里能够当GC Root的有哪几种对象?

小张听后,想了想,最后还是腼腆的说道:我记得有好几种对象是可以当GC Root的,不过现在只记得方法区里的类静态属性所引用的对象好像可以,其他的情况实在是想不起来了。

 

我说道:嗯,你刚才说的算一种,其它的情况还有:

1). 虚拟机栈/本地方法栈中JNI中的引用的对象。

2). System Class Loader/Boot Class Loader加载的类对象。

3). 激活状态的Thread 线程。

4). 方法区中的常量引用的对象,等等。

 

我继续问道:那你觉得刚才你列的引用链里面哪个可以当GC Root?

小张回答道:我怎么感觉好几都可以当GC Root。sThreadLocal可以当GC Root,因为它是Looper类的类静态属性。UI线程永生,也可以当GC Root,因此这条引用链上的所有对象都不能被GC内存回收掉。

 

我说道:我们还是利用leakCanary来分析一下内存泄露,看看真实的引用链到底是什么样的吧。

 

leakCanary分析结果

 

同时还可以用专业版的MAT或者Android Studio的Profiler来dump heap进行分析和佐证!

 

MAT分析结果

 

Profiler分析结果

 

我说道:你看分析工具都是 Activity -> handler -> message -> queue -> UI线程作为GC Root引用链,是不是和你想象的不一样?

 

小张很吃惊:真是没想到结果居然是这样!

 

我又问道:那根据这个理论,如果我们现在的handler关联的looper是子线程而非UI线程的话,你觉得还会有内存泄露风险吗?

 

小张想了一下,说道:那就应该不会了,因为随着子线程运行完毕,子线程的Looper和Message queue,handler对象也随之消亡,这条引用链也就断裂了,Activity销毁后就可以被GC回收掉!

 

我笑道:嗯,实际上你在IDE里这么写的话,就不会有内存泄露风险的提示出现了。

 

 

我:好了,现在原因我们搞清楚了,那要防止在UI线程使用匿名handler的内存泄露有什么办法吗?

小张答道:可以把Handler声明为静态内部类,这样就不会默认含有Activity的引用了。

 

我继续问道:那这样的话,你如何在其handleMessage里更新UI呢?

小张答道:哦,看来还是需要持有Activity的引用才行,在Hanlder的构造函数里传递进去。

 

我又继续问道:这样岂不是问题又回来了吗?现在的问题是在Activity销毁时GC无法回收,这一点上是否可以有什么办法,让其可以被GC强制回收?

小张回答道:咱们可以在Handler里不直接强引用Activity,而改为弱引用,这样在GC时就会释放掉Activity。

 

我问道:那还有没有更好的办法呢?

小张说道:最简单的方法就是在Activity销毁时,也立即断掉这条引用链。

 

我追问道:怎么说?

小张回答道:我们可以在Activity onDestroy()时调用handler.removeCallbacksAndMessages(null),这样就把queue里所有的message都remove掉了,之前说过message被message pool回收掉会reset,因此不会再引用handler,这条引用链就断掉了。

 

我说道:嗯,很好,这种方式是最直接的。你终于把来龙去脉全部搞清楚了。我看天色已晚,要不你先回去,后面等我的反馈?

小张有点意犹未尽,便说道:和你一起能学到太多东西了,我能加一下你的微信吗?

 

我哈哈大笑:个人微信聊起来有些费事儿,如果你想定期学一些干货,我会经常在我个人公众号上分享的,有什么问题也可以给我私信或者留言。

小张于是问道:好啊,那我扫一下你的二维码,关注一下你的公众号,跟你一起学习成长!

 

 

一切从android的handler说起(一)之message

一切从android的handler说起(二)之threadLocal

一切从android的handler说起(三)之UI线程不卡顿

一切从android的handler说起(四)之postDelay原理

一切从android的handler说起(五)之触摸事件模型

一切从android的handler说起(六)之生命周期来源

 


 

进入公众号,回复“程序员“可以领取一份计算机技术电子书福利合集

欢迎转发,关注公众号 肖晖

每天几分钟,掌握一个硬核面试知识点

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章