阅读本文大概需要 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里这么写的话,就不会有内存泄露风险的提示出现了。
我:小张,我们已经搞清楚了匿名Handler在UI线程中可能导致内存泄露的原因,那现在有没有什么办法防止这种内存泄露呢?
小张答道:我们可以将Handler声明为静态内部类,这样就不会默认含有Activity的引用了。
我继续问道:既然这样的话,你如何在其handleMessage方法里更新UI呢?
小张答道:看来还是需要持有Activity的引用才行,我们可以在Handler的构造函数里传递进去。
我又进一步问道:这样岂不是问题又回来了吗?现在的另一个问题是在Activity销毁时GC无法回收,这一点上是否有什么办法可以让其可以被GC强制回收?
小张回答道:我们可以在Handler里不直接强引用Activity,而改为弱引用,这样在GC时就会释放掉Activity。
我问道:还有没有更好的解决方法呢?
小张说道:最简单的方法就是在Activity销毁时,也立即断掉这条引用链。
我追问道:具体怎么操作呢?
小张回答道:我们可以在Activity的onDestroy()方法中调用handler.removeCallbacksAndMessages(null),这样就可以把queue里面的所有message都移除了。之前说过message会被message pool回收并重置,因此不会再引用handler,这样这条引用链就断掉了。
我说道:嗯,这个方法很好。你已经完全理解了这个问题的来龙去脉。我看天色已晚,你先回去休息一下吧。我会在稍后给你反馈。