LeakCanary使用及原理

LeakCanary Android 和 Java 内存泄露检测。

demo

一个非常简单的 LeakCanary demo: https://github.com/liaohuqiu/leakcanary-demo

开始使用

在 build.gradle 中加入引用,不同的编译使用不同的引用:

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
 }

这样,就万事俱备了! 在 debug build 中,如果检测到某个 activity 有内存泄露,LeakCanary 就是自动地显示一个通知。

为什么需要使用 LeakCanary?

问得好,看这个文章LeakCanary: 让内存泄露无所遁形

如何使用

使用 RefWatcher 监控那些本该被回收的对象。


RefWatcher refWatcher = {...};

// 监控
refWatcher.watch(schrodingerCat);

LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。

public class ExampleApplication extends Application {

  public static RefWatcher getRefWatcher(Context context) {
    ExampleApplication application = (ExampleApplication) context.getApplicationContext();
    return application.refWatcher;
  }

  private RefWatcher refWatcher;

  @Override public void onCreate() {
    super.onCreate();
    refWatcher = LeakCanary.install(this);
  }
}

使用 RefWatcher 监控 Fragment:

public abstract class BaseFragment extends Fragment {

  @Override public void onDestroy() {
    super.onDestroy();
    RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
    refWatcher.watch(this);
  }
}

如何复制 leak trace?

在 Logcat 中,你可以看到类似这样的 leak trace:

In com.example.leakcanary:1.0:1 com.example.leakcanary.MainActivity has leaked:

* GC ROOT thread java.lang.Thread. (named 'AsyncTask #1')
* references com.example.leakcanary.MainActivity$3.this$0 (anonymous class extends android.os.AsyncTask)
* leaks com.example.leakcanary.MainActivity instance

* Reference Key: e71f3bf5-d786-4145-8539-584afaecad1d
* Device: Genymotion generic Google Nexus 6 - 5.1.0 - API 22 - 1440x2560 vbox86p
* Android Version: 5.1 API: 22
* Durations: watch=5086ms, gc=110ms, heap dump=435ms, analysis=2086ms

你甚至可以通过分享按钮把这些东西分享出去。

SDK 导致的内存泄露

随着时间的推移,很多SDK 和厂商 ROM 中的内存泄露问题已经被尽快修复了。但是,当这样的问题发生时,一般的开发者能做的事情很有限。

LeakCanary 有一个已知问题的忽略列表,AndroidExcludedRefs.java,如果你发现了一个新的问题,请提一个 issue 并附上 leak trace, reference key, 机器型号和 SDK 版本。如果可以附带上 dump 文件的 链接那就再好不过了。

对于最新发布的 Android,这点尤其重要。你有机会在帮助在早期发现新的内存泄露,这对整个 Android 社区都有极大的益处。

开发版本的 Snapshots 包在这里: Sonatype's snapshots repository。

leak trace 之外

有时,leak trace 不够,你需要通过 MAT 或者 YourKit 深挖 dump 文件。

通过以下方法,你能找到问题所在:

1.查找所有的 com.squareup.leakcanary.KeyedWeakReference 实例。
2.检查 key 字段
3.Find the KeyedWeakReference that has a key field equal to the reference key reported by LeakCanary.
4.找到 key 和 和 logcat 输出的 key 值一样的 KeyedWeakReference。
5.referent 字段对应的就是泄露的对象。
6.剩下的,就是动手修复了。最好是检查到 GC root 的最短强引用路径开始。

自定义

UI 样式

DisplayLeakActivity 有一个默认的图标和标签,你只要在你自己的 APP 资源中,替换以下资源就可。

res/
  drawable-hdpi/
    __leak_canary_icon.png
  drawable-mdpi/
    __leak_canary_icon.png
  drawable-xhdpi/
    __leak_canary_icon.png
  drawable-xxhdpi/
    __leak_canary_icon.png
  drawable-xxxhdpi/
    __leak_canary_icon.png


  MyLeaks

