概述
内存泄漏是 隐形炸弹,它不会像崩溃问题一样立马爆发,但是随着app使用时间增长,内存问题的积累会导致一些严重的问题,比如UI卡顿,内存溢出等。
Activity 内存泄漏预防
Activity 之所以单独拎出来说,是因为由于Activity承担了与用户的交互界面,它会包含大量的资源引用,以及与系统服务的交互的Context。这会导致Activity对象关联的其他对象的总内存占用会非常大。
由于Activity造成的内存泄漏的原因主要有以下几种情况:
不当使用static
比如下图中,给一个ImageView设置static,activity中的view都会默认持有Activity对象(因为UI元素都是依附Activity而存在的),那么一个静态的view持有的Activity就无法在GC时被释放。
这是因为 静态变量 是 GCRoot之一的 方法区变量
。
下图中的ActivityB就是一个会引起泄漏的典型现象。
只注册了系统监听器但是没有解除注册
比如下图中 注册了广播监听器,但是没有在onDestory中进行解除注册。
在这个Activity页面上按下返回键时,就会报出如下日志,告知Activity引起了泄漏:
这种日志还算比较友好的,明确告知了内存泄漏是出自哪个Activity。
handler使用不当引起泄漏
下图是Handler的典型错误用法:
静态内部类默认持有Activity的引用,handler执行异步任务的时机是不确定的,如果执行过程中,Activity发生了回收,会导致回收失败,形成内存泄漏。
改动为如下写法可以防止这种泄漏:
在 Activity内部定义静态内部类Handler:
第三方库不当使用Context
下图中,执行了三方库的初始化代码,并且将 Activity.this传给了它。
有些三方库的初始化必须要用Context对象,如果按照上面的这种写法,直接给了一个activity对象当做context传给了 它,而三方库中又将 context 设置为了静态成员。这也就间接让activity成为了不可回收的对象,GC时就形成了泄漏。
这是一种隐形的Activity内存泄漏。
这种情况下,我们要注意,给外部传context时,不要直接使用activity,建议使用 activity.getApplicationContext()
将全局的context给三方库。
这也提示我们,我们自己在做sdk时,要注意,不要直接将外部传入的context设置成静态,最好也去获取它的 getApplicationContext()
比如下图,这是 极光推送的sdk的一段混淆后的代码,可以看出,它没有直接使用 外部传入的 context,而是先执行了 var0.getApplicationContext()
进行相关初始化操作。
内存泄漏的检测
AndroidStudio的profile结合 MAT 工具
在app运行时打开profile,截取一段操作之后的内存情况,并 dump 一段内存日志,然后将 日志 导入到 MAT工具中分析泄漏原因。这个是常规操作,本文不展开讨论。
LeakCanary
它是检测内存泄漏的 自动化工具。它是 Square 公司的一个开源库,通过它,可以在 App运行过程中检测内存泄漏。
当内存泄漏发生时,会生成泄漏发生的引用链,并用通知栏消息的方式通知开发人员。
LeakCanery的核心功能只有两个部分:
- 内存泄漏的检测
- 泄漏对象的引用链分析
JVM基础知识回顾
Java虚拟机中有 强软弱虚四大引用类型。
默认的,也是最常见的就是 强引用。GC的可达性分析,也是基于强引用的,只要顺着强引用的引用链找到最顶端的对象,这个对象并不是 GcRoot,那么 它就是可以被回收的。
常见的GC有:常量,静态变量,正在运行时的方法的局部变量等。
软引用,当发生GC时,发现内存已经不足了,此时会优先回收软引用。 弱引用,只要发生GC,就会进行回收。 虚引用,仅做标记用,没有其他实际用途。
LeakCanery内存泄漏的检测的原理
WeakReference,弱引用,常见用法如下:
创建一个WeakReference
,传入一个模拟的大对象 BigObject()
,
在GC之前打印一次 reference.get()
,尝试去获取这个bigObject。这段代码的执行结果如下:
在 GC之前,reference.get()
能够获取到 bigObject对象。但是在GC之后获取到的是null。
改造一下,WeakReference在创建时可以传入 一个 queue(类型为: ReferenceQueue
)
当一个弱引用持有的对象被 GC回收时,这个对象将会进入到 刚才传入的 queue 中。 上图代码的执行结果为:
GC之前,reference.get()
能获取到 bigObject
, queue
内容为空。 GC之后,reference.get()
获取不到 bigObject
,但是queue
非空。 这说明,GC回收之后,这个bigObject进入到了 ReferenceQueue
中。
如果略微修改一下代码,如下图,将 new BigObject()
提炼出来BigObject bigObject = new BigObject();
, 那么在这段代码执行的过程中,它就是gcRoot
(这是因为 gcRoot包含了 一种情况:正在执行的方法中的局部变量)
LeakCanery就是基于 WeakReference 弱引用
和ReferenceQueue 引用队列
实现的.
其原理总结如下:
- 当一个Activity要被回收时,就将其包装到一个
WeakReferece 弱引用
中,并且在创建WeakReference
时传入一个自定义的ReferenceQueue
- 给上一步创建出来的
WeakReference
做一个标记key
,并且在一个强引用Set
中添加相应的key
记录 - 主动触发
System.gc();
,遍历 自定义的ReferenceQueue
, 将查找到的 对象,在 上一步的Set
中 按照key
进行删除。 - 遍历完成之后,
Set
中还保留着的 对象,就是造成了泄漏的对象了。
文字描述可能比较抽象,我用 流程图表述一下:
LeakCanery源码分析
一个可回收对象,在System.gc()
时就应该被回收,但是,在安卓app中,我们并不确定Activity
应该何时被系统回收,按照正常流程,Activity
在执行 onDestory
时,就应该确认该Activity
应该被回收。
所以,我们LeakCanery
首先要解决的就是,监听所有Activity
的onDestory
方法的调用。
实现方式也很简单,就是利用了 Application
的 registerActivityLifecycleCallbacks()
监听所有Activity的生命周期。
上图中监听器的具体实现如下。
这样,就能收集到所有即将销毁的Activity
对象了。
负责收集的是 RefWathcer
,他是 LeakCanery
的核心类,用来检测一个对象是否会发生内存泄漏。
下图是他的工作原理
主要做了三件事:
- 创建一个随机字符串作为 key,用来标记要回收的对象
- 将要回收的对象包装到 WeakReference中,并用上一步创建的key作为标记
- 对 weakReference 进行泄漏检测
最核心的内容,就是在 ensureGoneAsync
中。 它的代码如下:
进而跟踪到 ensureGone()
,源码如下:
解释说明:
- 遍历 ReferenceQueue,按照每一个元素的key,删除 强引用Set中的元素,并同时 移除 ReferenceQueue的该元素(参见上文
leakCanery原理图.jpg
) - 检测 强引用Set中 是否还包含 要回收对象的key,如果包含,说明要回收的对象并没有被回收,也就是发生了内存泄漏
- 分析heap堆信息,并生成内存泄漏的分析报告,上报给程序开发者。
其中第一步,removeWeaklyReachableReferences()
的源代码如下:
图中 retainedKeys
就是 leakCanery原理图.jpg
中提到的 强引用Set
. 这儿的代码写的很简洁巧妙,只用了一个while循环
,就实现了 queue
元素的弹出,以及retainedkeys
的元素的移除。
其中第二步,gone()
方法判断 元素是否被回收。
实现很简单,只做了一个简单的 contains
判断。
第三步中的 生成报告,并不是核心原理,就不展开讨论。
内存检测的时机
LeakCanery的内存泄漏检测是比较消耗性能的,为了达到检测的目的,又不能过于影响 UI线程的渲染。 源码上做了一些优化。
上一节中,ensureGoneAsync()
中调用了 watchExecutor.execute()
, 实际上是向主线程的MessageQueue中插入了一个 IdleHandler
, 看名字就知道,它是一个闲时任务,只有当主线程不忙的时候,才会去执行。 IdleHandler
源代码如下:
java
/**
* Callback interface for discovering when a thread is going to block
* waiting for more messages.
*/
public static interface IdleHandler {
/**
* Called when the message queue has run out of messages and will now
* wait for more. Return true to keep your idle handler active, false
* to have it removed. This may be called if there are still messages
* pending in the queue, but they are all scheduled to be dispatched
* after the current time.
*/
boolean queueIdle();
}
它的 queueIdle
方法 方法就是具体执行过程。从MessageQueue
的源代码可以看出,这个 queueIdle
只有在 当没有任何message时,才会执行到 IdleHandler的回调. 所以可以理解为,我们向 MessageQueue
中发送了一个闲时任务,而由于 Looper
会让MessageQueue
的next方法
不停地执行,所以闲时任务
一定会在 常规message都处理完毕之后执行。
java
Message next() {
//...
for (;;) {
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
if (msg != null) {
//...
return msg; // 正常返回message时,就去执行message的回调
}
}
//...
}
for (int i = 0; i < pendingIdleHandlerCount; i++) {
boolean keep = false;
try {
keep = idler.queueIdle(); // 当没有任何message时,才会执行到 IdleHandler的回调
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
}
}
}
通过这种方式,可以有效地实现 内存检测,并且不影响UI绘制。
IdleHandler的妙用
受LeakCanery的启发,我们其实可以利用 Looper.myQueue().addIdleHandler()
来做app的启动优化。
比如说,一些优先级不高的第三方库的初始化,尤其是这些初始化的动作必须在主线程中时,将他们作为闲时任务加入到 Handler
机制中。
但是这个方法名,IdleHandler
,方法很有迷惑性,个人觉得换成 IdleMessage
更为合适。
LeakCanery的特殊机型适配
由于国内手机厂商实在是太多了,他们对原生系统的修改会导致一些本来就存在的内存泄漏,这些情况并不属于我们代码的问题,所以要在 内存泄漏检测时排除在外,以免混淆我们的排查结果。
上图中,LeakCanery
使用了 excludedRefs
将这些场景都排除在外,这种统一规避特殊机型的方式,我们也可以借鉴。
LeayCanery如何检测Activity以外的类
它默认只能检测Activity引起的泄漏。但是,RefWathcer
的 watch方法检测的实际上是一个 Object
,并没有限定Activity。所以理论上可以检测任何类。
LeakCanery.install(context)
方法返回的就是一个RefWatcher
对象,我们只需要在Application中保存此对象,然后将被检测的对象 传给 watch
方法即可。
如下图所示:
总结
本文介绍了内存泄漏发生的原因,以及预防泄漏的方法。 另外,讲解了 内存泄漏的检测方法,以及开源库LeakCanery的工作原理。 从LeakCanery的源码中,学到了3点Tip:
其一,如何利用 MessageQueue.addIdleHandler 发送一个闲时消息,优化App的启动速度。 其二,关于统一逻辑之下,要把特殊情况排除在外的写法 其三,LeakCanery不只是能够检测 Activity的泄漏,我们可以利用 它install产生的RefWatcher对象,来对任何对象进行泄漏检测。