美文网首页
Leakcanary浅析

Leakcanary浅析

作者: IN_BLACK_IN | 来源:发表于2022-02-11 16:05 被阅读0次

    本文主要分析内存泄漏的检测原理和如何实现生产环境应用,代码分析基于Leakcanary 1.6版本。

    如何检测内存泄漏

    要想搞懂如何检测内存泄漏,有一个基本知识点需要知道:

    引用类型 被垃圾回收时间 用途 用途
    强引用 从来不会 对象的一般状态 JVM停止运行时终止
    软引用 当内存不足时 对象缓存 内存不足时终止
    弱引用 正常垃圾回收时 对象缓存 垃圾回收后终止
    虚引用 正常垃圾回收时 跟踪对象的垃圾回收 垃圾回收后终止

    这里我们需要用到弱引用,因为弱引用符合我们检测的场景,就是当内存充足的时候触发的系统gc,弱引用对象也能被回收,这样有利于我们观察对象回收情况,并且弱引用在初始化的时候,有如下构造方法:

    WeakReference(T referent, ReferenceQueue<? super T> q)
    

    其中第二个参数是个队列,当对象被回收后会被放入该队列中,我们就可以通过在activity或者fragment onDestroy的时候,主动触发系统gc,然后查看队列中是否有该对象,如果没有,我们就认为发生了一次内存泄漏。我们看下leakcanry的具体实现:

    public static void install(Context context, RefWatcher refWatcher) {
      Application application = (Application) context.getApplicationContext();
      ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
    
      application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
    }
    
    private final Application.ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() {
      @Override public void onActivityDestroyed(Activity activity) {
        refWatcher.watch(activity);
      }
    };
    

    通过application的registerActivityLifecycleCallbacks监听所有activity的生命周期。接下去再看下在onActivityDestoryed中做了什么:

    public void watch(Object watchedReference, String referenceName) {
        .....
        final long watchStartNanoTime = System.nanoTime();
        // 随机生成一个key
        String key = UUID.randomUUID().toString();
        // key放入Set<String>
        retainedKeys.add(key);
        // new 一个包含key信息的弱引用对象
        final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);
    
        // 异步监测
        ensureGoneAsync(watchStartNanoTime, reference);
      }
    
    // 启动异步监测
    private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
      watchExecutor.execute(new Retryable() {
        @Override public Retryable.Result run() {
          return ensureGone(reference, watchStartNanoTime);
        }
      });
    }
    
    Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    
        // 更新retainedKeys,把存在queue中回收掉的key在retainedKeys去除掉
        removeWeaklyReachableReferences();
    
        // 如果当前应用的key已经不在retainedKeys中,说明引用在之前就被gc了
        if (gone(reference)) {
          return DONE;
        }
        // gc
        gcTrigger.runGc();
        // 再次更新retainedKeys
        removeWeaklyReachableReferences();
        // 这个时候引用的key还在retainedKeys,说明引用已经内存泄漏
        if (!gone(reference)) {
          ......dump操作
        }
        return DONE;
      }
    

    以上是acitivty的监测方法,接下去我们看下fragment的相关流程。

    // 1、install
    public static void install(Context context, RefWatcher refWatcher) {
        List<FragmentRefWatcher> fragmentRefWatchers = new ArrayList<>();
    
        if (SDK_INT >= O) {
          // os包下面的Fragment
          fragmentRefWatchers.add(new AndroidOFragmentRefWatcher(refWatcher));
        }
    
        try {
          // v4包下面的Fragment,这边用反射因为类放在另外一个module下
          Class<?> fragmentRefWatcherClass = Class.forName(SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME);
          Constructor<?> constructor = fragmentRefWatcherClass.getDeclaredConstructor(RefWatcher.class);
          FragmentRefWatcher supportFragmentRefWatcher = (FragmentRefWatcher) constructor.newInstance(refWatcher);
          fragmentRefWatchers.add(supportFragmentRefWatcher);
        } catch (Exception ignored) {
        }
    
        if (fragmentRefWatchers.size() == 0) {
          return;
        }
    
        Helper helper = new Helper(fragmentRefWatchers);
    
        Application application = (Application) context.getApplicationContext();
        // 添加Activity的生命周期回调
        application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks);
    }
    
    // 2、在activity的onActivityCreated生命周期调用的时候添加检测fragment逻辑
    private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() {
        @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        // 遍历os包和v4包下所有的watcher
        for (FragmentRefWatcher watcher : fragmentRefWatchers) {
          // 在onActivityCreated的时候给这个activity添加检测fragment逻辑
          watcher.watchFragments(activity);
        }
      }
    };
    
    // 3、给activity添加检测fragment的逻辑
    @Override public void watchFragments(Activity activity) {
      FragmentManager fragmentManager = activity.getFragmentManager();
      // 添加递归回调
      fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true);
    }
    
    // 4、 在fragment的生命周期中检测
    private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks = new FragmentManager.FragmentLifecycleCallbacks() {
      @Override
      public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
        // 在fragment的生命周期中检测
        refWatcher.watch(fragment);
      }
    };
    

    接下去具体的watch逻辑就和activity的一模一样,这里就不多赘述了。

    生产环境应用

    如果把leakcanary直接搬到线上的话有有以下几个问题需要解决:

    • dump出来的文件有100M左右甚至更大
    • Debug.dumpHprofData(heapDumpFile.getAbsolutePath());dump方法会冻结应用所有进程,持续数秒到数十秒的卡顿

    基于以上两个问题,我们可以参考下几家大厂的解决方案:

    微信开源的 Martix 中的Matrix-Android-ResourceCanary

    Martix 把分析过程需要用到的部分字符串数据和Bitmap的buffer数组外,其余的buffer数据都直接剔除,这样处理之后的Hprof文件通常能比原始文件小1/10以上。

    其他主要增加了一些误报的优化。

    美团的 Probe:Android线上OOM问题定位组件

    通过 native hook 了虚拟机写入 hprof 文件的操作,在这里先过滤了某些数据,最终生成的 hprof 文件就是裁剪后的了。在不考虑可能会有的兼容性问题,这个方案要比其他的方案要高效,因为它解决了 hprof 加载的内存问题。

    但是Probe是闭源的。

    快手的 KOOM——高性能线上内存监控方案

    快手最近开源的KOOM有这几个亮点:

    • 实现了Probe中提出 hook 生成 hprof 的 native 方法
    • 解决了 dumpHprofData 方法阻塞问题
    • 使用了 LeakCanary2 中使用的 hprof 分析库 shark

    对于 dumpHprofData 方法阻塞问题,koom的解决方式是fork新进程来执行dump操作,这样对父进程的正常执行没有影响。以下是耗时对比:

    fork dump normal dump
    耗时(ms) 139.5 19962.5

    综上对比,只有koom解决了线上应用的两大痛点。

    相关文章

      网友评论

          本文标题:Leakcanary浅析

          本文链接:https://www.haomeiwen.com/subject/sedwkrtx.html