Skip to content

第26讲:面对内存泄漏,如何进行优化? ​

内存泄漏是一个隐形炸弹,其本身并不会造成程序异常,但是随着量的增长会导致其他各种并发症:OOM,UI 卡顿等。

Activity 内存泄漏预防 ​

为什么要单独将 Activity 单独做预防,是因为 Activity 承担了与用户交互的职责,因此内部需要持有大量的资源引用以及与系统交互的 Context,这会导致一个 Activity 对象的 retained size 特别大。一旦 Activity 因为被外部系统所持有而导致发生内存泄漏,被牵连导致其他对象的内存泄漏也会非常多。

造成 Activity 内存泄漏的场景主要有以下几种情况。

1. 将 Context 或者 View 置为 static ​

View 默认会持有一个 Context 的引用,如果将其置为 static 将会造成 View 在方法区中无法被快速回收,最终导致 Activity 内存泄漏。

图中的 imageView 会造成 ActivityB 无法被 GC 回收。

2. 未解注册各种 Listener ​

在 Activity 中可能会注册各种系统监听器,比如广播。

运行上述 ActivityC,然后按下返回键。控制台将会显示如下 log,提示有内存泄漏发生:

3. 非静态 Handler 导致 Activity 泄漏 ​

上述代码中的Handler也会造成ActivityD内存泄漏,一般需要将其置为static,然后内部持有一个Activity的弱引用来避免内存泄漏。如下所示:

4. 三方库使用 Context ​

在项目中经常会使用各种三方库,有些三方库的初始化需要我们传入一个 Context 对象。但是三方库中很有可能一直持有此 Context 引用,比如以下代码:

上述代码中将 ActivityE 本身当作一个 Context 传递给了一个模拟的三方库 ThirdParty 中,但是在三方库中将传入的 context 重新置为一个静态 static 类型。这种情况是一种隐形的 Activity 泄漏,在我们自己的项目中很难察觉出,所以平时开发过程中,尽量使用 Context.getApplicationContext,不要直接将 Activity 传递给其他组件。

提示:这也提醒我们自己在实现 SDK 时,也尽量避免造成外部 Context 的泄漏。比如下图是 JPush 中初始化的部分混淆代码:

虽然是经过混淆之后的代码,但是也能大概猜出 checkContext 方法内部会使用 context.getApplicationContext 给内部 Context 赋值,因此即使我们传给 JPush 的是 Activity,也不会造成 Activity 泄漏。

内存泄漏检测 ​

在开发阶段安卓工程师可以直接使用 Android Studio 来查看 Activity 是否存在内存泄漏,并结合 MAT 来查看发生内存泄漏的具体对象。这部分内容相信大多数安卓工程师都信手拈来,这节内容不展开详细介绍,详细使用过程可以参考:Android Studio和MAT结合使用来分析内存问题。

除了 Android Studio 之外,另一个检查内存泄漏的神器就是 LeakCanary,也是本节重点介绍内容。

LeakCanary 是 Square 公司的一个开源库。通过它可以在 App 运行过程中检测内存泄漏,当内存泄漏发生时会生成发生泄漏对象的引用链,并通知程序开发人员。

可以看出 LeakCanary 主要分 2 大核心部分:

  1. 如何检测内存泄漏;

  2. 分析内存泄漏对象的引用链。

如何检测内存泄漏

JVM 理论知识 ​

Java 中的 WeakReference 是弱引用类型,每当发生 GC 时,它所持有的对象如果没有被其他强引用所持有,那么它所引用的对象就会被回收。比如以下代码:

上述代码运行之后,打印结果如下:

java
before gc, reference.get is com.danny.lagoumemoryleak.WeakRefDemo$BigObject@7852e922
after gc, reference.get is null

WeakReference 的构造函数可以传入 ReferenceQueue,当 WeakReference 指向的对象被垃圾回收器回收时,会把 WeakReference 放入 ReferenceQueue 中。比如我在上述代码中,调用 WeakReference 的构造器时,传入一个自定义的 ReferenceQueue,如下所示:

那么打印结果如下:

java
before gc, reference.get is com.danny.lagoumemoryleak.WeakRefDemo$BigObject@7852e922
before gc, queue is null
after gc, reference.get is null
after gc, queue is java.lang.ref.WeakReference@4e25154f

可以看出,当 BigObject 被回收之后,WeakReference 会被添加到所传入的 ReferenceQueue 中。

再修改一下上述代码,模拟一个内存泄漏,如下所示:

birObject 是一个强引用,导致 new BigObject() 的内存空间不会被 GC 回收。最终打印结果如下:

java
before gc, reference.get is com.danny.lagoumemoryleak.WeakRefDemo$BigObject@7852e922
before gc, queue is null
after gc, reference.get is com.danny.lagoumemoryleak.WeakRefDemo$BigObject@7852e922
after gc, queue is null

实现思路 ​

LeakCanary 中对内存泄漏检测的核心原理就是基于 WeakReference 和 ReferenceQueue 实现的。

  1. 当一个 Activity 需要被回收时,就将其包装到一个 WeakReference 中,并且在 WeakReference 的构造器中传入自定义的 ReferenceQueue。

  2. 然后给包装后的 WeakReference 做一个标记 Key,并且在一个强引用 Set 中添加相应的 Key 记录

  3. 最后主动触发 GC,遍历自定义 ReferenceQueue 中所有的记录,并根据获取的 Reference 对象将 Set 中的记录也删除

