Android 性能优化之内存泄漏检测以及内存优化(中)

字数 3975阅读 446

上篇博客我们写到了 Java/Android 内存的分配以及相关 GC 的详细分析,这篇博客我们会继续分析 Android 中内存泄漏的检测以及相关案例,和 Android 的内存优化相关内容。
  上篇:Android 性能优化之内存泄漏检测以及内存优化(上)
  中篇:Android 性能优化之内存泄漏检测以及内存优化(中)
  下篇:Android 性能优化之内存泄漏检测以及内存优化(下)
  转载请注明出处:http://blog.csdn.net/self_study/article/details/66969064
  对技术感兴趣的同鞋加群544645972一起交流。

Android 内存泄漏检测

�通过上篇博客我们了解了 Android JVM/ART 内存的相关知识和泄漏的原因,再来归类一下内存泄漏的源头,这里我们简单将其归为一下三类:<ul><li>自身编码引起</li>由项目开发人员自身的编码造成;<li>第三方代码引起</li>这里的第三方代码包含两类,第三方非开源的 SDK 和开源的第三方框架;<li>系统原因</li>由 Android 系统自身造成的泄漏,如像 WebView、InputMethodManager 等引起的问题,还有某些第三方 ROM 存在的问题。</ul>

Android 内存泄漏的定位,检测与修复

�内存泄漏不像闪退的 BUG,排查起来相对要困难一些,比较极端的情况是当你的应用 OOM 才发现存在内存泄漏问题,到了这种情况才去排查处理问题的话,对用户的影响就太大了,为此我们应该在编码阶段尽早地发现问题,而不是拖到上线之后去影响用户体验,下面总结一下常用内存泄漏的定位和检测工具:

Lint

Lint 是 Android studio 自带的静态代码分析工具,使用起来也很方便,选中需要扫描的 module,然后点击顶部菜单栏 Analyze -> Inspect Code ,选择需要扫描的地方即可:


这里写图片描述

      
这里写图片描述

这里写图片描述

最后在 Performance 里面有一项是 Handler reference leaks,里面列出来了可能由于内部 Handler 对象持有外部 Activity 引用导致内存泄漏的地方,这些地方都可以根据实际的使用场景去排查一下,因为毕竟不是每个内部 Handler 对象都会导致内存泄漏。Lint 还可以自定义扫描规则,使用姿势很多很强大,感兴趣的可以去了解一下,除了 Lint 之外,还有像 FindBugs、Checkstyle 等静态代码分析工具也是很不错的。

StrictMode

StrictMode 是 Android 系统提供的 API,在开发环境下引入可以更早的暴露发现问题给开发者,于开发阶段解决它,StrictMode 最常被使用来检测在主线程中进行读写磁盘或者网络操作等耗时任务,把这些耗时任务放置于主线程会造成主线程阻塞卡顿甚至可能出现 ANR ,官方例子:

 public void onCreate() {
     if (DEVELOPER_MODE) {
         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()   // or .detectAll() for all detectable problems
                 .penaltyLog()
                 .build());
         StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                 .detectLeakedSqlLiteObjects()
                 .detectLeakedClosableObjects()
                 .penaltyLog()
                 .penaltyDeath()
                 .build());
     }
     super.onCreate();
 }

把上面这段代码放在早期初始化的 Application、Activity 或者其他应用组件的 onCreate 函数里面来启用 StrictMode 功能,一般 StrictMode 只是在测试环境下启用,到了线上环境就不要开启这个功能。启用 StrictMode 之后,在 logcat 过滤日志的地方加上 StrictMode 的过滤 tag,如果发现一堆红色告警的 log,说明可能就出现了内存泄漏或者其他的相关问题了:


这里写图片描述

比如上面这个就是因为调用 registerReceiver 之后忘记调用 unRegisterReceiver 导致的 activity 泄漏,根据错误信息便可以定位和修复问题。

LeakCanary

� LeakCanary 是一个 Android 内存泄漏检测的神器,正确使用可以大大减少内存泄漏和 OOM 问题,地址:

https://github.com/square/leakcanary

集成 LeakCanary 也很简单,在 build.gradle 文件中加入:

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
 }

然后在 Application 类中添加下面代码:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}

上面两步做完之后就算是集成了 LeakCanary 了,非常简单方便,如果程序出现了内存泄漏会弹出 notification,点击这个 notification 就会进入到下面这个界面,或者集成 LeakCanary 之后在桌面会有一个 LeakCanary 的图标,点击进去是所有的内存泄漏列表,点击其中一项同样是进入到下面界面:

这里写图片描述

这个界面就会详细展示引用持有链,一目了然,对于问题的解决方便了很多,堪称神器,更多实用姿势可以看看 LeakCanary FAQ
  �还有一点需要提到的是,LeakCanary 在检测内存泄漏的时候会阻塞主界面,这是一点体验有点不爽的地方,但是这时候阻塞肯定是必要的,因为此时必须要挂起线程来获取当前堆的状态。然后也并不是每个 LeakCanary 提示的地方都有内存泄漏,这时候可能需要借助 MAT 等工具去具体分析。不过 LeakCanary 有一点非常好的地方是因为 Android 系统也会有一些内存泄漏,而 LeakCanary 对此则提供了一个 AndroidExcludedRefs 类来帮助我们排除这些问题。

Android Memory Monitor

�Memory Monitor 是 Android Studio 自带的一个监控内存使用状态的工具,入口如下所示:


这里写图片描述

在 Android Monitor 点开之后 logcat 的右侧就是 Monitor 工具,其中可以检测内存、CPU、网络等内容,我们这里只用到了 Memory Monitor 功能,点击红色箭头所指的区域,就会 dump 此时此刻的 Memory 信息,并且生成一个 .hprof 文件,dump 完成之后会自动打开这个文件的显示界面,如果没有打开,可以通过点击最左侧的 Capture 界面或者 Tool Window 里面的 Capture 进入 dump 的 .hprof 文件列表:


这里写图片描述

  �接着我们来分析一下这个生成的 .hprof 文件所展示的信息:
这里写图片描述

首先左上角的下拉框,可以选择 App Heap、Image Heap 和 Zygote Heap,对应的就是上篇博客讲到的 Allocation Space,Image Space 和 Zygote Space,我们这里选择 Allocation Space,然后第二个选择 PackageTreeView 这一项,展开之后就能看见一个树形结构了,然后继续展开我们应用包名的对应对象,就可以很清晰的看到有多少个 Activity 对象了,上面那两栏展示的信息按照从左到右的顺序,定义如下所示:

Column Description
Class Name 占有这块内存的类名
Total Count 未被处理的数量
Heap Count 在上面选择的指定 heap 中的数量
Sizeof 这个对象的大小,如果在变化中,就显示 0
Shallow Size 在当前这个 heap 中的所有该对象的总数
Retained Size 这个类的所有对象占有的总内存大小
Instance 这个类的指定对象
Reference Tree 指向这个选中对象的引用,还有指向这个引用的引用
Depth 从 GC Root 到该对象的引用链路的最短步数
Shallow Size 这个引用的大小
Dominating Size 这个引用占有的内存大小

然后可以点击展开右侧的 Analyzer Tasks 项,勾选上需要检测的任务,然后系统就会给你分析出结果:

这里写图片描述

从分析的结果可以看到泄漏的 Activity 有两个,非常直观,然后点开其中一个,观察下面的 ReferenceTree 选项:
这里写图片描述

可以看到 Thread 对象持有了 SecondActivity 对象的引用,也就是 GC Root 持有了该 Activity 的引用,导致这个 Activity 无法回收,问题的根源我们就发现了,接下来去处理它就好了。
  �关于更多 Android Memory Monitor 的使用可以去看看这个官方文档:HPROF Viewer and Analyzer

MAT

MAT(Memory Analyzer Tools)是一个 Eclipse 插件,它是一个快速、功能丰富的 JAVA heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗,MAT 插件的下载地址:Eclipse Memory Analyzer Open Source Project,上面通过 Android studio 生成的 .hprof 文件因为格式稍有不同,所以需要经过一个简单的转换,然后就可以通过 MAT 去打开了:

这里写图片描述

通过 MAT 去打开转换之后的这个文件:
这里写图片描述

用的最多的就是 Histogram 功能,点击 Actions 下的 Histogram 项就可以得到 Histogram 结果:
这里写图片描述

我们可以在左上角写入一个正则表达式,然后就可以对所有的 Class Name 进行筛选了,很方便,顶栏展示的信息 "Objects" 代表该类名对象的数量,剩下的 "Shallow Heap" 和 "Retained Heap" 则和 Android Memory Monitor 类似。咱们接着点击 SecondActivity,然后右键:
这里写图片描述

在弹出来的菜单中选择 List objects->with incoming references 将该类的实例全部列出来:
这里写图片描述

通过这个列表我们可以看到 SecondActivity@0x12faa900 这个对象被一个 this$00x12c65140 的匿名内部类对象持有,然后展开这一项,发现这个对象是一个 handler 对象:
这里写图片描述

快速定位找到这个对象没有被释放的原因,可以右键 Path to GC Roots->exclude all phantom/weak/soft etc. references 来显示出这个对象到 GC Root 的引用链,因为强引用才会导致对象无法释放,所以这里我们要排除其他三种引用:
这里写图片描述

这么处理之后的结果就很明显了:
这里写图片描述

一个非常明显的强引用持有链,GC Root 我们前面的博客中说到包含了线程,所以这里的 Thread 对象 GC Root 持有了 SecondActivity 的引用,导致该 Activity 无法被释放。
  �MAT 还有一个功能就是能够对比两个 .hprof 文件,将两个文件都添加到 Compare Basket 里面:
这里写图片描述

添加进去之后点击右上角的 ! 按钮,然后就会生成两个文件的对比:
这里写图片描述

同样适用正则表达式将需要的类筛选出来:
这里写图片描述

结果也很明显,退出 Activity 之后该 Activity 对象未被回收,仍然在内存中,或者可以调整对比选项让对比结果更加明显:
这里写图片描述

也可以对比两个对象集合,方法与此类似,都是将两个 Dump 结果中的对象集合添加到 Compare Basket 中去对比,找出差异后用 Histogram 查询的方法找出 GC Root,定位到具体的某个对象上。

adb shell && Memory Usage

可以通过命令 adb shell dumpsys meminfo [package name] 来将指定 package name 的内存信息打印出来,这种模式可以非常直观地看到 Activity 未释放导致的内存泄漏:

这里写图片描述
这里写图片描述

或者也可以通过 Android studio 的 Memory Usage 功能进行查看,最后的结果是一样的:
这里写图片描述

Allocation Tracker

Android studio 还自带一个 Allocation Tracker 工具,功能和 DDMS 中的基本差不多,这个工具可以监控一段时间之内的内存分配:

这里写图片描述

在内存图中点击途中标红的部分,启动追踪,再次点击就是停止追踪,随后自动生成一个 .alloc 文件,这个文件就记录了这次追踪到的所有数据,然后会在右上角打开一个数据面板:
这里写图片描述

这个工具详细的介绍可以看看这个博客:Android性能专项测试之Allocation Tracker(Android Studio)

常见的内存泄漏案例

�我们来看看常见的导致内存泄漏的案例:

静态变量造成的内存泄漏

�由于静态变量的生命周期和应用一样长,所以如果静态变量持有 Activity 或者 Activity 中 View 对象的应用,就会导致该静态变量一直直接或者间接持有 Activity 的引用,导致该 Activity 无法释放,从而引发内存泄漏,不过需要注意的是在大多数这种情况下由于静态变量只是持有了一个 Activity 的引用,所以导致的结果只是一个 Activity 对象未能在退出之后释放,这种问题一般不会导致 OOM 问题,只能通过上面介绍过的几种工具在开发中去观察发现。
  这种问题的解决思路很简单,就是不让静态变量直接或者间接持有 Activity 的强引用,可以将其修改为 soft reference 或者 weak reference 等等之类的,或者如果可以的话将 Activity Context 更换为 Application Context,这样就能保证生命周期一致不会导致内存泄漏的问题了。