保存 leak trace

DisplayLeakActivity saves up to 7 heap dumps & leak traces in the app directory. You can change that number by providing R.integer.__leak_canary_max_stored_leaks in your app:

在 APP 的目录中,DisplayLeakActivity 保存了 7 个 dump 文件和 leak trace。你可以在你的 APP 中,定义 R.integer.__leak_canary_max_stored_leaks 来覆盖类库的默认值。



  20

上传 leak trace 到服务器

你可以改变处理完成的默认行为,将 leak trace 和 heap dump 上传到你的服务器以便统计分析。

你可以改变处理完成的默认行为,将 leak trace 和 heap dump 上传到你的服务器以便统计分析。

public class LeakUploadService extends DisplayLeakService {
  @Override
  protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
    if (!result.leakFound || result.excludedLeak) {
      return;
    }
    myServer.uploadLeakBlocking(heapDump.heapDumpFile, leakInfo);
  }
}

请确认 release 版本 使用 RefWatcher.DISABLED:

public class ExampleApplication extends Application {

  public static RefWatcher getRefWatcher(Context context) {
    ExampleApplication application = (ExampleApplication) context.getApplicationContext();
    return application.refWatcher;
  }

  private RefWatcher refWatcher;

  @Override public void onCreate() {
    super.onCreate();
    refWatcher = installLeakCanary();
  }

  protected RefWatcher installLeakCanary() {
    return RefWatcher.DISABLED;
  }
}

自定义 RefWatcher:

public class DebugExampleApplication extends ExampleApplication {
  protected RefWatcher installLeakCanary() {
    return LeakCanary.install(app, LeakUploadService.class);
  }
}

别忘了注册 service:



  
    
  


[图片上传失败...(image-305dc5-1522739113278)]

工作机制原理

  1. RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。

  2. 然后在后台线程检查引用是否被清除,如果没有,调用GC。

  3. 如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。

  4. 在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。

  5. 得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。

  6. HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。

  7. 引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。

LeakCanary使用及原理_第1张图片
image

但事实上一切并没那么简单,LeakCanary的设计者在实现的时候实际上为我们考虑了很多细节。可以通过源码分析来走一遍一次内存泄漏检查的流程。在一个Activity生命周期结束调用oonDestroy方法的时候会触发LeakCanary进行一次内存泄漏检查,LeakCanary开始进行检查的入口函数实际上是RefWatcher类的,watch方法,其源码如下:

public void watch(Object watchedReference, String referenceName) {

  ...

  String key = UUID.randomUUID().toString();
  retainedKeys.add(key);
  final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);

  watchExecutor.execute(new Runnable() {
      @Override
      public void run() {
          ensureGone(reference, watchStartNanoTime);
      }
  });
} 这个函数做的主要工作就是为需要进行泄漏检查的Activity创建一个带有唯一key标志的弱引用,并将这个弱引用key保存至retainedKeys中,然后将后面的工作交给watchExecutor来执行。这个watchExecutor在LeakCanary中是AndroidWatchExecutor的实例,调用它的execute方法实际上就是向主线程的消息队列中插入了一个IdleHandler消息,这个消息只有在对应的消息队列为空的时候才会去执行,因此RefWatcher的watch方法就保证了在主线程空闲的时候才会去执行ensureGone方法,防止因为内存泄漏检查任务而严重影响应用的正常执行。ensureGone的主要源码如下:

void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    ...

    removeWeaklyReachableReferences();
    if (gone(reference) || debuggerControl.isDebuggerAttached()) {
        return;
    }
    gcTrigger.runGc();      // 手动执行一次gc
    removeWeaklyReachableReferences();
    if (!gone(reference)) {

        long startDumpHeap = System.nanoTime();
        long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

        File heapDumpFile = heapDumper.dumpHeap();
        if (heapDumpFile == null) {
            // Could not dump the heap, abort.
            Log.d(TAG, "Could not dump the heap, abort.");
            return;
        }
        long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

        heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs,
                watchDurationMs, gcDurationMs, heapDumpDurationMs));
    }
  } 

