Android性能优化之LeakCanary应用分析



一、引言

在项目开发过程中,特别是产品临上市前,性能优化是必须在做的;而性能优中,内存是一个不得不提的点;内存泄漏是一个永恒的话题,已成为内存优化的一个重量级的方向。在以前做C语言开的发的时候,每个malloc都需要人为去释放,否则就很容易出现死机及系统异常的问题,也经常出现野指针。当然在Android开发中,由于JVM会自动回收,不过执行GC的时候,就会造成系统卡顿;另外,有些使用不当同样会造成内存泄漏。在负责格力一代手机的时候,临上市前,都是一个Activity一个Activity去检查内存使用情况(重复打开某个Activity,看内存没有一直增加,有的话,则表示该Activity肯定有问题)。

“A small leak will sink a great ship.” - Benjamin Franklin
千里之堤, 毁于蚁穴。 -- 《韩非子·喻老》

二、LeakCanary介绍

LeakCanary是一款Square公司(NB的公司)提供的检测内存泄漏的工具,是一个集成方便,使用便捷,配置超级简单的框架,实现的功能却是极为强大的。在当前流行的内存泄漏分析工具中,选择一款好的对口的工具能让我们事半功倍。(其实自带的工具用好了一样强大:TraceView,GPU/CPU/内存/网络 Monitor,Hierarchy Viewer等等)

GitHub:Leakcanary

  • analyzer:核心类是分析HeapAnalyzer,作用是dump下来的堆内存,验证是否真的发生内存溢出
  • watcher:核心类是RefWatcher,作用是观察一个对象是否被释放。当发现一个对象应该被释放而没有被释放时,就会触发HeapDump
  • android:核心module,是LeakCanary的入口,核心类是LeakCanary,它会创建RefWatcher,去观察activity的引用。
  • android-no-op:跟android哪个module一样,里面也有LeakCanary这个类,但no-op代表不会做任何操作,用于线上版本的依赖。
  • sample:就是一个使用Demo

三、LeakCanary应用

3.1 依赖

首先在gradle中添加LeakCanary依赖(注意android studio3.0版本以后则需要使用debugImplementation这种依赖方式)

dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
    releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
    // Optional, if you use support library fragments:
    debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.1'
}

3.2 注入到项目中

接着在项目的Application类中,开始启动LeakCanary。


