性能优化(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内存泄漏分析(二),最后祝大家性能优化之路愉快。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容