二十六、内存泄漏的预防和处理

概述

内存泄漏是 隐形炸弹,它不会像崩溃问题一样立马爆发,但是随着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的核心功能只有两个部分:

  1. 内存泄漏的检测
  2. 泄漏对象的引用链分析

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 引用队列实现的.

其原理总结如下:

  1. 当一个Activity要被回收时,就将其包装到一个WeakReferece 弱引用中,并且在创建 WeakReference时传入一个自定义的 ReferenceQueue
  2. 给上一步创建出来的 WeakReference 做一个标记 key ,并且在一个强引用Set中添加相应的key记录
  3. 主动触发System.gc();,遍历 自定义的 ReferenceQueue, 将查找到的 对象,在 上一步的 Set 中 按照key进行删除。
  4. 遍历完成之后, Set 中还保留着的 对象,就是造成了泄漏的对象了。

文字描述可能比较抽象,我用 流程图表述一下:

LeakCanery源码分析

一个可回收对象,在System.gc()时就应该被回收,但是,在安卓app中,我们并不确定Activity应该何时被系统回收,按照正常流程,Activity在执行 onDestory时,就应该确认该Activity应该被回收。

所以,我们LeakCanery首先要解决的就是,监听所有ActivityonDestory方法的调用。

实现方式也很简单,就是利用了 ApplicationregisterActivityLifecycleCallbacks() 监听所有Activity的生命周期。

上图中监听器的具体实现如下。

这样,就能收集到所有即将销毁的Activity对象了。

负责收集的是 RefWathcer,他是 LeakCanery的核心类,用来检测一个对象是否会发生内存泄漏。

下图是他的工作原理

主要做了三件事:

  1. 创建一个随机字符串作为 key,用来标记要回收的对象
  2. 将要回收的对象包装到 WeakReference中,并用上一步创建的key作为标记
  3. 对 weakReference 进行泄漏检测

最核心的内容,就是在 ensureGoneAsync中。 它的代码如下:

进而跟踪到 ensureGone(),源码如下:

解释说明:

  1. 遍历 ReferenceQueue,按照每一个元素的key,删除 强引用Set中的元素,并同时 移除 ReferenceQueue的该元素(参见上文leakCanery原理图.jpg
  2. 检测 强引用Set中 是否还包含 要回收对象的key,如果包含,说明要回收的对象并没有被回收,也就是发生了内存泄漏
  3. 分析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会让MessageQueuenext方法不停地执行,所以闲时任务一定会在 常规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对象,来对任何对象进行泄漏检测。

相关推荐
2401_882727572 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
NoneCoder2 小时前
CSS系列(36)-- Containment详解
前端·css
anyup_前端梦工厂2 小时前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand2 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL2 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
六卿2 小时前
react防止页面崩溃
前端·react.js·前端框架
z千鑫3 小时前
【前端】详解前端三大主流框架:React、Vue与Angular的比较与选择
前端·vue.js·react.js
m0_748256143 小时前
前端 MYTED单篇TED词汇学习功能优化
前端·学习
小白学前端6664 小时前
React Router 深入指南:从入门到进阶
前端·react.js·react
web130933203985 小时前
前端下载后端文件流,文件可以下载,但是打不开,显示“文件已损坏”的问题分析与解决方案
前端