import com.squareup.leakcanary.LeakCanary; public class CBabyApplication extends Application { private static final String TAG = CBabyApplication.class.getSimpleName(); private static CBabyApplication mInstance; @Override public void onCreate() { super.onCreate(); LeakCanary.install(this); //LeakCanary logShow("onCreate"); mInstance = this; bindPlayService(); ...... } ...... }

这样就可以了,感觉非常方便!比加载友盟应用统计还简单。 在 debug build 中,如果检测到某个 activity 有内存泄露,LeakCanary 就是自动地显示一个通知。

3.3 实例测试

在上述环境下,看下leakcanary检测内存泄漏的情况,在打开一个Activity的时候,启动一个线程,代码如下:

        Toast.makeText(this, "Start", Toast.LENGTH_SHORT).show();
        new Thread(){
            @Override
            public void run() {
                super.run();
                SystemClock.sleep(10000);
            }
        }.start();

当打开这个Activity后,在10秒内退出该Activity,则leakcanary检测就提示警告了:



点击通知栏,可以看到泄露信息:



主要原因:当启动Activity后,线程也运行起来了,此时退出Activity,很明显上面的代码会产生内存泄露,因为Activity虽然销毁了,但是Thread类持有外部类的引用,产生了内存泄漏。

四、内存泄露

4.1 概念

垃圾回收器无法回收原本应该被回收的对象,这个对象就引发了内存泄漏。

4.2 内存泄漏的影响

  • 导致用户手机的内存越来越少
  • APP出现卡顿
  • 导致APP异常退出
  • APP被强行关闭

4.3 排查内存泄漏的步骤

  • 通过统计平台了解OOM或各异常情况
  • 安排重现问题
  • 在内存分析工具中反复查看,找到原本该回收掉的对象
  • 确定问题并修复

五、LeakCanary原理

5.1 主要类

LeakCanary框架的主要类:

类名 作用
DisplayLeakActivity 内存泄漏的查看页面
HeapAnalyzerService 内存堆分析服务, 为了保证App进程不会因此受影响变慢&内存溢出,运行于独立的进程
HeapAnalyzer 分析由RefWatcher生成的堆转储信息, 验证内存泄漏是否真实存在
HeapDump 堆转储信息类,存储堆转储的相关信息
ServiceHeapDumpListener 一个监听,包含了开启分析的方法
RefWatcher 核心类,检测不可达引用(可能地),当发现不可达引用时,它会触发
HeapDumper 堆信息转储
ActivityRefWatcher Activity引用检测,包含了Activity生命周期的监听执行与停止

5.2 Activity检测机制是什么

通过Application.registerActivityLifecycleCallbacks来绑定Activity生命周期的监听,从而监控所有Activity; 在Activity执行onDestroy时,开始检测当前页面是否存在内存泄漏,并分析结果。因此,如果想要在不同的地方都需要检测是否存在内存泄漏,需要手动添加。

5.3 内存泄漏检测机制是什么

KeyedWeakReference与ReferenceQueue联合使用,在弱引用关联的对象被回收后,会将引用添加到ReferenceQueue;清空后,可以根据是否继续含有该引用来判定是否被回收;判定回收,手动GC, 再次判定回收,采用双重判定来确保当前引用是否被回收的状态正确性;如果两次都未回收,则确定为泄漏对象。

5.4 内存泄漏轨迹的生成过程

从GCRoot开始逐步生成引用轨迹。

5.5 检测流程

LeakCanary will automatically show a notification when an activity memory leak is detected in your debug build.
当出现内存泄露的时候,LeakCanary将自动的发出一个内存泄露的通知。

  • 在onDestroy中创建了KeyedWeakReference的对象来监视需要监视的fragment,activity或其他一些对象等;进行弱引用
  • 在一个后台线程,会去查看是否有这个KeyedWeakReference对象,是否被cleared了;如果没有,则强制执行GC
  • 如果GC之后,还是没有clear这个reference,那么久保存一个heap堆栈的hrof文件;
  • 然后一个独立的HeapAnalyzerService服务进程将启动,并通过HeapAnalyzer集成HAHA另外一个开源项目的代码来对hrof进行自动的分析。
  • HeapAnalyzer将去定位KeyedWeakReference
  • HeapAnalyzer计算出这个引用到GC root一条最短的强引用链路,来确定是否存在leak
  • 最终再讲这个结果传递给DisplayLeakService并发出通知,显示泄露路径。

六、常见内存泄露类型

6.1 对context,activity的长时间的引用,最常见的就是static的类型引用

在看下mContext在RawContactEditorView中怎么使用的

CharSequence accountType = type.getDisplayLabel(mContext);
mAccountTypeTextView.setText(mContext.getString(R.string.external_profile_title,accountType));
accountType = mContext.getString(R.string.account_phone);

大多用于资源的获取,没必要静态,将static去掉,属于没有必要的静态引用。

其实使用getApplicationContext就够用了
所以这种情况的修改的方式是,将

mContactListFilterController = ContactListFilterController.getInstance(this);

改成

mContactListFilterController = ContactListFilterController.getInstance(getApplicationContext());
ContactListFilterController.getInstance(getActivity().getApplicationContext()), resultCode, data);

非常常见!!!没必要的activity应用,有些情况是用application Context就够了。

6.2 TextWatcher会引起activity内存泄露

EditText设置了addTextChangedListener的界面,要在onDestroy里调用removeTextChangedListener释放掉。

待续......

七、总结

漫无目的的看源码,很容易迷失在茫茫的Code Sea中,因此要学会善于使用工具!


参考: