Android内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到 gc roots 导致无法被GC回收。无用的对象占据着内存空间,使得实际可使用内存变小,形象地说法就是内存泄漏了。
泄漏有哪些危害
运行性能的问题: Android在运行的时候,如果内存泄露导致其他组件可用的内存变少,一方面会使得GC的频率加剧,在发生GC的时候,所有进程都必须进行等待,GC的频率越多,从而用户越容易感知到卡顿。另一方面,内存变少,将可能使得系统会额外分配给你一些内存,而影响整个系统的运行状况。运行崩溃问题: 一旦内存不足以分配某些内存,那么将会导致崩溃,这对于体验而言是致命的。我们在进行内存分析的时候,可以发现总有一些机型会出现OutOfMemory的崩溃栈,大抵都和内存泄露有关。
定位泄漏的方法
女生的小情绪,往往难以琢磨,如果我们忽略这些小脾气,往往又会得到惩罚。因而我们需要寻找一种方法来定位可能的对于内存妹纸的伤害。我们将分别从两个方面,来帮助我们分析和定位。一是宏观方法,通过一些很简单的方法来判断是否存在泄露,另一方面是通过精确定位的方式来提出具体的解决方案。
1)Android Studio Memory Monitor
Android Studio 提供了非常方便的工具,便于我们定位问题。AndroidMonitors模块中含有 Memory Tab,这个Tab以流线的方式,展示了每一时刻内,已分配的内存和还空闲的内存。
图中浅蓝颜色的部分表示已经分配的内存,而灰色部分表明空闲的内存
总选中Devices和相应的包名后,就能看到动态内存分配的情况。
如下图所示,当已分配的内存剧烈下降的时候,就标明发生了GC事件,GC发生的时刻和频率是我们关注的重点。
我们回到刚才的那个例子 #泄漏示例# ,下图是点击MainActivity然后按返回键退出,再进入再退出,重复几次后,内存Monitor显示的结果。从这个例子中,我们可以看到,尽管进过了几次 GC,但是内存用量却一直在增大,说明有些对象被某些静态或者其他GC Roots的对象引用着,导致其不能被释放。因而可以说明,其存在比较严重的内存泄漏问题
2)Android Devices Monitor
Android Devices Monitor提供了比较方便辅助的定位方法,在 Heap Tab下面,显示着% Used的使用量,如果这个值在GC后没有明显下降,那么就意味着发生了内存泄漏,具体的操作步骤如下。
1. 选择DDMS视图,并打开Devices视图和Heap视图
2. 点击选择要监控的进程,比如:上图中我选择的是system_process
3. 选中Devices视图界面上的updateheap图标
4. 点击Heap视图中的CauseGC 按钮(相当于向虚拟机发送了一次GC请求的操作)
第一次点击 CauseGC 后的内存占比
在多次退出和进入后的内存占比
3)精确定位方法
我们在查看是否存在内存泄漏情况的时候,基于的基础单位往往是Activity,因而就可以想到一种思路,即通过在界面回退后,强制进行GC,然后判断是否还存在对该Activity的引用,这样就能得知是否存在泄漏。
MAT 使用简介
具体的的实施步骤如下:
1. 客户端中打开相应的Activity,并执行可能触发内存泄漏的操作.
2. 退出Activity界面,并点击Initiate GC(左起第二个按钮)
3. 点击Dump Java Heap,等待一会后,这个时候可以看到Dump 出来的日志。
4. 由于Android Profile文件不被 MAT 支持,因为我们需要执行转换操作。 ./hprof-convpath/file.hprof exitPath/heap-converted.hprof
5. 在 MAT 中打开文件,并选择Leak Suspects Report,等待最后的结果。
6. Select* From instanceof android.app.Activity 通过Activity的类名来过滤信息,在右键菜单里面,分别点击MergePaths to Shortest GC Root 和 exclude allphantom/weak/soft etc. references, 排除被弱引用持有的情况。
4)LeakCanary 自动定位
Square 开源了LeakCanary来用作对于内存泄露情况的自动检测。
LeakCanary实现了引用观察者RefWatcher。RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。通过在Activity重要的生命周期中,在后台线程检查引用是否被清除,如果没有,调用GC。如果在GC后,引用还是未被清除,那么可能发生了内存泄露,这时候把heap内存dump到 APP 对应的文件系统中的.hprof 文件中。在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA来解析这个文件。得益于唯一的 reference key, HeapAnalyzer 找到KeyedWeakReference,定位内存泄露。HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。
可以看到,Square在使用LeakCanary并进行相应的修改后,效果还是相当不错的。
由于官方开源的LeakCanary只能在Debug版上使用,在Release上通过NullObject方式实现了一个空实现,来避免性能问题。如果想通过小流量的方式来批量地发现用户内存泄露的情况,那么就需要对源码进行整改,刚好我做了这么一件事情,有兴趣的人可以拿去使用。移除UI展示等逻辑后可在Release上使用的LeakCanary。有了用户相关的数据的泄露栈就能很好地处理各种泄露问题,使得应用良好稳定地运行。
常见内存泄漏CASE与修复方法
泄漏CASE
1. 注册对象未反注册
在组件启动后,注册了某个对象的观察者,在组件回收的时候,忘记取消注册了。可以参考这样的例子,Activity声明的时候实现了对于下载进度接口的监听,而这个监听接口在实现的时候使用的是强引用,如果不进行主动反注册,Activity会因为被下载库持有引用,从而导致无法回收。
2. 长线执行的异步任务
组件内部有一个可能长时间执行的任务,通过内部类持有了对组件的引用。想象这样一个场景,界面上的某一个组件需要异步地去请求天气数据,在得到结果后显示在界面上。在网络回调的Callback中,持有了这个组件,从而在网络请求执行过程中,组件是无法进行回收的。
3. Android SDK的泄露
这类泄露一般不严重,不用特殊处理。比如TextLine.sCached对象会持有一个拥有三个TextLine的对象池,但TextLine的回收方法recycle处理得有bug,在android-5.1.0_r1修复了一部分,修复连接。其他的泄露地方可从这里看出一部分,SDK泄露统计。
4. 类的静态变量持有大数据对象
静态变量长期维持到大数据对象的引用,阻止垃圾回收。
5. 资源对象未关闭象
资源性对象如Cursor、File、Socket,应该在使用后及时关闭。未在finally中关闭,会导致异常情况下资源对象未被释放的隐患。
6. Handler 泄漏
Handler通过发送Message与主线程交互,Message发出之后是存储在MessageQueue中的,有些Message也不是马上就被处理的。在Message中存在一个target,是Handler的一个引用,如果Message在Queue中存在的时间越长,就会导致Handler无法被回收。如果Handler是非静态的,则会导致Activity或者Service不会被回收。handler在使用过后,在组件退出的时候没有处理这些handler。通过Handler post出去一个任务后,没有在最后调用removeCallbacks的接口,清除掉所有跟这个Runnable相关的message。
修复方法
1. 尽量避免在组件内部使用内部类,内部的一些逻辑类可以使用Static的声明,避免持有对组件的引用。
2. 如果一定要持有内部类的引用,可以通过WeakReference来进行封装,这样可以缓解掉一些泄漏情况。
3. 对于Handler使用较多的情况,可以考虑使用WeakHandler
4. 正确关闭资源,对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销。
5. 在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完Bitmap 后先调用 recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear() ; array = null)等,最好遵循谁创建谁释放的原则。
6. 对 Activity 等组件的引用应该控制在 Activity 的生命周期之内; 如果不能就考虑使用getApplicationContext 或者 getApplication,以避免 Activity 被外部长生命周期的对象引用而泄露。