性能优化(2.1)-LeakCanary原理分析

主目录见:Android高级进阶知识(这是总目录索引)
 性能优化很重要的一个环节就是检测有没有内存泄漏,以前我们内存泄漏会借助MAT,androidstudio Monitor(androidstudio 3.0改成Android profiler)等工具,检测过程会比较麻烦一点,而LeakCanary作为一个自动内存泄漏工具出现,应该说它的简单易用给我们省了好多工作量,提升了我们的代码质量。也许大家会说,java不是有自动垃圾回收机制吗?但是其实一些持有外部引用超过他应有的生命周期的话,那么这个时候垃圾回收机制是不会去回收的,这时候就会出现不可预期地内存暴走。

一.目标

今天的目标就是为了能明白LeakCanary大致的原理过程,然后大家能更放心使用,具体目标如下:
1.明白LeakCanary版本的差异;
2.LeakCanary的内存检测思路。

二.源码分析

具体的使用我们就不在这边说了,因为github上面都有,而且现在4.0版本以上的使用变得简单很多,我们来在代码中的使用:

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...
  }
}

很简单,但是其实在4.0以上其实才变得如此易用,这是为什么呢?这跟Application.ActivityLifecycleCallbacks这个方法有关(这个接口在Android 4.0才有),这个方法其实我们在前面的换肤框架实现解析(二)这篇文章有讲解过,这个方法可以监测到Activity的各个生命周期,然后在LeakCanary就可以在onActivityDestroyed方法中为所有的Activity调用refWatcher.watch(activity)

总体流程

在分析LeakCanary之前,我们先来明确一下总体流程,检测主要分为三个步骤:

  • 1.分析是否有可疑的泄漏对象,主要是通过弱引用机制来检测;
  • 2.如果第一步发现了可疑泄漏对象,那么就会dump内存快照,然后分析.hprof文件确定是否真的泄漏了。
  • 3.展示分析的结果。

1.分析可疑泄漏对象

我们知道,因为我们应用了Application.ActivityLifecycleCallbacks(Activity)方法,所以我们程序会在ActivityRefWatcher类中注册一个lifecycleCallbacks对象来监测Activity的生命周期,LeakCanary就是在onActivityDestroyed里面调用了refWatcher.watch(activity)方法,这里的refWatcher是在前面build()方法中初始化的,我们现在直接看RefWatcher的watch()方法:

 public void watch(Object watchedReference) {
    watch(watchedReference, "");
  }

这里的watchedReference就是我们的每个activity对象,我们看到这个方法又调用了内部两个参数的watch方法:

 public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    final long watchStartNanoTime = System.nanoTime();
//对一个引用产生一个唯一的Key
    String key = UUID.randomUUID().toString();
//放到key集合中
    retainedKeys.add(key);
//将要监测的对象添加一个弱引用
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);
//在子线程中分析这个弱引用
    ensureGoneAsync(watchStartNanoTime, reference);
  }

从上面的代码可以知道,其实就是给监测对象添加一个弱引用,然后使用ReferenceQueue来监测它的可达性的改变,其中key是一个唯一的uuid,而最后的ensureGoneAsync()是我们主要的分析方法了,我们来看看:

 private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }

我们看到程序中watchExecutor是个什么东西呢?LeakCanary为我们实现了AndroidWatchExecutor,这里面利用HandlerThread的机制,在子线程中来处理分析这个逻辑(如果这个地方不懂,推荐看HandlerThread源码分析),我们主要的分析方法是在ensureGone中,我们直接来看:

 Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
//删除所有已经在ReferenceQueue中的弱引用
    removeWeaklyReachableReferences();
//如果当前处于调试状态则返回
    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
//如果当前的对象只有弱引用了,那么说明不会泄露
    if (gone(reference)) {
      return DONE;
    }
//如果当前的对象还没有改变弱可达状态,则我们手动调用GC
    gcTrigger.runGc();
//再次删除,确认对象是不是已经在ReferenceQueue中
    removeWeaklyReachableReferences();
//如果当前对象还没有在ReferenceQueue,说明可能泄露了,则dump内存快照
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
//我们开始dump内存快照
      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;
  }

这里我们看到,我们为啥最后还有dump内存快照,然后进行分析.hprof文件呢,其实我们这里的GC只是建议虚拟机说进行一次内存回收,但是最终要不要进行内存回收是JVM说了算,如果这里建议没被通过的时候,那么我们的可达性就不会发生改变,我们就需要第二个步骤dump内存快照来分析。

2.dump内存快照

我们看到程序的最后调用了heapdumpListeneranalyze方法,那么这里的heapdumpListener是什么呢?这里要从LeakCanary类中的install()方法看起:

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

我们这里有个方法listenerServiceClass,这个方法我们跟进去看下:

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

从这里我们可以看到我们的heapdumpListener其实就是我们的ServiceHeapDumpListener类对象,所以我们看到这个类的analyze方法:

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

HeapAnalyzerService是个IntentService的子类(同样的,不懂IntentService的话推荐IntentService源码分析),所以我们的主要方法是在onHandIntent方法中分析的:

 @Override protected void onHandleIntent(Intent intent) {
    if (intent == null) {
      CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
      return;
    }
    String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
    HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);

    HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);

    AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
    AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
  }

我们看到程序new了一个HeapAnalyzer对象,这个类主要负责分析hprof文件的。然后程序会调用HeapAnalyzercheckForLeak方法:

  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 {
//加载hprof文件
      HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
      HprofParser parser = new HprofParser(buffer);
//解析
      Snapshot snapshot = parser.parse();
//精简gcroots,把重复的路径删除,重新封装成不重复的路径的容器
      deduplicateGcRoots(snapshot);
//找到泄漏对象的引用
      Instance leakingRef = findLeakingReference(referenceKey, snapshot);

      // False alarm, weak reference was cleared in between key check and heap dump.
      if (leakingRef == null) {
        return noLeak(since(analysisStartNanoTime));
      }
 //查找从这个对象的引用到GC ROOT的最短路径
      return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
    } catch (Throwable e) {
      return failure(e, since(analysisStartNanoTime));
    }
  }

上面的代码逻辑应该算是比较简单,具体细节大家也不需要硬抠,我们知道,我们上面的代码主要就是为了寻找到hprof文件中泄漏对象的引用路径(泄漏对象到gcroot的最短路径),如果能找到说明我们的对象确实泄漏了,最后会调用AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result)将发送出去。

3.泄漏结果展示

泄漏结果主要是在DisplayLeakService类中实现的,实现方法也不是很麻烦,大家可以自行查看,以为不在于主流程中,我们暂时就不讲了。

总结:我们知道我们android系统中可能自身存在一些泄漏情况,所以我们LeakCanary提供了AndroidExcludedRefs类来进行排除监测,这样我们不需要在乎Framework层本身的泄漏问题。现在LeakCanary的使用越来越多了,希望我们也能适当在代码中引入来检测自己写的代码是否有泄漏的风险,进而提升我们的代码质量,当然我们平时也要关注一些常见的内存泄漏情况,我们可以参考MAT内存泄漏分析(一)MAT内存泄漏分析(二),最后祝大家性能优化之路愉快。

    原文作者:ZJ_Rocky
    原文地址: https://www.jianshu.com/p/1fdcdd64c3a5
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