Android 性能监控框架 Matrix(1)内存泄漏监控及原理介绍

3,458 阅读5分钟

ResourceCanary 介绍

Matrix 的内存泄漏监控是由 ResourceCanary 实现的,准确的说,ResourceCanary 只能实现 Activity 的内存泄漏检测,但在出现 Activity 内存泄漏时,可以选择 dump 一个堆转储文件,通过该文件,可以分析应用是否存在重复的 Bitmap。

使用

ResourceCanary 是基于 WeakReference 特性和 Square Haha 库开发的 Activity 泄漏和 Bitmap 重复创建检测工具,使用之前,需要进行如下配置:

Matrix.Builder builder = new Matrix.Builder(this);

// 用于在用户点击生成的问题通知时,通过这个 Intent 跳转到指定的 Activity
Intent intent = new Intent();
intent.setClassName(this.getPackageName(), "com.tencent.mm.ui.matrix.ManualDumpActivity");

ResourceConfig resourceConfig = new ResourceConfig.Builder()
        .dynamicConfig(new DynamicConfigImplDemo()) // 用于动态获取一些自定义的选项,不同 Plugin 有不同的选项
        .setAutoDumpHprofMode(ResourceConfig.DumpMode.AUTO_DUMP) // 自动生成 Hprof 文件
//        .setDetectDebuger(true) //matrix test code
        .setNotificationContentIntent(intent) // 问题通知
        .build();

builder.plugin(new ResourcePlugin(resourceConfig));

// 这个类可用于修复一些内存泄漏问题
ResourcePlugin.activityLeakFixer(this);

如果想要在具体的 Activity 中检测内存泄漏,那么获取 Plugin 并执行 start 方法(一般在 onCreate 方法中执行)即可:

Plugin plugin = Matrix.with().getPluginByClass(ResourcePlugin.class);
if (!plugin.isPluginStarted()) {
    plugin.start();
}

捕获到问题后,会上报信息如下:

{
    "tag": "memory",
    "type": 0,
    "process": "sample.tencent.matrix",
    "time": 1590396618440,
    "activity": "sample.tencent.matrix.resource.TestLeakActivity",
}

如果 DumpMode 为 AUTO_DUMP,还会生成一个压缩文件,里面包含一个堆转储文件和一个 result.info 文件,可以根据 result.info 文件发现具体是哪一个 Activity 泄漏了:

{
    "tag":"memory",
    "process":"com.tencent.mm",
    "resultZipPath":"/storage/emulated/0/Android/data/com.tencent.mm/cache/matrix_resource/dump_result_17400_20170713183615.zip",
    "activity":"com.tencent.mm.plugin.setting.ui.setting.SettingsUI",
}

配置

ResourcePlugin 执行之前,需要通过 ResourceConfig 配置,配置选项有:

public static final class Builder {
    private DumpMode mDefaultDumpHprofMode = DEFAULT_DUMP_HPROF_MODE;
    private IDynamicConfig dynamicConfig;
    private Intent mContentIntent;
    private boolean mDetectDebugger = false;
}

其中, ContentIntent 用于发送通知。

DumpMode 用于控制检测到问题后的行为,可选值有:

  1. NO_DUMP,是一个轻量级的模式,会回调 Plugin 的 onDetectIssue 方法,但只报告出现内存泄漏问题的 Activity 的名称
  2. SILENCE_DUMP,和 NO_DUMP 类似,但会回调 IActivityLeakCallback
  3. MANUAL_DUMP,用于生成一个通知,点击后跳转到对应的 Activity,Activity 由 ContentIntent 指定
  4. AUTO_DUMP,用于生成堆转储文件

IDynamicConfig 是一个接口,可用于动态获取一些自定义的选项值:

public interface IDynamicConfig {
    String get(String key, String defStr);
    int get(String key, int defInt);
    long get(String key, long defLong);
    boolean get(String key, boolean defBool);
    float get(String key, float defFloat);
}

和 Resource Canary 相关的选项有:

enum ExptEnum {
    //resource
    clicfg_matrix_resource_detect_interval_millis, // 后台线程轮询间隔
    clicfg_matrix_resource_detect_interval_millis_bg, // 应用不可见时的轮询间隔
    clicfg_matrix_resource_max_detect_times, // 重复检测多次后才认为出现了内存泄漏,避免误判
    clicfg_matrix_resource_dump_hprof_enable, // 没见代码有用到
}

实现该接口对应的方法,即可通过 ResourceConfig 获取上述选项的值:

public final class ResourceConfig {

    // 后台线程轮询间隔默认为 1min
    private static final long DEFAULT_DETECT_INTERVAL_MILLIS = TimeUnit.MINUTES.toMillis(1);
    // 应用不可见时,后台线程轮询间隔默认为 1min
    private static final long DEFAULT_DETECT_INTERVAL_MILLIS_BG = TimeUnit.MINUTES.toMillis(20);
    // 默认重复检测 10 次后,如果依然能获取到 Activity ,才认为出现了内存泄漏
    private static final int DEFAULT_MAX_REDETECT_TIMES = 10;

    public long getScanIntervalMillis() { ... }
    public long getBgScanIntervalMillis() { ... }
    public int getMaxRedetectTimes() { ... }
}

可以看到,默认情况下,Resource Canary 在应用可见(onForeground)时每隔 1 分钟检测一次,在应用不可见时每隔 20 分钟检测一次。对于同一个 Activity,在重复检测 10 次后,如果依然能通过弱引用获取,那么就认为出现了内存泄漏。

原理介绍

这部分内容摘抄自官方文档

监测阶段

在监测阶段,对于 4.0 之前的版本,由于没有 ActivityLifecycleCallbacks,而使用反射有性能问题,使用 BaseActivity 又存在侵入性的问题,因此,ResourceCanary 放弃了对 Android 4.0 之前的版本的支持,直接使用 ActivityLifecycleCallbacks 和弱引用来检测 Activity 的内存泄漏。

分析阶段

在分析阶段,由于对 Activity 的强引用链很可能不止一条,因此问题的关键在于找到最短的引用链。比如有如下引用关系:

那么,将 GC Root 和 Object 1 的引用关系解除即可。对于多条 GC Root 引用链的情况,多次检测即可,这样至少保证了每次执行 ResourceCanary 模块的耗时稳定在一个可预计的范围内,不至于在极端情况下耽误其他流程。

LeakCanary 已实现了上述算法,但 Matrix 改进了其中的一些问题:

  1. 增加一个一定能被回收的“哨兵”对象,用来确认系统确实进行了GC
  2. 直接通过 WeakReference.get() 来判断对象是否已被回收,避免因延迟导致误判
  3. 若发现某个 Activity 无法被回收,再重复判断 3 次(0.6.5 版本的代码默认是 10 次),以防在判断时该 Activity 被局部变量持有导致误判
  4. 对已判断为泄漏的 Activity,记录其类名,避免重复提示该 Activity 已泄漏

从 Hprof 文件获取所有冗余的 Bitmap 对象

对于这个问题,Android Moniter 已经有完整的实现,原理简单粗暴:把所有未被回收的 Bitmap 的数据 buffer 取出来,然后先对比所有长度为 1 的 buffer,找出相同的,记录所属的 Bitmap 对象;再对比所有长度为 2 的、长度为 3 的 buffer……直到把所有 buffer 都比对完,这样就记录了所有冗余的 Bitmap 对象,接着再套用 LeakCanary 获取引用链的逻辑把这些 Bitmap 对象到 GC Root 的最短强引用链找出来即可。

性能开销

在监测阶段,Resource Canary 的周期性轮询是在后台线程执行的,默认轮询间隔为 1min,以微信通讯录、朋友圈界面的帧率作为参考,接入后应用的平均帧率下降了 10 帧左右,开销并不明显。但 Dump Hprof 的开销较大,整个 App 会卡死约 5~15s。

分析部分放到了服务器环境中执行。实际使用时分析一个 200M 左右的 Hprof 平均需要 15s 左右的时间。此部分主要消耗在引用链分析上,因为需要广度优先遍历完 Hprof 中记录的全部对象。