Android内存泄漏案例4讲#
概述#
主要讲解内存泄漏的4种情况:Activity、fragment、view、异步任务
内存问题概述#
内存泄漏的关注点——对象占据的内存
- 本该回收的对象,无法被垃圾处理器回收;
- 程序不再使用的对象,无法被垃圾处理器回收;
- 在页面生命周期销毁后,页面持有、创建过的对象无法被垃圾处理器回收;
- 在任务(线程、Service、广播)结束后,任务持有、创建过的对象无法被垃圾处理器回收
Android内存问题l类型概述
内存抖动的表现形式:
- 忽高忽低,锯齿状
内存泄漏的表现形式:
- 页面或任务已经结束,相关的对象仍然被GC root索引,导致垃圾回收器无法回收,内存片段仍然存在
内存溢出的表现形式:
App可用内存已到达系统规定的上限,如某型号手机规定单个App最大可分配内存为196mb
系统能为App分配的内存不足,系统可分配内存已经到达上限,如系统可用只有1mb,app需要20mb
本文要讲的是内存泄漏的4种类型。
内存泄漏的危害#
典型的例子:创建8000个实例就会导致内存溢出了
1 | package org.example.a; |
Android中内存泄漏的判断方式#
判断一个对象是否无法被回收,最佳的判断方式是定义清楚这个对象预期的生命周期,通常对象是随着页面、任务的生命周期产生和消亡的,Android中可以借此机制判断是否存在泄漏现象:
Activity
:通过查看Activity#mDestroyed
属性来判断Activity
是否已经销毁,如果为true,表明该Activity
已经被标记为销毁状态,此时hprof文件中若仍然存在此Activity,则表明这个Activity
占据的内存处于泄漏状态;Fragment
:通过Fragment#mFragmentManager
属性来判断该Fragment
是否处于无用状态,如果mFragmentManager
为空,且hprof文件中若仍然存在此Fragment
,则表明Fragment
占据的内存处于泄漏状态;View
:通过un wrapper#mContext
获得Activity
,如果Activity
不为空,则按照判断Activity
的方式判断Activity
是否泄漏ContextWrapper
:通过unwrapper ContextWrapper获
得Activity
,如果Activity
不为空,则按照判断Activity的方式判断Activity是否泄漏Dialog
,通过判断mDecor
是否为空。mDecor
为空,表明Dialog
处于不被引用的状态,mDecor
仍然存在在hporf里且为空,则表明Dialog泄漏了MessageQueue
:通过判断MessageQueue#mQuitting
或mQuiting
是否退出,如果该值是true
且未被回收,则认为是泄漏。ViewRootImpl
:通过ViewRootImpl
是否为空来判断,为空表明处于无用状态,为空且未被回收则被认为是泄漏Window
:通过Window#mDestroyed
来判断window
是否处于无用状态,mDestroyed
为true
且未被回收,则认为是泄漏Toast
:拿到mTN
,通过mTN#mView
是否为空来判断当前Toast
是否已经hide
,如果mView
为空,表明Toast
已经hide
,此时Toast
未被回收则认为是泄漏Editor
:Editor
是用于TextView处理editable text
的辅助类,通过ditor#mTextView
为空来判断Ediator
是否处于无用状态;如果mTextView为空且未被回收则认为Editor
泄漏了
内存泄漏的典型 场景
- 非静态内部类、非静态匿名内部类持有外部类对象的引用:常见的如
Listener
、CallBack
、Handler
、Dialog
- 非静态的
Handler
,持有Activity
,Message
持有Handler
,Message
被MessageQueue
持有,主线程MessageQueue
持久存在,导致Activity不会被释放 - 资源对象未关闭:数据库连接、
Cursor
、IO
流使用完后未close - 属性动画:未及时使用
cancel
关闭;Animator
持续存在,导致Animator
持有的Activity
、Fragment
、View
泄漏(Animator#updateListener
一般都是匿名内部类,匿名内部类的问题参考场景1) - 逻辑问题:广播监听后未及时解注册;
认识Hprof文件#
Hprof文件导出
- 通过调用
Debug.dumpHprofData(String filePath)
方法来生成hprof文件 - 通过执行shell命令
adb shell am dumpheap pid /data/local/tmp/x.hprof
来生成指定进程的hprof文件到目标目录
Heap分区:
app heap
:当前App
在堆中占据的内存image heap
:系统启动镜像,包括启动期间预加载的类zygote heap
:所有App
的父进程,所有App
共享zygote
的内存空间,zygote
预加载了许多资源和代码,供所有App
读取
Instance View:
depth
:从gc root
到当前对象的引用链最短深度。被gc root引用到的对象不会被回收。个人收获:
depath = 0
,表示不被gc root引用,即会被垃圾回收器回收。个人收获:常见
gc root
有5种——局部变量、Activity threads
、静态变量、JNI
引用、类加载器native size
:native
对象占据的内存大小shallow size
:对象本身占用的内存,何为对象本身?不包括它引用的其他实例个人收获:
shallow size = 类定义+ 父类变量所占空间大小 + 类成员变量所占空间 + [ alignment]
类定义:固定为8
Byte
父类变量所占空间大小:当前类继承了其他类的成员变量,显然这些变量是占用空间的
自身变量:当前类的成员变量,如果是基本数据类型,则按基本类型计算;如果是引用数据类型,则固定为4byte,当前类仅持有变量名,
alignment
:指位数对齐,目的是让shallow zie
的值为8的倍数,如某个类A,前三项计算结果未15byte
,则shallo size
为了达到8的倍数,会设置一个alignment
值,凑够16byte
。retained size
:Retained Size是指, 当实例A被回收时, 可以同时被回收的实例的Shallow Size之和
Instance View易混淆概念:
shallow size:
Shallow Size
是指实例自身占用的内存, 可以理解为保存该’数据结构’需要多少内存, 注意不包括它引用的其他实例
retained size:
实例A的Retained Size
是指, 当实例A被回收时, 可以同时被回收的实例的Shallow Size之和
图中A, B, C, D四个实例, 为了方便计算, 我们假设所有实例的Shallow Size都是1kb
删除D实例:垃圾回收期会移除D实例;D实例的Retained Size=Shallow Size=1kb
删除C实例:垃圾回收器会移除C和D实例;C实例的Retained Size = C实例的Shallow Size + D实例的Shallow Size = 2kb
删除B实例:垃圾回收器会移除B实例,因为C仍然被A引用,所以C不会被移除,同理D也不会被移除;B实例的Retained Size=Shallow Size=1kb
删除A实例:垃圾回收器会移除B、C、D实例;A实例的Retained Size=4kb
怎么从prof文件内分析内存泄漏?
熟能生巧——memory profiler
实时预览功能区:
Hprof文件预览区:
案例1——hprof文件显示出了内存泄漏#
本案例如图所示,Profiler将内存泄漏的分析结果直接显示到了工具栏上,我们只需要按步骤展开方法调用链即可找到泄漏点
①点+导入hprof
文件
②选择app
堆存储
③根据类名排序
④仅展示Activity
、Fragment
内存泄漏
⑤过滤关键字,如app
进程的包名
⑥点击Leaks
,下方Class Name
区域会展示出内存泄漏的类名
⑦在Class Name
区域点击选择要观察的类名
⑧在Instance List
区域点击选择该类的实例
⑨在Instace Details
区域点击References
选项卡
⑩选中 GC root only
,一层一层展开引用链
也可以在第9步时候观察Activity的生命周期来判断内存泄漏
可以看到一个页面存在三个实例,这三个实例都因为某些原因无法被垃圾回收器回收。我们逐一分析下。
第三个Activity实例:
首先点击Fields
,查看Activity
的生命周期,可以看到Instacen Details - Fields -Instance视图
中Activity#mDestroyed = true
,表明此页面已经销毁了
Android中内存泄漏的判断方式#Activity内存泄漏判断方式一节也提到观察mDestroyed字段来判断Activity是否出现了内存泄漏
其次点击References
,点一层层展开引用链,可以看到Activity
使用了PlayUtil
,PlayUtil
初始化的时候传入了此Activity
,而PlayUtil
是一个单例类,该单例类全局持有了此Activity
的实例,导致Activity
一直被PlayUtil
持有着,在Activity
生命周期结束后,Activity
占据的堆内存,无法被垃圾回收器回收,出现了泄漏的情况。
单例类持有Activity
,导致Activity
占据的内存无法被释放,遇到这种问题,解决起来也很容易:
- 替换
context
,使用ApplicationContext
- 使用一个更轻量无任何业务的
Activity
来初始化PlayUtil
- 如无必要不要使用
context
PlayUtil
问题,笔者采用了方法3:
修改前
1 | private PlayUtil(Context context) { |
修改后
1 | private PlayUtil(Context context) { |
这样可以达到PlayUtil
不持有Activity
的目的,在Activity#onDestroyed
时候,Activity
占据的内存将被垃圾回收器回收。
接着我们继续看该Activity的第二个实例,查看该实例的生命周期可知该Activity已经处于Destroyed状态,但内存未被回收,这是为什么呢?
我们继续查看References
,可以看到Activity被LoadProgress
持有了,且无法被释放。
笔者分析可能是Activity#showLoading
期间,Activity
转入后台或其他因素,并未来得及调用dissLoading
导致LoadProgress
工具类持续持有该Activity的引用,产生了内存泄漏。我们来看看问题代码
问题代码
Activity#initLoading,传入了当前Activity
1 | private void initLoading() { |
LoadProgress内使用HashMap缓存了传入的Activity
1 | this.dialogs.put(context, lp); |
解决方法
遇到这种问题也很好处理——在适当的时机清除HashMap
缓存的Activity
引用。
按照这种思路,我们看到:LoadProgress
工具类里提供了HashMap
的清空方法LoadProgress#cleanUpTrash
,那么我们在合适的地方,清空`Activity缓存即可
在Activity#onDestroyed
调用
1 | @Override |
这样,当Activity
生命周期处于onDestroyed
的时候,HashMap
会清空持有的Activity
,避免Activity
内存泄漏
最后我们来看Activity
的第一个实例
有前两个泄漏点的分析经验,可得知Activity
此时已经处于onDestroyed
状态,被某个类引用了,导致Activity的
内存无法回收.
熟能生巧,我们按图索骥可看到是MediaplayerIml
通过mMediaPlayListenerCacheList
持有了该Activity,该类是用于存储播放回调的,用于AIDL
服务端通知Client
的回调,更新播放界面。
问题代码
Activity#onCreate
时候调用了注册回调
1 | mediaPlayerIml.registerListener(playListener); |
MediaPlayerIml#registerListener
内部通过list
缓存了Activity
1 | private List<MediaPlayListener> mMediaPlayListenerCacheList = new ArrayList<>(); |
解决方法
在适当的时机清除MediaPlayerIml
持有的Activity
:
Activity#onDestroy
是个很不错的选择
1 |
|
总结3个内存泄漏点
PlayUtil
构造函数持有Activity
未释放LoadProgress
成员变量HashMap
持有Activity
未释放AIDL
远程服务端持有Listener
,Listener
持有Activity
未释放
优化效果
优化前Activity
进入3次创建了3份实例:
优化前,多次进入该页面,产生4个实例,有3个实例无法被垃圾回收,出现了内存泄漏的情况
优化后,多次进入该页面,只产生一个实例
根据Retained Szie
来看,优化前后节省内存,效果减少至原先的 (49937/332 = )$1/150$ 。由此可见Activity
及时回收,极大的节省了内存占用。
案例2——hprof文件显示出Fragment内存泄漏#
接下来我们来看fragment内存泄漏,老规矩查看fields和references,确保它符合内存泄漏的情形;我们点击jump to source查看泄漏的位置
问题代码:Fragment#MZBannerView#内部类Runnbale
1 | /** |
可以看到Fragment
内存泄漏的第一个原因,内部类runnable
持有了view
的实例,每个Runnbale
会发送一个延时5秒的消息,消息发送期间,有可能view
、fragment
已经结束了生命周期,此时产生了内存泄漏。
解决办法也很简单,view离开窗口的时候,释放Handler
中消息,释放Runnbale
对view
的引用。
- 静态
Runnbale
内部类+对view
的弱引用(此部分代码与前面的示例很相似,不重复贴代码了) - 离开窗口
remove#handler
消息
view
对Activity
暴露了pause
方法,在Activity
销毁时,强制清空handler
的任务;
1 | /** |
Activity代码:
1 |
|
案例3——view内存泄漏#
前文提到,profile#Leaks
视图无法展示非Activity
、非Fragment
的内存泄漏,换言之,除了Activity
、Fragment
的内存泄漏外,其他类的内存问题我们只能自己检索hprof
文件查询了。
下面有一个极佳的view内存泄漏例子,它的操作步骤为:
- 播放音乐,唤醒音乐悬浮窗
- 播放一段时间后,关闭音乐悬浮窗
- 重复步骤1和2
悬浮窗长这个样

我们重复三次之后,得到一份hprof文件,下面我们来分析一下内存泄漏问题
①输入view
的名称
②选择view
③可以看到分配了3个实例对象
④Instance List
视图显示,view
有3个实例对象及其引用
我们从上至下依次看3分实例的调用链
view的第一个实例
先查看Fields区域,观察mLayoutmode
值,判断view
是否离开了窗口,如果已经离开了窗口,表明view未被回收,存在内存泄漏
可以看到mLayoutMode = -1
,表明布局已经离开屏幕了,此实例存在内存泄漏的情况
接着我们查看References
区域,逐级点开我们发现Handler
发送的Message
持有了当前view
,导致view
在离开窗口的时候,无法被垃圾回收器回收。
右键点击查看问题代码
问题代码:
1 | playHandler.post(new Runnable() { |
看到new Runnbale
,这是是匿名内部类,匿名内部类持有当前类的引用,匿名Runnbale
未执行完毕,Runnbale
内存未释放的时候,view
就无法被释放,而匿名Runnbale
的释放时机不可控,由Handler
、Looper
、Runnbale
执行情况影响。
那么我们该怎么优化呢?
- 使用非匿名或静态的
Handler
+弱引用,处理此任务 - 在主线程处理此任务
view
退出的时候释放Message
对view
的引用
笔者采用了方案3:
1 | tv_play.setText(playItem.getProgramTitle()); |
遇到了异常提示非UI线程更新UI,可知该代码块默认在子线程执行
1 | ViewRootImpl: Accessibility content change on non-UI thread. Future Android versions will throw an exception. |
遂作罢放弃方案2,方案1代码与下面view的第三个实例写法一致,不重复写了;我们解释一下方案3:
view退出的时候释放Message对view的引用
根据上图所示,我们看到Message-Runnbale-View
的引用关系可知,Looper
中的Message
持续的引用view
,我们最高效释放内存的做法是view
离开窗口的时候,斩断Message
与view
的引用关系,那么我们该怎么做呢?答案是:
- 结束子线程任务
- 清空
Looper
缓存的Message
- 释放
Handler
第一步:结束子线程任务很简单
1 | thread.interrupt() |
本案例给Handler
传入的是Runnbale
,Handler
未提供结束Runnbale
的接口,此项优化搁置
第二步:清空Message
已知Looper
提供了清空Message
的接口
Looper#quit
Looper#quitSafely
- 主线程的Looper无法退出
已知Handler提供了释放Message的接口
Handler#removeCallbacksAndMessages
那我们优化起来就很简单了,清空Handler
持有的Message
1 |
|
主线程调quit
和quitSafely
会抛异常,所以Looper相关API无法在主线程调用,不适合当前的例子,遂作罢:
1 | java.lang.IllegalStateException: Main thread not allowed to quit. |
我们继续看view的第二个实例
先查看Fields区域,观察mLayoutmode
值,判断view
是否离开了窗口,如果已经离开了窗口,表明view未被回收,存在内存泄漏
可以看到mLayoutMode = -1
,表明布局已经离开屏幕了,此实例存在内存泄漏的情况
接着我们看References区域,观察调用链
可以看到MediaPlayerIml
有一个成员变量mMediaPlayListenerCacheList
,缓存了MediaPlayListener
,MediaPlayListener
又是在view
实例里面创建的,并且作为内部类,它持有view
的实例。现在我们得到了清晰的调用链,MediaPlayerIml->mMediaPlayListenerCacheList->MediaPlayListener->view
,MediaPlayerIml
引用view
导致view
实例无法被释放
查看问题代码:
笔者发现view#onDetachedFromWindow
已经触发了移除list#listener
操作
1 |
|
可以看到内部实现是remove
调引用的
1 | /** |
那为什么会未回收持续占用内存呢?
- 抓拍
hprof
文件期间,代码未执行到unregisterListener
,导致view
内存未得到释放 mMediaPlayListenerCacheList
添加的listener
与remove
的listener
不是同一个- 此处没有产生内存泄漏,判断
view
是否应该被回收的依据有问题
搁置疑问,接着我们来看view的第三个实例,节省时间,笔者直接调到代码索引出,展示问题代码:
view#非静态内部类
1 | /** |
可以看到此处还是使用了非静态内部类m_handler
,m_handler
持有当前view
的引用,m_handler
如果长期存在,那么view
的内存也不会被释放
解决方法如下:
- 定义外部类
Handler
- 定义静态内部类
- 定义静态内部类+弱引用
笔者采用了方案3:
定义静态内部类
1 | private static class UpdateHandler extends Handler { |
在view
使用时,初始化handler
,构造参数传入组件id;
1 | m_handler = new UpdateHandler(MediaPlayerIml.getInstance(),mSeekBar,mPrograssBar); |
在view离开窗口时候,销毁handler数据;
1 |
|
总结我们针对此按理做的优化
- 静态
Handler
+弱引用,释放了对handler
对view
的引用,让view
及时销毁,view占据的内存及时被垃圾回收器释放 - 释放了
Message
对view
的引用,在view及时退出界面的时候,立即斩断message
对view
回顾一下优化前的实例数量,多次操作,隐藏展示悬浮窗之后,内存中存在多份悬浮窗实例,之前创建过的悬浮窗内存一直无法被回收:
优化后效果,多次操作,当屏幕上存在一个view
时,只存在一份view
实例:
在InstanceList
里可以看到Instance
数量为1,同样的操作次数下,优化前占用内存280824byte;优化后750byte,内存减少至原先的$1/374$;由此可见,view的就及时回收,极大的节省了内存占用。
案例4——异步任务内存泄漏#
异步任务,代指起子线程异步完成一些数据操作、网络接口请求等,通常会使用以下API:
Runnbale
,Thread
,线程池RxJava
HandlerThread
Timer
定时器
而这些异步任务很有可能操作内存泄漏,下面我们以Rxjava
为例,演示此问题,线程、线程池的问题也类似,就不再一一演示了。
大多数项目的网络基础库,传入Rxjava的是匿名Observer
,任务过多时,未执行的任务的Observer
会持有当前页面的引用,造成内存泄漏,演示出这个场景
那么问题来了
rxjava
就会存在内存泄漏吗?subscribe
传入的匿名内部类Consumer
实例不会造成内存泄漏吗?- 异步任务返回时,
Activity
已经处于onDestroyed
状态,Observer
持有``Activity引用,
Activity`内存还能被回收吗?
我们来验证一下rxjava
的泄漏场景:
假设我们在Activity#onResume
方法里,写了异步任务,任务结束后,设置view的属性,在任务结束之前,我们会调用Activity#finsh
操作退出当前页面,如下坨屎:页面在12秒后实际已经处于`onDestroyed状态了
为了演示问题,我将延时时间增大,写成12秒,模拟异步任务返回的情况
1 | Observable.timer(12000, TimeUnit.MILLISECONDS) |
测试步骤为:进入Activity
-立刻退出Activity
-一段时间之后观察Activity
的内存是否被回收
我们得到一份hprof
文件,来分析下
老规矩先看下Instance-Details-Instance
区域,Activity
的生命周期onDestroyed
的值是否为true
,按步骤点击一看,确实为true,证明Activity
已经离开窗口了,处于销毁的生命周期中,我们期望的时候垃圾回收器可以回收Activity占据的内存,但事实上我们在Hprof文件看到了,表明Activity
占据的内存未回收。
紧着着我们面临下一个问题,如何找到导致Activity内存泄漏的原因呢?谁引用了Activity
?
点击Instance-Details-References
区域,我们可以很快得到答案,按步骤点击Jump to Source
果然,立刻跳转到内存泄漏所在的代码块,终于我们通过分析hprof文件找到了问题所在:
那么如何解决此问题呢?rxjava
提供了CompositeDisposable
解决此类泄漏问题,做法如下:
- 创建实例对象
1 | /** |
- 用
compositeDisposable
实例去控制任务的生命周期
1 | compositeDisposable.add(Observable.timer(12000, TimeUnit.MILLISECONDS) |
- 页面生命周期
onDestroyed
期间清空任务
1 |
|
优化后的效果:
优化后可看到Depth
为空,GC root
为空,表明没有其他实例引用Activity
了,当垃圾回收器扫描到此实例,该实例内存会被回收。
还记得开头的问题吗?
rxjava
就会存在内存泄漏吗?答:会存在,consumer
作为Activity
的内部类,持有当前Activity
的引用,任务未结束,Activity
已销毁就会出现内存泄漏subscribe
传入的匿名内部类Consumer
实例不会造成内存泄漏吗?答:只要是匿名内部类,就很有可能内存泄漏,上例子已经证明会产生内存问题。- 异步任务返回时,
Activity
已经处于onDestroyed
状态,Observer
持有``Activity引用,
Activity`内存还能被回收吗?答:无法被回收
片段式的经验#
- 静态内部类多次创建,有什么问题吗?-不确定
- 静态内部类的实例,需要手动释放吗?-不确定
- 仍未找到准确的属性能够判定view是否离开窗口
- 静态类要调用一些非静态方法,可能需要扩展更多的弱引用实例通过静态类构造函数传进来
- 跨App跳转,很难避免标准模式的
Activity
不被多次创建,但是可以做到Activity退出一次,就释放一次Activity内存,这是基本要求 - 静态内部类使用某些实例前,可能需要判断该实例是否为空——在异步场景很容易出现空指针
- 尽量不要在
xml
中定义一些view
,如TextureView
、Surfaceview
,他们默认是与Activity
绑定的,他们的构造函数传入的context
是Activity
实例,有可能造成内存泄漏,最好改为代码动态创建这些复杂的view - glide为什么会内存泄漏?
- MVP为什么会内存泄漏?参考DeviceSetFragment的泄漏点3例子
context
被view
持有,view
需要及时释放对context
的引用,释放方式为view = null
;这样就可以减少指向context
的引用了;当指向context
的引用总数为0
,context(Activity)
就会被回收了- 有些内存问题需要特定的条件才能出现,如开启关闭摄像头、输入搜索框之后清空搜索框、先进入B页面再回退刷新A页面,monkey无法替代人来操作这些步骤,人为点击还是有意义的
- 发现一个泄漏点,解决完之后,再重复测试,才有可能发现下一个泄漏点,只有解决第一个泄漏点,才能发现其他未出现的泄漏点
- 优化完之后可能会发现原先无法复现的新问题,优化之后要继续进行大量的重复测试,直到确保不出现泄漏情况才能结束优化工作
待求证的疑问#
System.exit(0)
杀死进程,能释放该进程下的所有内存吗?- 非静态内部类持有当前类的引用,还是非静态内部类的实例持有当前类的引用?
- 有必要把所有的内部类都定义成静态吗?参考java - Java 内部类有坑。。100 % 内存泄露!_个人文章 - SegmentFault 思否、(8 封私信 / 80 条消息) 为什么Java内部类要设计成静态和非静态两种? - 知乎 (zhihu.com)