一、Android中内存泄露的经典场景
1. Activity中的Handler长期持有activity引用导致activity泄漏。
原因:
(1). 在Activity内new一个Handler时,该handler实例会对Activity持有一个隐式的引用。
(2). Looper通过loop方法不断循环消息队列,而消息队列中的Message又会持有handler的引用。
(3). 当Activity被finish掉后,消息队列未处理完,而handler持有对activity引用,因此即使activity被finish掉,也不会被gc掉。
解决方法:
(1) 将内部类handler创建到外部类,handler仅持有activity的弱引用。
(2) 将内部类handler设为静态,因为静态内部类不持有对外部类的引用。
(3) 或者在activity的onDestory方法中干掉handler的所有callback和message,mHandler.removeCallbacksAndMessages(null);
2. 非静态匿名内部类、匿名内部类造成内存泄露
原因:
(1)当我们在Activity中直接new一个Thread,但当我们的Activity被finish时,Thread仍在运行,就造成了内存泄露
解决方式:
(1)同样把Thread定义为静态的内部类,这样就不会持有外部类的引用。
3. 单例+依赖注入
原因:
(1)当我们在单例类中(静态实体,生命周期和App一样,非常长)持有Activity/context的对象引用,造成内存泄露
解决方法:
(1)单例的类尽量不要持有Activity/context对象,非要持有的话,可以选择ApplicationContext。
(2)可以持有Activity/context的弱引用,在它们被内存回收的时候避免强引用而造成无法回收的问题。
4. 资源对象没关闭造成的内存泄漏
原因:
(1)Cursor、InputStream、OutputStream、Socket等在使用后,没有进行关闭,然后长期堆积就会导致内存泄漏.
解决方案:
(1)在使用完毕后进行关闭。例如使用完cursor,可以在finally进行cursor.close()。其他同理。
5. webview导致的内存泄漏
原因:
(1) 在销毁webview前没有把webview从父view中移除,会导致内存泄漏。
解决:
(1) 在销毁webview前一定要onDetachedFromWindow,我们先将webview从它的父view中移除再调用destroy方法。
@Override
public void destroy() {
try {
ViewParent parent = getParent();
if (parent != null) {
((ViewGroup) parent).removeView(this);
}
clearView();
removeAllViews();
super.destroy();
} catch (Exception e) {
e.printStackTrace();
}
}
二、提前发现
1.通过工具
(1)通过Android Studio的Monitor工具,打开Memory标签查看内存变化情况,通过dump java head采集数据,生成hprof文件
(2)通过MAT工具,导入hprof文件,它会帮我们分析内存泄露的原因
(3)通过LeakCanary插件,LeakCanary会在app运行时分析当前的内存快照,找到对象的引用链,并显示到页面上,好处是可以直接在手机上查看(如果检测到泄漏会发送到通知栏,点击通知栏就可以跳转到具体的泄漏分析页面。)
2.通过代码
(1)debug版本可以起一个长期工作的线程LeakThread在后台专门做泄漏检测
(2)向Application注册一个页面生命周期的监听:application.registerActivityLifecycleCallbacks
(3)在监听类中对 onActivityDestoryed(Activity activity) 的事件回调做处理:
如果一个Activity走到onDestroy,那么这个Activity对象就是需要被回收的目标。
我们声明一个检测对象的弱引用ref = new WeakReference(activity)
(4)在LeakThread中我们每隔一段时间检测一下ref.get() 是否为空,为空说明activity已被释放。不为空可以手动触一次发gc;如果超过一段时间,比如50s,页面对象还未被清理,我们可以推断内存泄漏的发生。
(5)当内存泄漏发生时,提示给开发者,并自动dump出.prof文件。
三、常见的内存泄漏检测工具
1.MAT
MAT出自于Eclipse公司,可以利用Android Studio的Profile工具里面的内存工具,导出.hprof文件,然后通过Android SDK的hprof-conv命令把.hprof文件转为MAT工具支持的标准.hrpof格式文件,再用MAT打开标准格式的.hrpof文件进行分析,通过分析怀疑对象是否存在,以及排除软、弱、虚引用后查看其引用路径,从而确定泄漏途径。
- MAT下载地址:https://www.eclipse.org/mat/downloads.php
- 使用方式:可以参考《性能优化工具 - MAT》
hprof-conv命令如下:
// mac系统切换到~/Library/Android/sdk/tools/目录执行以下命令
hprof-conv android.hprof mat.hprof
⚠️MacOS打开MAT会报错,正确打开姿势是右键选择“显示包内容”,命令行进入MacOS目录,执行命令打开:
./MemoryAnalyzer -data ./workspace
2.LeakCanary
LeakCanary是Square公司开源的一个内存泄漏检测工具。
- 官方文档:https://square.github.io/leakcanary/
- 使用方法:《性能优化工具 - LeakCanary》
- 触发检测时机:每当Activity/Fragment执行完onDestory的时候,会触发LeakCanary初始化RefWatcher检测该Activity/Fragment是否还在内存中,从而判断是否内存泄漏。
- 触发dump时机:当Activity/Fragment被销毁时,
ObjectWatcher
会对它们持有弱引用,并且会进行gc,gc完毕5s钟后查看Activity/Fragment是否还存在,如果存在的话,就会认为存在潜在的内存泄漏。LeakCanary并不会在发现潜在内存泄漏时就进行dump内存堆栈,而是等到泄漏对象超过一定阈值(APP在前台的话默认5个,如果APP在后台的话默认1个)才会进行dump内存堆栈。 - 如何分析内存堆栈:LeakCanary会使用Shark工具进行内存分析,对于每一个泄漏的对象,它都会找到一个阻止泄漏对象被gc的路径,这个路径就是从垃圾回收器里面找到的泄漏对象的强引用路径。
四、帮助定位线上OOM的方法
1.通过lifecycler监听每一个Activity的创建和销毁
对于一些显式的OOM,如针对一些颗粒度是Activity的泄露,可以在每个Activity的创建和销毁打印Xlog日志,在进行定位线上OOM的时候,可以通过捞取用户日志,过滤相关日志,最终定位是哪一个Activity销毁的,从而解决一些颗粒度为Activity的内存泄露,如退出Activity没有移除延时任务等。
利用这种方法,除了发现一些比较明显的OOM之外,还有作为线索定位其他问题。之前通过这个日志定位到一个由于加载大图导致撑爆内存的问题,当时查了很多用户的日志都没有线索,直到后来捞取到一个用户的日志从启动到OOM崩溃只花了两分钟,于是就跟着用户的操作,进到某一个用户的个人页,发现本地也能复现问题,这才定位出来是由于该用户的个人页里面比较多九宫格图片,在一些比较差的设备由于系统分给App的内存比较小,所以就很容易造成OOM,最终得以解决问题。