内部类持有外部类引用

我们上面的 demo 中模拟的就是内部类对象持有外部类对象的引用导致外部类对象无法释放的问题,在 Java 中非静态内部类和匿名内部类会持有他们所属外部类对象的引用,如果这个非静态内部类对象或者匿名内部类对象被一个耗时的线程(或者其他 GC Root)直接或者间接的引用,甚至这些内部类对象本身就在做一些耗时操作,这样就会导致这个内部类对象直接或者间接无法释放,内部类对象无法释放,外部类的对象也就无法释放造成内存泄漏,而且如果无法释放的对象积累起来就会造成 OOM,示例代码如下所示:

public class SecondActivity extends AppCompatActivity{
    private Handler handler;
    private Bitmap bitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pic);//decode 一个大图来模拟内存无法释放导致的崩溃
        findViewById(R.id.btn_second).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                finish();
            }
        });

        handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);

            }
        };
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                handler.sendEmptyMessage(0);
            }
        }).start();
    }
}

这个问题的解决方法可以根据实际情况进行选择:<ul><li>将非静态内部类或者匿名内部类修改为静态内部类,比如 Handler 修改为静态内部类,然后让 Handler 持有外部 Activity 的一个 Weak Reference 或者 Soft Reference;</li><li>在 Activity 页面销毁的时候将耗时任务停止,这样就能保证 GC Root 不会间接持有 Activity 的引用,也就不会导致内存泄漏;</li></ul>

错误使用 Activity Context

这个很好理解,在一个错误的地方使用 Activity Context,造成 Activity Context 被静态变量长时间引用导致无法释放而引发的内存泄漏,这个问题的处理方式也很简单,如果可以的话修改为 Application Context 或者将强引用变成其他引用。

资源对象没关闭造成的内存泄漏

资源性对象比如(Cursor,File 文件等)往往都用了一些缓冲,我们在不使用的时候应该及时关闭它们,以便它们的缓冲对象被及时回收,这些缓冲不仅存在于 java 虚拟机内,还存在于 java 虚拟机外,如果我们仅仅是把它的引用设置为 null 而不关闭它们,往往会造成内存泄漏。但是有些资源性对象,比如 SQLiteCursor(在析构函数 finalize(),如果我们没有关闭它,它自己会调 close() 关闭),如果我们没有关闭它系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的 close() 函数,将其关闭掉,然后再置为 null,在我们的程序退出时一定要确保我们的资源性对象已经关闭。
  程序中经常会进行查询数据库的操作,但是经常会有使用完毕 Cursor 后没有关闭的情况,如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会出现内存问题,这样就会给以后的测试和问题排查带来困难和风险,示例代码:

Cursor cursor = getContentResolver().query(uri...); 
if (cursor.moveToNext()) { 
... ... 
} 

更正代码:

Cursor cursor = null;
try {
    cursor = getContentResolver().query(uri...);
    if (cursor != null && cursor.moveToNext()) {
        ... ...
    }
} finally {
    if (cursor != null) {
        try {
            cursor.close();
        } catch (Exception e) {
            //ignore this
        }
    }
}

集合中对象没清理造成的内存泄漏

在实际开发过程中难免会有把对象添加到集合容器(比如 ArrayList)中的需求,如果在一个对象使用结束之后未将该对象从该容器中移除掉,就会造成该对象不能被正确回收,从而造成内存泄漏,解决办法当然就是在使用完之后将该对象从容器中移除。

WebView造成的内存泄露

具体的可以看看我的这篇博客:android WebView详解,常见漏洞详解和安全源码(下)

未取消注册导致的内存泄漏