经过上面 3 步之后,还保留在 Set 中的就是:应当被 GC 回收,但是实际还保留在内存中的对象,也就是发生泄漏了的对象。

源码分析 ​

在上面原理介绍的例子里,我们知道一个可回收对象在 System.gc() 之后就应该被 GC 回收。可是在 Android App 中,我们并不清楚何时系统会回收 Activity。但是,按照正常流程,当 Activity 调用 onDestroy 方法时就说明这个 Activity 就已经处于无用状态了。因此我们需要监听到每一个 Activity 的 onDestroy 方法的调用。

ActivityRefWatch

LeakCanary 中监听 Activity 生命周期是由 ActivityRefWatch 来负责的,主要是通过注册 Android 系统提供的 ActivityLifecycleCallbacks,来监听 Activity 的生命周期方法的调用,如下所示:

lifecycleCallbacks 的具体代码如下:

可以看出当监听到 Activity 的 onDestroy 方法后,会将其传给 RefWatcher 的 watch 方法。

RefWatcher

它是 LeakCanary 的一个核心类,用来检测一个对象是否会发生内存泄漏。主要实现是在 watch 方法中,如下所示:

解释说明:

  1. 图中 1 处生成一个随机的字符串 key,这个 key 就是用来标识 WeakReference 的,就相当于给 WeakReference 打了个标签;

  2. 图中 2 处将被检测对象包装到一个 WeakReference 中,并将其标识为步骤 1 中生成 key;

  3. 图中 3 处调用 ensureGoneAsync 开始执行检测操作。

因此关键代码就是在 ensureGoneAsync 方法中,代码如下:

通过 WatchExecutor 执行了一个重载的方法 ensureGone。

ensureGone 中实现了内存泄漏的检测,方法具体实现如下:

解释说明:

  1. 图中 1 处会遍历 ReferenceQueue 中所有的元素,并根据每个元素中的 key,相应的将集合 retainedKeys 中的元素也删除。

  2. 图中 2 处判断集合 retainedKeys 是否还包含被检测对象的弱引用,如果包含说明被检测对象并没有被回收,也就是发生了内存泄漏。

  3. 图中 3 处生成 Heap "堆"信息,并生成内存泄漏的分析报告,上报给程序开发人员。

removeWeaklyReachableReferences() 方法如下:

可以看出这个方法的主要目的就是从 retainedKeys 中移除已经被回收的 WeakReference 的标志。

gone(reference) 方法判断 reference 是否被回收了,如下:

实现很简单,只要在 retainedKeys 中不包含此 reference,就说明 WeakReference 引用的对象已经被回收。

LeakCanary 的实现原理其实比较简单,但是内部实现还有一些其他的细节值得我们注意。

内存泄漏的检测时机 ​

很显然这种内存泄漏的检测与分析是比较消耗性能的,因此为了尽量不影响 UI 线程的渲染,LeakCanary 也做了些优化操作。在 ensureGoneAsync 方法中调用了 WatchExecutor 的 execute 方法来执行检测操作,如下:

可以看出实际是向主线程 MessageQueue 中插入了一个 IdleHandler,IdleHandler 只会在主线程空闲时才会被 Looper 从队列中取出并执行。因此能够有效避免内存检测工作占用 UI 渲染时间。

通过 addIdleHandler 也经常用来做 App 的启动优化,比如在 Application 的 onCreate 方法中经常做 3 方库的初始化工作。可以将优先级较低、暂时使用不到的 3 方库的初始化操作放到 IdleHandler 中,从而加快 Application 的启动过程。不过个人感觉方法名叫 addIdleMessage 更合适一些,因为向 MessageQueue 插入的都是 Message 对象。

特殊机型适配 ​

因为有些特殊机型的系统本身就存在一些内存泄漏的情况,导致 Activity 不被回收,所以在检测内存泄漏时,需要将这些情况排除在外。在 LeakCanary 的初始化方法 install 中,通过 excludedRefs 方法指定了一系列需要忽略的场景。

这些场景都被枚举在 AndroidExcludedRefs 中,这种统一规避特殊机型的方式,也值得我们借鉴,因为国内的手机厂商实在是太多了。

LeakCanary 如何检测其他类 ​

LeakCanary 默认只能机检测 Activity 的泄漏,但是 RefWatcher 的 watch 方法传入的参数实际是 Object,所以理论上是可以检测任何类的。LeakCanary 的 install 方法会返回一个 RefWatcher 对象,我们只需要在 Application 中保存此 RefWatch 对象,然后将需要被检测的对象传给 watch 方法即可,具体如下所示:

testedObj 就是一个需要被检测内存泄漏的对象。

总结 ​

这节课主要介绍了 Android 内存泄漏优化的相关知识。主要分两部分:

  • 内存泄漏预防

这需要我们了解 JVM 发生内存泄漏的原因,并在平时开发阶段养成良好的编码规范,避免引入会发生内存泄漏的代码。针对编码规范 Android Studio 可以安装一个阿里代码规范的插件,能够起到一定的代码检查效果。

  • 内存泄漏检测

内存泄漏检测工具有很多 Android Studio 自带的 Profiler,以及 MAT 都是不错的选择。但是相比较而言,使用这些工具排查内存泄漏门槛稍高,并且全部是手动操作,略显麻烦。除了这两个工具之外,我还介绍了一个自动检测内存泄漏的开源库---LeakCanary。主要包括它的实现原理以及部分源码实现细节。