因为这个方法是在主线程中执行的,因此一般执行到这个方法中的时候之前被destroy的那个Activity的资源应该被JVM回收了,因此这个方法首先调用removeWeaklyReachableReferences方法来将引用队列中存在的弱引用从retainedKeys中删除掉,这样,retainedKeys中保留的就是还没有被回收对象的弱引用key。之后再用gone方法来判断我们需要检查的Activity的弱引用是否在retainedKeys中,如果没有,说明这个Activity对象已经被回收,检查结束。否则,LeakCanary主动触发一次gc,再进行以上两个步骤,如果发现这个Activity还没有被回收,就认为这个Activity很有可能泄漏了,并dump出当前的内存文件供之后进行分析。
之后的工作就是对内存文件进行分析,由于这个过程比较耗时,因此最终会把这个工作交给运行在另外一个进程中的HeapAnalyzerService来执行。HeapAnalyzerService通过调用HeapAnalyzer的checkForLeak方法进行内存分析,其源码如下:

public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {

    ...

    ISnapshot snapshot = null;
    try {
      snapshot = openSnapshot(heapDumpFile);  

      IObject leakingRef = findLeakingReference(referenceKey, snapshot);

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

      String className = leakingRef.getClazz().getName();

      AnalysisResult result =
          findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, true);

      if (!result.leakFound) {
        result = findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, false);
      }

      return result;
    } catch (SnapshotException e) {
      return failure(e, since(analysisStartNanoTime));
    } finally {
      cleanup(heapDumpFile, snapshot);
    }
}

这个方法进行的第一步就是利用HAHA将之前dump出来的内存文件解析成Snapshot对象,其中调用到的方法包括SnapshotFactory的parse和HprofIndexBuilder的fill方法。解析得到的Snapshot对象直观上和我们使用MAT进行内存分析时候罗列出内存中各个对象的结构很相似,它通过对象之间的引用链关系构成了一棵树,我们可以在这个树种查询到各个对象的信息,包括它的Class对象信息、内存地址、持有的引用及被持有的引用关系等。到了这一阶段,HAHA的任务就算完成,之后LeakCanary就需要在Snapshot中找到一条有效的到被泄漏对象之间的引用路径。首先它调用findLeakTrace方法来从Snapshot中找到被泄漏对象,源码如下:

private IObject findLeakingReference(String key, ISnapshot snapshot) throws SnapshotException {

    Collection refClasses =
        snapshot.getClassesByName(KeyedWeakReference.class.getName(), false);

    if (refClasses.size() != 1) {
      throw new IllegalStateException(
          "Expecting one class for " + KeyedWeakReference.class.getName() + " in " + refClasses);
    }

    IClass refClass = refClasses.iterator().next();

    int[] weakRefInstanceIds = refClass.getObjectIds();

    for (int weakRefInstanceId : weakRefInstanceIds) {
      IObject weakRef = snapshot.getObject(weakRefInstanceId);
      String keyCandidate =
          PrettyPrinter.objectAsString((IObject) weakRef.resolveValue("key"), 100);  
      if (keyCandidate.equals(key)) {  // 匹配key
        return (IObject) weakRef.resolveValue("referent");  // 定位泄漏对象
      }
    }
    throw new IllegalStateException("Could not find weak reference with key " + key);
  }

为了能够准确找到被泄漏对象,LeakCanary通过被泄漏对象的弱引用来在Snapshot中定位它。因为,如果一个对象被泄漏,一定也可以在内存中找到这个对象的弱引用,再通过弱引用对象的referent就可以直接定位被泄漏对象。下一步的工作就是找到一条有效的到被泄漏对象的最短的引用,这通过findLeakTrace来实现,实际上寻找最短路径的逻辑主要是封装在PathsFromGCRootsComputerImpl这个类的getNextShortestPath和processCurrentReferrefs这两个方法当中,其源码如下:

