最近在思考对于内存泄露的问题,进而想到了对于咱们最常见和熟知的Handler在Activity内导致的内存透露的问题,这个问题置信作为开发都是很相熟的,然而这背地更多的细节和导致透露的不同的状况,可能很多人就没有那么理解和分明了,因而这次和大家分享一下什么状况下会导致内存透露,以及内存透露背地的故事。
1.Handler在什么状况下会导致内存透露
Handler在应用过程中,什么状况会导致内存透露?如果大家搜寻的话,个别都是会查到,Handler持有了Activity的援用,导致Activity不能正确被回收,因而导致的内存透露。那么这里就有问题了,什么状况下Handler持有了Activity的援用?什么时候Activity会不能被正确回收?
因而咱们当初看两段段代码
代码1-1:
private fun setHandler() { val handler = object : Handler() { override fun handleMessage(msg: Message) { if (msg.what == 1) { run() } } } handler.sendEmptyMessageDelayed(1, 10000) } private fun run() { Log.e("Acclex", "run") }
代码1-2:
private fun setHandler() { val handler = LeakHandler() handler.sendEmptyMessageDelayed(1, 10000)}private fun run() { Log.e("Acclex", "run")}inner class LeakHandler : Handler() { override fun handleMessage(msg: Message) { if (msg.what == 1) { run() } }}
置信Android的小伙伴应该都能看进去,下面两段代码都是会导致内存透露的,咱们首先须要剖析一下为什么会导致内存透露。以及藏在内存透露背地的事。
2.为什么会导致内存透露
下面的两段代码会导致内存透露,为什么会导致内存透露呢?这个问题也很好答复,因为匿名外部类和默认的外部类会持有外部类的援用。
在Java中,匿名外部类和外部的非动态类在实例化的时候,默认会传入外部类的援用this进去,因而这两个handler会持有Activity的实例,当handler内有工作在执行的时候,咱们敞开了Activity,这个时候回导致Activity不能正确被回收,就回导致内存透露。
从下面的代码中咱们能够看到handler延时10秒发送了一个音讯,如果在工作还未执行的时候,咱们敞开Activity,这个时候Activity就回呈现内存透露,LeakCanary也会捕捉到内存透露的异样。然而如果咱们期待工作执行结束,再敞开Activity,是不会呈现内存透露,LeakCanary也不会捕捉到有什么异样。也就是说如果工作被执行实现了,那么Handler持有Activity援用将能够被正确的开释掉。
这里将会引申出一个新的问题,Handler内执行工作的是什么货色,Handler内对象援用的链条是怎么样的,最终持有的对象是什么?
这个问题咱们稍后再来解答,咱们当初先来看看别的状况下的Handler。
3.动态外部类的Handler
Android的小伙伴们应该都晓得在解决Handler内存透露的时候个别都应用动态外部类和弱援用,这样个别都能够解决掉内存透露的问题,那么这里有一个变种,会不会导致内存透露呢?上面能够看一下上面的代码
代码1-3:
class UnLeakHandler() : Handler() { lateinit var activity: MainActivity constructor(activity: MainActivity) : this() { this.activity = activity }}
代码1-4:
class UnLeakHandler(activity: MainActivity) : Handler() {}
如上代码,代码1-3内,咱们传入了援用并且存储了这个变量,代码1-4内咱们传入了援用,然而并没有存储这个变量,那么这两种状况下,那种状况下会导致内存透露呢?
答案是代码1,咱们传入了援用并且将它作为一个变量存储起来了,这样的状况下它会导致内存透露。
那么这个问题该如何解答?要解答这个问题咱们须要先了解一下Java运行时的程序计数器,虚拟机堆栈,本地办法栈,办法区,堆以及可作为GCRoot的对象。
Java运行时数据区
- 程序计数器 程序计数器就是以后执行字节码的信号的一个指示器,记录的是以后线程执行字节码指令的地址。通常它会扭转,而后实现代码的流程管制,程序执行,循环等。
- 虚拟机栈 虚拟机栈是Java办法运行过程中的一个内存模型。虚拟机栈会给没一个行将运行的办法创立一个栈帧的区域,这块区域存储了办法在运行时所须要的一些信息,次要包含:
- 局部变量表:蕴含办法内的非动态变量以及办法形参,根本类型的存储值,援用对象的指向对象的援用。
- 操作数栈:存储两头的运算后果,办法入参和返回后果。
- 运行时常量池援用:次要蕴含的是以后办法对运行时常量池的援用,不便类在加载时进行动静链接,依据援用符号转变为对办法或者变量的援用。
- 办法进口返回信息:办法执行结束后,返回调用地位的地址。
- 本地办法栈 相似于虚拟机栈,然而是由一些Cor汇编操作的一些办法信息,寄存这些非Java语言编写的本地办法的栈区。
- 堆 堆是运行时数据最大的一块区域,外面蕴含了绝大部分的对象(实例数组等)都在外面存储。堆是跟随者JVM的启动而创立的,咱们创立的对象根本都在堆上调配,并且咱们不须要被动去开释内存,因为GC会主动帮咱们进行治理和销毁。这里GC相干的一些常识咱们前面再做解说。
- 办法区 次要存储类的元信息(类的名字,办法,字段信息),动态变量,常量等。办法区这块在JDK不同版本有不同的实现办法,存储的货色也有变动,感兴趣的话大家能够自行理解。
GCRoot对象
GCRoot就如同字面形容的,GC开始的根对象,将GCRoot对象作为终点,向下搜寻,走过的门路就是一个援用链,如果一个对象到GCRoot没有任何援用链,那么GC将会把这个对象的内存进行回收。
那么GCRoot有哪几种类型呢?或者说哪些对象能够作为GCRoot的对象呢?
- 虚拟机栈援用的对象
- 办法区中动态属性援用的对象
- 办法区中常量援用的对象
- 本地办法栈中JNI援用的对象
好了,当初咱们能够解答下面的问题了,为什么代码1-3会导致内存透露而代码1-4不会导致内存透露,如果应用代码1-3,构造函数传入了内部的Activiy,并且这个Handler类将这个援用存储到了类的外部,也就是说这个援用被Handler存储到了堆的区域内,那么直到它被开释地位,它将始终持有Activity的援用。
而在代码1-4内,构造函数实质也是一种函数,执行的时候,是以栈帧的模式执行的,函数的形参被存储在了栈帧上,构造函数执行结束之后,这个栈帧将会弹出栈,传入的形参会被间接销毁,因而实质上代码1-4内创立的Handler并没有持有Activity的援用
4.Handler导致内存透露时的援用链
咱们看完了下面的Handler在几种状况下的内存透露以及不会导致透露的问题,再回到咱们开始的一个问题:Handler内执行工作的是什么货色,Handler内对象援用的链条是怎么样的,最终持有的对象是什么?
要解答这个问题,咱们须要去剖析一下Handler的源代码。
首先,Handler作为匿名外部类和非动态外部类创立的时候会持有内部Activity的援用,咱们调用Handler的sendMessage办法发送音讯,咱们先从这个办法看一下。
public final boolean sendEmptyMessage(int what){ return sendEmptyMessageDelayed(what, 0); } public final boolean sendEmptyMessageDelayed(int what, long delayMillis) { Message msg = Message.obtain(); msg.what = what; return sendMessageDelayed(msg, delayMillis); }
能够看到下面的办法,发送一条Empty的Message都调用的是提早发送的Message办法,区别只是延时不同。在sendEmptyMessageDelayed办法内,结构了一个Message并且传入了sendMessageDelayed,咱们再往下看,看一下sendMessageDelayed办法
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) { if (delayMillis < 0) { delayMillis = 0; } return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); } public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) { MessageQueue queue = mQueue; if (queue == null) { RuntimeException e = new RuntimeException( this + " sendMessageAtTime() called with no mQueue"); Log.w("Looper", e.getMessage(), e); return false; } return enqueueMessage(queue, msg, uptimeMillis); }
下面的代码咱们能够看到,sendMessageAtTime办法内,结构了一个MessageQueue并且这个MessageQueue默认应用的就是该Handler内的MessageQueue,而后调用enqueueMessage去发送这个msg,参数就是这个queue和msg,咱们在看一下这个enqueueMessage办法
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { msg.target = this; msg.workSourceUid = ThreadLocalWorkSource.getUid(); if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis);}
在enqueueMessage内,咱们终于找到了援用Handler的中央了,结构的这个msg内的target援用的就是以后的Handler,那么这个将要被传递进来的message援用了以后的Handler,那么上面还有接着援用吗?答案是当然,在调用MessageQueue的enqueueMessage办法的时候,会将msg传入。残缺代码较长,这边只帖一部分
Message p = mMessages;boolean needWake;if (p == null || when == 0 || when < p.when) { // New head, wake up the event queue if blocked. msg.next = p; mMessages = msg; needWake = mBlocked;} else { // Inserted within the middle of the queue. Usually we don't have to wake // up the event queue unless there is a barrier at the head of the queue // and the message is the earliest asynchronous message in the queue. needWake = mBlocked && p.target == null && msg.isAsynchronous(); Message prev; for (;;) { prev = p; p = p.next; if (p == null || when < p.when) { break; } if (needWake && p.isAsynchronous()) { needWake = false; } } msg.next = p; // invariant: p == prev.next prev.next = msg;}
这是执行enqueueMessage的一部分代码,咱们能够看到这边MessageQueue内结构了一个新的Message p,并且将这个对象复制给了 传递进来的msg.next,同时在以后MessageQueue的mMessages为空,也就是以后默认状况下没有消息传递的时候,将msg赋值给了mMessages,那么MessageQueue持有了要传递的Message对象。
这样咱们就能够很清晰的看到一个残缺的援用链了。
MessageQueue援用了Message,Message援用了Handler,Handler默认援用了外部类Activity的实例。咱们也能够在LeakCanary上看到一样的援用链,并且它的GCRoot是一个native层的办法,这块就波及到MessageQueue的事件发送的机制,以及和Looper以及Looper内的ThreadLocal的机制了,这就是另外一个话题了。
这里让咱们再回到之前的一个概念GCRoot,还记得咱们提到GCRoot的时候说到过,如果一个对象和GCRoot对象没有一个援用链,那么它将被回收。因而,这里就是冲突点了,Activity被咱们被动敞开了,这个时候咱们通知了虚拟机Activity能够被回收了,然而从GCRoot开始向下搜寻,发现其实Activity其实是有一条援用链的,GCRoot不能把它回收掉,然而Activity曾经被敞开了,因而这个时候就触发了内存透露,应该被销毁和开释的内存并没有正确被开释。
5.解决Handler内存透露的办法
那么咱们当初来总结一下如何解决Handler内存透露的办法。
- 动态类和弱援用,这个办法置信大家都晓得,动态类不会持有内部援用,弱援用能够避免Handler持有Activity
- Activity销毁,生命周期完结的时候清空MessageQueue内的所有Message
其实这两种办法都是通过断开援用用,让GCRoot不会有援用链连贯到Activity,从而让Activity失常回收。
6.总结思考扩大
其实Handler的内存透露是一个很常见,也是大家开发会应用和碰到的问题,然而它背地其实蕴含了很多细节和原理,是咱们能够理解的,同时这个问题还能够引申出别的问题,这里能够提一下,大家之后能够思考一下,也欢送大家写出它们背地的原理和大家分享。
- 咱们罕用的View.setOnClickListener很多时候也创立了匿名外部类或者是间接传入了Activity,为什么这种状况下的Activity或者Fragment没有泄露。
- 咱们在应用ViewModel以及LiveData的时候,结构这些对象,以及察看对应数据的时候,如果Activity或者Fragment敞开了,为什么不会导致内存透露。
- 咱们开发的时候,本人编码或者应用一些第三方库,例如RxJava的时候,如何尽量避免内存透露。
其实内存透露在不论什么语言,什么平台上,都是有可能产生的,而咱们须要本人去被动关注这个方面,在编写代码的时候尽量躲避掉一些可能会导致内存透露的代码。
相干视频举荐:
【Android handle面试】Handler中的Message如何创立?
【Android handle面试】MessageQueue如何放弃各线程增加音讯的线程平安?
Android(安卓)开发零根底从入门到精通教程:Studio装置/UI/Fragment/四大组件/风行框架/我的项目公布与治理/我的项目实战
本文转自 https://juejin.cn/post/6844904083795476487,如有侵权,请分割删除。