Android内存泄漏原理分析和优化实践

内存泄漏是Android开发必须重视的问题,它可能导致应用性能低下,内存抖动,甚至OOM.如何检测和分析OOM是必须要掌握的知识.本文将从三个方面来介绍内存泄漏问题,分别是内存泄漏的基础知识,常见的内存泄漏场景和内存泄漏分析和解决办法.

一. 内存泄漏的基本知识

1. 什么是内存泄漏?

没有用的对象无法被回收

2. 内存泄漏的危害?

• 应用可用的内存减少,增加了堆内存的压力
• 严重的时候可能会导致OOM Error
• 触发更频繁的GC ,造成卡顿,内存抖动等现象

3. 为什么会内存泄漏?

长生命周期的对象持有短生命周期对象“强/软引用”,导致本应该被回收的短生命周期的对象却无法被正常回收。

4. 对象什么时候被回收——可达性分析算法介绍
2.虚拟机栈:每个method,线程私有,存放局部变量,方法结束后自动释放内存。
3.本地方法栈:native method。
4.方法区:它主要存放静态数据,全局变量,编译时就分配好,在程序整个运行期间都存在。
5.堆:通常用来存放 new 出来的对象。由 GC 负责回收。
由长生命周期的对象持有短生命周期对象“强/软引用”,介绍一下四种引用类型:

二,常见的内存泄漏场景

注意:Activity 内存泄漏预防

Activity内部持有大量的资源引用以及与系统交互的 Context,这会导致一个 Activity 对象的 retained size 特别大。一旦 Activity 因为被外部系统所持有而导致发生内存泄漏,被牵连导致其他对象的内存泄漏也会非常多。
这里介绍一下Retained Size:

Shallow size:对象本身占用内存的大小。 Retained Size: 对象本身的Shallow Size + 对象能直接或间接访问到的对象的Shallow Size

1.静态变量造成的内存泄漏——将 Context 或者 View 置为 static,

原因:静态变量的生命周期与Application相同,编译时就分配好,在程序整个运行期间都存在。
View 默认会持有一个 Context 的引用,如果将其置为 static 将会造成 View 在方法区中无法被快速回收,最终导致 Activity 内存泄漏。

由于static handler中要使用view进行更新,这里提供的方法是去掉static 修饰,提供一个方法,获得对应的对象。

2. 非静态 Handler 导致 Activity 泄漏

原因:由于 Handler 属于 TLS(Thread Local Storage)变量,对应GCRoot:仍处于存活状态中的线程对象,导致它的生命周期和 Activity 不一致。
非静态内部类,持有外部类的强引用。因此通过 Handler 来更新 UI 一般很难保证跟 View 或者 Activity 的生命周期一致,故很容易导致无法正确释放。

3.单例或三方库使用Activity context

原因:由于单例模式的静态特性,使得它的生命周期和我们的应用一样长,一不小心让单例无限制的持有 Activity 的强引用就会导致内存泄漏。
解决方案:把传入的 Context 改为同应用生命周期一样长的 Application 中的 Context。两种方式:
1.context.getApplicationContext()。

  • 通过重写 Application,提供 getContext ()方法,那样就不需要在获取单例时传入 context。
  • 1.在实现 SDK 时,也尽量避免造成外部 Context 的泄漏。对传入的Context,主动转换为application Context
    2.并不是所有的context能用applicationContext。

    Context使用规则

    • NO1 表示 Application 和 Service 可以启动一个 Activity,不过需要创建一个新的 task 任务队列。
    • 对于 Dialog 而言,只有在 Activity 中才能创建。

    5.非静态内部类和匿名类导致的内存泄漏

    使用非静态内部类和匿名类都会默认持有外部类的引用,如果生命周期不一致,就会导致内存泄漏。
    非静态内部类默认会持有外部类的引用,而外部类中又有一个该非静态内部类的静态实例,该静态实例的生命周期和应用的一样长,而静态实例又持有Activity 的引用,因此导致 Activity 的内存资源不能正常回收。

    3.1 分析已知的内存泄漏

    方式一:MAT分析和使用技巧

    MAT是Memory Analyzer tool的缩写,是一种快速,功能丰富的Java堆分析工具,能帮助你查找内存泄漏和减少内存消耗。
    很多情况下,需要用hprof-conv处理测试提供的hprof文件,否则会报下边面的错误。

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

  • App build.gradle 添加依赖,在logcat查看LeakCanary关键字确定是否集成成功
    dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
    Android TV 注意项

    Monkey简介

    monkey -p com.mediatek.wwtv.tvcenter -c android.intent.category.DEFAULT --throttle 500 --pct-nav 25 --pct-majornav 25 --pct-syskeys 25
    --pct-appswitch 25 --ignore-crashes --ignore-timeouts --ignore-security-exceptions --kill-process-after-error -v -v 10000
    命令解释:

    -c 指定activity的category类别,注意activity应该指定,否则测试的脚本不会执行该Activity
    --throttle:后面接时间,单位为ms,表示事件之间的固定延迟(即执行每一个指令间隔的时间),如果不接该项,monkey将不会延迟
    --pct-nav:后面接基本导航事件百分比,主要来自方向输入设备的上、下、左、右事件
    --pct-marjornav:后面接主要导航事件百分比,通常指引发图形界面的一些动作,如键盘中间按键、返回按键、菜单按键等
    --pct-syskeys:后面接系统按键事件百分比,通常指仅供系统使用的保留按键,如HOME键、BACK键、拨号键、挂断键、音量键等
    --pct-appswitch:后面接应用启动事件百分比,应用启动事件(activity launches)即打开应用,通过调用startActivity()方法最大限度地开启该package下的所有应用
    --pct-touch:后面接触摸事件百分比,触摸事件泛指发生在某一位置的一个down-up事件,点击
    --pct-motion:后面接动作事件百分比,动作事件泛指从某一位置接下(即down事件)后经过一系列伪随机事件后弹出(即up事件)
    -v -v指定monkey报告等级,一个 -v增加一个级别,默认缺省值是0级,
    -v,Level 0(缺省值),除启动提示、测试完成和最终结果之外,提供较少信息
    -v -v,Level 1,提供较为详细的测试信息,如:逐个发送到Activity的事件
    -v -v -v,Level 2,提供更加详细的设置信息,如:测试中被选中的或未被选中的Activity

    3. TV 端查看泄漏列表详情

    adb shell am start -n "com.xxx/leakcanary.internal.activity.LeakLauncherActivity"

    LeakCanary 原理浅析

    LeakCanary 号称在App 运行过程中检测内存泄漏,当内存泄漏发生时会生成发生泄漏对象的引用链那它如何检测内存泄漏?
    先说一下WeakReference和ReferenceQueue
    WeakReference 的构造函数可以传入 ReferenceQueue,当 WeakReference 指向的对象被垃圾回收器回收时,会把 WeakReference 放入ReferenceQueue 中

    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

    使用强引用对象:

    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

    可以看出未GC成功,queue为空.这里请注意一下,这里的bigobject对象有强引用和虚引用两种对象,只有只包含虚引用的对象在GC时会被回收.

    LeakCanary 中对内存泄漏检测的核心原理就是基于 WeakReference 和 ReferenceQueue 实现的。
    1.当一个 Activity 需要被回收时,就将其包装到一个 WeakReference 中,并且在 WeakReference 的构造器中传入自定义的 ReferenceQueue。
    2.然后给包装后的 WeakReference 做一个标记 Key,并且在一个强引用 Set 中添加相应的 Key 记录