Android 内存泄露分析

转载请注明出处: http://woaitqs.github.io/android/2016/02/01/android

摘要 最近在开发过程中遇到了不少内存泄露的问题,而且也由此引发了崩溃率的升高,因为花了一些时间来解决内存泄漏的问题。在这里将发现、定位和解决问题中得到的一些经验分享出来,便于诸君参看。

一、什么是内存泄漏

内存泄露在编码过程中经常会遇到,通常我们在系统的崩溃栈中,能够看到与下面类似的异常。

java.lang.OutOfMemoryError: Failed to allocate a 4308492 byte allocation with 467872 free bytes and 456KB until OOM
at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:609)
at android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:444)
at android.graphics.drawable.Drawable.createFromResourceStream(Drawable.java:988)
at android.content.res.Resources.loadDrawableForCookie(Resources.java:2580)
at android.content.res.Resources.loadDrawable(Resources.java:2487)
at android.content.res.Resources.getDrawable(Resources.java:814)
at android.content.res.Resources.getDrawable(Resources.java:767)
at com.nostra13.universalimageloader.core.DisplayImageOptions.getImageOnLoading(DisplayImageOptions.java:134)

内存泄漏是指某些本来应该被回收的内存,因为某些原因导致没法被回收,从而使得可用内存越来越少,当可用内存不足以进行分配的时候,系统可能会爆出 OutOfMemory 异常。

二、内存泄露有哪些危害

Android系统分配给单个应用程序的内存资源始终是有限的,而发生内存泄露后,用户不在使用组件也将持续占有RAM资源,从而导致两方面的问题。

  1. 运行性能的问题: Android在运行的时候,如果内存泄露导致其他组件可用的内存变少,一方面会使得GC的频率加剧,在发生GC的时候,所有进程都必须进行等待,GC的频率越多,从而用户越容易感知到卡顿。另一方面,内存变少,将可能使得系统会额外分配给你一些内存,而影响整个系统的运行状况。

  2. 运行崩溃问题: 一旦内存不足以分配某些内存,那么将会导致崩溃,这对于体验而言是致命的。我们在进行内存分析的时候,可以发现总有一些机型会出现OutOfMemory的崩溃栈,大抵都和内存泄露有关。

三、如何检测内存泄露

1)通过 Android Studio 提供的工具

Android Studio 内存分析

如图所示,Android Studio 提供的工具可以流式地显示当前内存使用情况,已经分配的部分和空闲的部分。当分配内存始终保持增加的时候,就需要考虑是否存在内存泄露的情况。

2)利用Android Studio进行准确定位

Android Logcat Memory 检测模块

我们在查看是否存在内存泄漏情况的时候,基于的基础单位往往是Activity,因而就可以想到一种思路,即通过在界面回退后,强制进行GC,然后判断是否还存在对该Activity的引用,这样就能得知是否存在泄漏

具体的的实施步骤如下:

  1. 客户端中打开相应的Activity,并执行可能触发内存泄漏的操作.

  2. 退出Activity界面,并点击Initiate GC(左起第二个按钮)

  3. 点击Dump Java Heap,等待一会后,这个时候可以看到Dump 出来的日志。

  4. 由于Android Profile文件不被 MAT 支持,因为我们需要执行转换操作。 ./hprof-conv path/file.hprof exitPath/heap-converted.hprof

  5. 在 MAT 中打开文件,并选择Leak Suspects Report,等待最后的结果。 内存使用情况

  6. 通过Activity的类名来过滤信息,在右键菜单里面,分别点击Merge Paths to Shortest GC Rootexclude all phantom/weak/soft etc. references, 排除被弱引用持有的情况。

  7. Fix them all!

3)通过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并进行相应的修改后,效果还是相当不错的。 Square 内存泄露占比

由于官方开源的LeakCanary只能在Debug版上使用,在Release上通过NullObject方式实现了一个空实现,来避免性能问题。如果想通过小流量的方式来批量地发现用户内存泄露的情况,那么就需要对源码进行整改,刚好我做了这么一件事情,有兴趣的人可以拿去使用。移除UI展示等逻辑后可在Release上使用的LeakCanary。有了用户相关的数据的泄露栈就能很好地处理各种泄露问题,使得应用良好稳定地运行。

四、参见内存泄露情况

  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 被外部长生命周期的对象引用而泄露。

参考文献

  1. Android 内存泄漏总结

  2. 使用Android studio分析内存泄露

  3. Google IO:Android内存管理主题演讲记录

  4. LeakCanary:检测所有的内存泄漏

— EOF —

Published: February 01 2016