首页 > 代码库 > LeakCanary核心原理源码浅析

LeakCanary核心原理源码浅析

网上大牛太多,不敢说分析,也不敢装成大大,所以只能是浅析…
那么今天这篇主要解决什么问题呢?其实就一个问题,LeakCanay.install(this)这个函数到底是怎么走的,用测试的话说就是数据流是怎么走的,用探索性测试的方法说就是:快递法。

LeakCanay项目的地址是:https://github.com/square/leakcanary

那么经过半天的下载之后,成功导入到as中。

直接从入口函数分析:LeakCanary.install(this),那么打算是分四个部分分析,第一部分分析入口,第二部分分析怎么判断泄漏,第三部分是对内存快照的处理,第四部分是总结和扩展,东西不多,鉴于我自己的水平只能看一点算一点了 :)

一、函数入口和常用类分析

  public static RefWatcher install(Application application) {
    return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
        .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
        .buildAndInstall();
  }

那么install静态方法里面包含了3个方法,这3个方法都是通过AndroidRefWatcherBuilder这个辅助类来配置相关信息的:

listenerServiceClass方法是和结果分析相关的服务绑定,绑定到DisplayLeakService.class这个类上面,这个类负责通知泄漏消息给你
excludedRefs方法是排除一些开发可以忽略的泄漏路径(一般是系统级别BUG),这些枚举在AndroidExcludedRefs这个类当中定义
buildAndInstall这才是重点方法,所以看下:

  public RefWatcher buildAndInstall() {
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
      LeakCanary.enableDisplayLeakActivity(context);
      ActivityRefWatcher.installOnIcsPlus((Application) context, refWatcher);
    }
    return refWatcher;
  }

上面没啥了,无非就是实例化RefWatcher对象,这个对象是用来判断泄漏对象的,马上会说
所以直接看ActivityRefWatcher.installOnIcsPlus方法:

  public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
    if (SDK_INT < ICE_CREAM_SANDWICH) {
      // If you need to support Android < ICS, override onDestroy() in your base activity.
      return;
    }
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
    activityRefWatcher.watchActivities();
  }

嗯,已经跳转到了ActivityRefWatcher这个类,这个类提供了下面这个api:

  public void watchActivities() {
    // Make sure you don‘t get installed twice.
    stopWatchingActivities();
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
  }

所以核心方法找到了,就是registerActivityLifecycleCallbacks这个方法,这是application提供的一个方法,用来统一管理所有activity的生命周期,LeakCanay是在onDestory()方法实现监控的:

  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new Application.ActivityLifecycleCallbacks() {
        @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        }

        @Override public void onActivityStarted(Activity activity) {
        }

        @Override public void onActivityResumed(Activity activity) {
        }

        @Override public void onActivityPaused(Activity activity) {
        }

        @Override public void onActivityStopped(Activity activity) {
        }

        @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
        }

        @Override public void onActivityDestroyed(Activity activity) {
          ActivityRefWatcher.this.onActivityDestroyed(activity);
        }
      };

之后把activity直接扔给RefWatcher类处理,所以第一部分完成

总结:
LeakCanay的自动检测泄漏是在4.0之上的,用了Application的ActivityLifecycleCallbacks回调接口中的onDestory()回调实现对项目中的activity进行对象监听,然后交给RefWatcher监听类进行进一步处理。

二、怎么判断泄漏

那么我们回顾一下传统的检测内存泄漏的方法:

1.GC后dump一份内存快照

2.频繁操作GC后再dump一份内存快照

3.重复并用mat工具对比对象增量

所以旧方法对劳动力和精确度都是一个考验啊,LeakCanay同样是用了内存快照,但是在精确对比之前还加入了弱引用算法,可以理解成是粗定位到精定位的一个过程。

接着上一部分,跳到RefWatcher.watch这个函数:

public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    final long watchStartNanoTime = System.nanoTime();
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);

    ensureGoneAsync(watchStartNanoTime, reference);
  }

上面做的事情就是把activity对象封装成带key值和带引用队列(ReferenceQueue)的KeyedWeakReference对象,key值是用来最终定位泄漏对象用的,第三部分会用到,引用队列是用来监控弱引用回收的,这个类是继承WeakReference类,所以封装完你会看到这个类的一些特点:

1.带key,通过UUID.randomUUID().toString(),是唯一key序列

2.包装成WeakReference并添加到ReferenceQueue

之后呢,封装完成就要开始分析了,核心方法是ensureGone,同时也是LeakCanay的核心所在,所以我注释了几个关键步骤:

  Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

    removeWeaklyReachableReferences();   //看下检测的弱引用回收了没

    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
    if (gone(reference)) {              //好,回收了,那么这个activity没有泄漏
      return DONE;
    }
    gcTrigger.runGc();                  //还是没有回收,手动GC一下
    removeWeaklyReachableReferences();   //在看看对象回收没有
    if (!gone(reference)) {                    //竟然还没回收,那么怀疑是内存泄漏了,所以下一步dump内存快照.hprof下来进一步精确分析
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

      File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));
    }
    return DONE;
  }

一个个看,removeWeaklyReachableReferences这个方法:

private void removeWeaklyReachableReferences() {
    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
    // reachable. This is before finalization or garbage collection has actually happened.
    KeyedWeakReference ref;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
      retainedKeys.remove(ref.key);
    }
  }

如果这个对象作为弱引用,被回收了,那么添加到引用队列(ReferenceQueue)当中去,所以这个函数.poll是出栈的意思,如果成功出栈了,那么说明你加入了引用队列,然后可以认为是已经被回收了的,然后retainedKeys这个是一个Set容器,在之前会加入生成的唯一key作为标识,这里如果这个对象回收了,那么就移除这个key值。

然后是gone函数,就是看retainedKeys容器有没有key,如果回收了,就不存在key了,那么就没有泄漏,否则就怀疑有泄漏。

然后后面的手动GC和检查都是一个类似二次确认的道理,还是没有回收,那么才会进入精确阶段,.hropf分析大法,这是第三部分内容。

总结:怀疑该对象有没有泄漏,需要经过两部走,RefWatcher封装了非常核心的方法,把对象封装成WackReference,并执行GC回收,如果存在强引用,那么会无法回收,就会认为有内存泄漏的可能了,然后才会快照分析发。

三、.hropf内存快照分析

那么在第二步根据弱引用有没有回收这个上已经是基本确定了这个对象有没有泄漏,那么下一部就是获取dumpheap以及对这个dumpheap进行analyze。

调用就是下面这几行,然而这几行的背后其实引用的又是另外一个项目haha,具体的开源地址为:https://github.com/square/haha

File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));

那么重点看heapdumpListener.analyze这个方法,还记得第一部分的入口处有一个listenerServiceClass,打开来看看:

  public AndroidRefWatcherBuilder listenerServiceClass(
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
    return heapDumpListener(new ServiceHeapDumpListener(context, listenerServiceClass));
  }

所以追着下去可以看到在ServiceHeapDumpListener这个类封装了analyze方法的具体实现,如下:

 @Override public void analyze(HeapDump heapDump) {
    checkNotNull(heapDump, "heapDump");
    HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
  }

之后便是开启一个服务专门用来处理heapdump,找出最短路径并展示通知,所以直接跳到:

AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);

这个最关键的方法里面。

  /**
   * Searches the heap dump for a {@link KeyedWeakReference} instance with the corresponding key,
   * and then computes the shortest strong reference path from that instance to the GC roots.
   */
  public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
    long analysisStartNanoTime = System.nanoTime();

    if (!heapDumpFile.exists()) {
      Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
      return failure(exception, since(analysisStartNanoTime));
    }

    try {
      HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
      HprofParser parser = new HprofParser(buffer);
      Snapshot snapshot = parser.parse();  //1.把.hprof转为Snapshot,这个Snapshot对象就包含了对象引用的所有路径
      deduplicateGcRoots(snapshot);   //2.精简gcroots??没懂?

      Instance leakingRef = findLeakingReference(referenceKey, snapshot);  //3.找出泄漏的对象

      // False alarm, weak reference was cleared in between key check and heap dump.
      if (leakingRef == null) {
        return noLeak(since(analysisStartNanoTime));
      }

      return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);   //4.找出泄漏对象的最短路径
    } catch (Throwable e) {
      return failure(e, since(analysisStartNanoTime));
    }
  }