一些 Android 程序可能引用我们的 Android 程序的对象(比如注册机制),即使我们的 Android 程序已经结束了,但是别的应用程序仍然还持有对我们 Android 程序某个对象的引用,这样也会造成内存不能被回收,比如调用 registerReceiver 后未调用unregisterReceiver。假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息,则可以在 LockScreen 中定义一个 PhoneStateListener 的对象,同时将它注册到 TelephonyManager 服务中,对于 LockScreen 对象,当需要显示锁屏界面的时候就会创建一个 LockScreen 对象,而当锁屏界面消失的时候 LockScreen 对象就会被释放掉,但是如果在释放 LockScreen 对象的时候忘记取消我们之前注册的 PhoneStateListener 对象,则会间接导致 LockScreen 无法被回收,如果不断的使锁屏界面显示和消失,则最终会由于大量的 LockScreen 对象没有办法被回收而引起 OOM,虽然有些系统程序本身好像是可以自动取消注册的(当然不及时),但是我们还是应该在程序结束时明确的取消注册。

因为内存碎片导致分配内存不足

还有一种情况是因为频繁的内存分配和释放,导致内存区域里面存在很多碎片,当这些碎片足够多,new 一个大对象的时候,所有的碎片中没有一个碎片足够大以分配给这个对象,但是所有的碎片空间加起来又是足够的时候,就会出现 OOM,而且这种 OOM 从某种意义上讲,是完全能够避免的。
  由于产生内存碎片的场景很多,从 Memory Monitor 来看,下面场景的内存抖动是很容易产生内存碎片的:


这里写图片描述

最常见产生内存抖动的例子就是在 ListView 的 getView 方法中未复用 convertView 导致 View 的频繁创建和释放,针对这个问题的处理方式那当然就是复用 convertView;或者是 String 拼接创建大量小的对象(比如在一些频繁调用的地方打字符串拼接的 log 的时候);如果是其他的问题,就需要通过 Memory Monitor 去观察内存的实时分配释放情况,找到内存抖动的地方修复它,或者如果当出现下面这种情况下的 OOM 时,也是由于内存碎片导致无法分配内存:


这里写图片描述

出现上面这种类型的 Crash 时就要去分析应用里面是不是存在大量分配释放对象的地方了。

Android 内存优化

内存优化请看下篇:Android 性能优化之内存泄漏检测以及内存优化(下)

引用

http://blog.csdn.net/luoshengyang/article/details/42555483
http://blog.csdn.net/luoshengyang/article/details/41688319
http://blog.csdn.net/luoshengyang/article/details/42492621
http://blog.csdn.net/luoshengyang/article/details/41338251
http://blog.csdn.net/luoshengyang/article/details/41581063
https://mp.weixin.qq.com/s?__biz=MzA4MzEwOTkyMQ==&mid=2667377215&idx=1&sn=26e3e9ec5f4cf3e7ed1e90a0790cc071&chksm=84f32371b384aa67166a3ff60e3f8ffdfbeed17b4c8b46b538d5a3eec524c9d0bcac33951a1a&scene=0&key=c2240201df732cf062d22d3cf95164740442d817864520af90bb0e71fa51102f2e91475a4f597ec20653c59d305c8a3e518d3f575d419dfcf8fb63a776e0d9fa6d3a9a6a52e84fedf3f467fe4af1ba8b&ascene=0&uin=Mjg5MDI3NjQ2Mg%3D%3D&devicetype=iMac+MacBookPro11%2C4+OSX+OSX+10.12.3+build(16D32)&version=12010310&nettype=WIFI&fontScale=100&pass_ticket=Upl17Ws6QQsmZSia%2F%2B0xkZs9DYxAJBQicqh8rcaxYUjcu3ztlJUPxYrQKML%2BUtuf
http://geek.csdn.net/news/detail/127226
http://www.jianshu.com/p/216b03c22bb8
https://zhuanlan.zhihu.com/p/25213586
https://joyrun.github.io/2016/08/08/AndroidMemoryLeak/
http://www.cnblogs.com/larack/p/6071209.html
https://source.android.com/devices/tech/dalvik/gc-debug.html
http://blog.csdn.net/high2011/article/details/53138202
http://gityuan.com/2015/10/03/Android-GC/
http://www.ayqy.net/blog/android-gc-log%E8%A7%A3%E8%AF%BB/
https://developer.android.com/studio/profile/investigate-ram.html
https://zhuanlan.zhihu.com/p/26043999

推荐阅读更多精彩内容