public int[] getNextShortestPath() throws SnapshotException {
      switch (state) {
        case 0: // INITIAL
        {

          ...
        }
        case 1: // FINAL
          return null;

        case 2: // PROCESSING GC ROOT
        {
          ...
        }
        case 3: // NORMAL PROCESSING
        {
          int[] res;

          // finish processing the current entry
          if (currentReferrers != null) {
            res = processCurrentReferrefs(lastReadReferrer + 1);
            if (res != null) return res;
          }

          // Continue with the FIFO
          while (fifo.size() > 0) {
            currentPath = fifo.getFirst();
            fifo.removeFirst();
            currentId = currentPath.getIndex();
            currentReferrers = inboundIndex.get(currentId);

            if (currentReferrers != null) {
              res = processCurrentReferrefs(0);
              if (res != null) return res;
            }
          }
          return null;
        }

        default:
          ...
      }
    }


    private int[] processCurrentReferrefs(int fromIndex) throws SnapshotException {
      GCRootInfo[] rootInfo = null;
      for (int i = fromIndex; i < currentReferrers.length; i++) {
        ...
      }
      for (int referrer : currentReferrers) {
        if (referrer >= 0 && !visited.get(referrer) && !roots.containsKey(referrer)) {
          if (excludeMap == null) {
            fifo.add(new Path(referrer, currentPath));
            visited.set(referrer);
          } else {
            if (!refersOnlyThroughExcluded(referrer, currentId)) {
              fifo.add(new Path(referrer, currentPath));
              visited.set(referrer);
            }
          }
        }
      }
      return null;
    }
  }

为了是逻辑更清晰,在这里省略了对GCRoot的处理。这个类将整个内存映像信息抽象成了一个以GCRoot为根的树,getNextShortestPath的状态3是对一般节点的处理,由于之前已经定位了被泄漏的对象在这棵树中的位置,为了找到一条到GCRoot最短的路径,PathsFromGCRootsComputerImpl采用的方法是类似于广度优先的搜索策略,在getNextShortestPath中从被泄漏的对象开始,调用一次processCurrentReferrefs将持有它引用的节点(父节点),加入到一个FIFO队列中,然后依次再调用getNextShortestPath和processCurrentReferrefs来从FIFO中取节点及将这个节点的父节点再加入FIFO队列中,一层一层向上寻找,哪条路径最先到达GCRoot就表示它应该是一条最短路径。由于FIFO保存了查询信息,因此如果要找次最短路径只需要再调用一次getNextShortestPath触发下一次查找即可,其算法原理如下图所示。

LeakCanary使用及原理_第2张图片
image

至此,主要的工作就完成了,后面就是调用buildLeakTrace构建查询结果,这个过程相对简单,仅仅是将之前查找的最短路径转换成最后需要显示的LeakTrace对象,这个对象中包括了一个由路径上各个节点LeakTraceElement组成的链表,代表了检查到的最短泄漏路径。最后一个步骤就是将这些结果封装成AnalysisResult对象然后交给DisplayLeakService进行处理。这个service主要的工作是将检查结果写入文件,以便之后能够直接看到最近几次内存泄露的分析结果,同时以notification的方式通知用户检测到了一次内存泄漏。使用者还可以继承这个service类来并实现afterDefaultHandling来自定义对检查结果的处理,比如将结果上传刚到服务器等。

以上就是对LeakCanary源码的分析,中间省略了一些细节处理的说明,但不得不提的是LeakCanary支持自定义泄漏豁对象ExcludedRefs的集合,这些豁免对象一般都是一些已知的系统泄漏问题或者自己工程中已知但又需要被排除在检查之外的泄漏问题构成的。LeakCanary在findLeakTrace方法中如果发现这个集合中的对象存在于泄漏路径上,就会排除掉这条泄漏路径并尝试寻找下一条。

你可能感兴趣的:(LeakCanary使用及原理)