上面的checkForLeak方法就是输入.hprof,输出分析结果,主要有以下几个步骤:

1.把.hprof转为Snapshot,这个Snapshot对象就包含了对象引用的所有路径

2.精简gcroots,把重复的路径删除,重新封装成不重复的路径的容器

3.找出泄漏的对象

4.找出泄漏对象的最短路径

重点分析放在第3、4步:

  private Instance findLeakingReference(String key, Snapshot snapshot) {
    ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
    List<String> keysFound = new ArrayList<>();
    for (Instance instance : refClass.getInstancesList()) {
      List<ClassInstance.FieldValue> values = classInstanceValues(instance);
      String keyCandidate = asString(fieldValue(values, "key"));
      if (keyCandidate.equals(key)) {
        return fieldValue(values, "referent");
      }
      keysFound.add(keyCandidate);
    }
    throw new IllegalStateException(
        "Could not find weak reference with key " + key + " in " + keysFound);
  }

那么上面这个方法是在snapshot快照中找到第一个弱引用(因为就是这个对象没有回收,泄漏了嘛),然后根据遍历这个对象的所有实例,如果key值和最开始定义封装的key值相同,那么返回这个泄漏对象,就是已近在快照中定位到了泄漏对象了。

 private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
      Instance leakingRef) {

    ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
    ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);

    // False alarm, no strong reference path to GC Roots.
    if (result.leakingNode == null) {
      return noLeak(since(analysisStartNanoTime));
    }

    LeakTrace leakTrace = buildLeakTrace(result.leakingNode);

    String className = leakingRef.getClassObj().getClassName();

    // Side effect: computes retained size.
    snapshot.computeDominators();

    Instance leakingInstance = result.leakingNode.instance;

    long retainedSize = leakingInstance.getTotalRetainedSize();

    retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);

    return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
        since(analysisStartNanoTime));
  }

最后一个步骤是根据上一部得到的泄漏对象找到最短路径,封装在ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);这句中,具体算法就不看了,因为看不懂,汗一个…

总结:在第三个分析步骤,解析hprof文件中,是先把这个文件封装成snapshot,然后根据弱引用和前面定义的key值,确定泄漏的对象,最后找到最短泄漏路径,作为结果反馈出来,那么如果在快照中找不到这个怀疑泄漏的对象,那么就认为这个对象其实并没有泄漏,因为已经回收了,如下的代码:

// False alarm, weak reference was cleared in between key check and heap dump.
      if (leakingRef == null) {
        return noLeak(since(analysisStartNanoTime));
      }

四、总结和扩展

那么LeakCanay的源码浅析就分析完了,从一开始觉得这工具高大上到分析完觉得就是这样(当前我是写不出来的)。最后总结一下全文,也方便作为分享的忽悠 :)

   LeakCanay的入口是在application的onCreate()方法中声明的,其实用的就是Application的ActivityLifecycleCallbacks回调接口监听所有activity的onDestory()的,在这个方法进行RefWatcher.watch对这个对象进行监控。

   具体是这样做的,封装成带key的弱引用对象,然后GC看弱引用对象有没有回收,没有回收的话就怀疑是泄漏了,需要二次确认。然后生成HPROF文件,分析这个快照文件有没有存在带这个key值的泄漏对象,如果没有,那么没有泄漏,否则找出最短路径,打印给我们,我们就能够找到这个泄漏对象了。

好了,一堆代码就总结成上面那句话了,那么扩展是啥呢?其实无非就是结果输出了,还记得一开始的初始化吗?把DisplayLeakService.class类作为参数传入,把DisplayLeakService是负责生成结果的,那么继承这玩意的父类不就可以自定义输出结果了吗,比如可以持续集成,然后出现泄漏后就发个邮件神马的,自动提单之类的,感谢开源~~~

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    LeakCanary核心原理源码浅析