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

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

一. 内存泄漏的基本知识

1. 什么是内存泄漏?

没有用的对象无法被回收

2. 内存泄漏的危害?

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

3. 为什么会内存泄漏?

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

4. 对象什么时候被回收——可达性分析算法介绍
可达性分析算法

能够被GC Root引用的对象是否可达


GC Root

由GCRoot不得不说一下Java内存模型了


java内存模型

简单的介绍一下:
1.程序计数器:线程私有,记录各个线程运行的位置
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()。

  1. 通过重写 Application,提供 getContext ()方法,那样就不需要在获取单例时传入 context。

提示:
1.在实现 SDK 时,也尽量避免造成外部 Context 的泄漏。对传入的Context,主动转换为application Context
2.并不是所有的context能用applicationContext。

Context使用规则

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

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

使用非静态内部类和匿名类都会默认持有外部类的引用,如果生命周期不一致,就会导致内存泄漏。

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

将该内部类设为静态内部类 也可以将该内部类抽取出来封装成一个单例,Handler 泄漏也属于这种.

6. Listener 注册和解绑的声明周期不一致导致

Activity的onResume 注册,在onDestory 解绑,导致多次注册listener 并添加到对应的list中,只解锁最后一个导致内存泄漏。

6.广播,File,IO流,Cursor, Listener资源未释放

三,内存泄漏检测分析实践

3.1 分析已知的内存泄漏

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

MAT是Memory Analyzer tool的缩写,是一种快速,功能丰富的Java堆分析工具,能帮助你查找内存泄漏和减少内存消耗。

很多情况下,需要用hprof-conv处理测试提供的hprof文件,否则会报下边面的错误。

MAT 的使用技巧

技巧一:通过OQL(Objective Query Language )查询对象

由于内存泄漏一般发生在Activity中,查询Activity对象

点击下图中标记的OQL图标 输入 select * from instanceof android.app.Activity,感叹号执行查询。
看到this0引用了这个Activity而this0是表示内部类的意思,this$0又被mLisener引用

技巧二:通过搜索关键字发现内存泄漏对象

方式二:Android Profile 内存泄漏分析

Android Profiler是Android Studio3.0用来替换之前Android Monitor的观察工具,Android Profile 可以分析CPU,Memory,Network,Energy

1.点击View > Tool Windows > Profiler或者点击工具栏的图标即可打开Load file 或者进程

Android Profiler hprof内存泄漏分析

Android Profile 进程内存泄漏分析

这里GC的原因是回收掉软/弱/虚引用。

3.2 寻找并解决未知的内存泄漏

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

  1. App build.gradle 添加依赖,在logcat查看LeakCanary关键字确定是否集成成功
    dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
    }
  2. 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"

泄漏详情


可以看到GC root和引用链.

Logcat也可查看泄漏

这里可以看到hprof文件位置,如果不是很明显可以结合MAT或profiler进行分析。

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

使用强引用对象:

log输出

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 记录
3.最后主动触发 GC,遍历自定义 ReferenceQueue 中所有的记录,并根据获取的 Reference 对象将 Set 中的记录也删除
经过上面 3 步之后,还保留在 Set 中的就是:应当被 GC 回收,但是实际还保留在内存中的对象,也就是发生泄漏了的对象。
在一个Activity执行完onDestroy()之后,将它放入WeakReference中,然后将这个WeakReference类型的Activity对象与
ReferenceQueque关联。这时再从ReferenceQueque中查看是否有没有该对象,如果没有,执行gc,再次查看,还是没有的话则判
断发生内存泄漏了。最后用HAHA这个开源库去分析dump之后的heap内存。

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351