源码分析二:LeakCanary

413 阅读7分钟

分析内存泄漏分析工具

核心一是ReferenceQueue,这是一个用来保存Reference的队列,具体见WeakReference的构造方法:

public class WeakReference<T> extends Reference<T> {
    public WeakReference(T var1) {
        super(var1);
    }

    public WeakReference(T var1, ReferenceQueue<? super T> var2) {
        super(var1, var2);
    }
}

先了解WeakReference和ReferenceQueue:

WeakReference和ReferenceQueue

首先需要知道WeakReference。当GC线程扫描它所管辖的内存区域时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否(这一点与SoftReference不同),都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

                                                WeakReference_obj

WeakReference和ReferenceQueue联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。上图中的实线a表示强引用,虚线aa表示弱引用。如果切断a,那么Object对象将会被回收。正如下面这段测试代码所示。

A a = new A();
 ReferenceQueue queue = new ReferenceQueue();
 WeakReference aa = new WeakReference(a, queue);
 a = null;
 Runtime.getRuntime().gc();
 System.runFinalization();
 try {
 
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 Reference poll = null;
 while ((poll = queue.poll()) != null) {
 System.out.println(poll.toString());
 }

当垃圾回收器回收对象的时候,aa这个弱引用将会入队进入ReferenceQueue,所以queue.poll()将不会为空,除非这个对象没有被垃圾回收器清理。ok了,开始分析程序。

程序入口

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

关键在buildAndInstall这个方法

public RefWatcher buildAndInstall() {
  RefWatcher refWatcher = build();
  if (refWatcher != DISABLED) {
    LeakCanary.enableDisplayLeakActivity(context);
    ActivityRefWatcher.install((Application) context, refWatcher);//关键代码
  }
  return refWatcher;
}

看install

public static void install(Application application, RefWatcher refWatcher) {
  new ActivityRefWatcher(application, refWatcher).watchActivities();
}

watchActivities

public void watchActivities() {
  // Make sure you don't get installed twice.
  stopWatchingActivities();
  application.registerActivityLifecycleCallbacks(lifecycleCallbacks);//4.0提供的生命周期的监测
}

lifecycleCallbacks

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 Destroy的时候去监测内存泄漏

      }
    };

onActivityDestroyed:

void onActivityDestroyed(Activity activity) {
  refWatcher.watch(activity);
}

到这里,可以看到leakcanary在4.0以上系统的监测是通过ActivityLifecycleCallbacks来监测的,监测时机是在onActivityDestroyed。具体监测方法是在refWatcher,继续往下看。

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();//生成唯一key
  retainedKeys.add(key);//retainedKeys中添加这个key
  final KeyedWeakReference reference =
      new KeyedWeakReference(watchedReference, key, referenceName, queue);
    //包装成WeakReference并添加到ReferenceQueue
  ensureGoneAsync(watchStartNanoTime, reference);
}

主要就是封装。且用retainedKeys这个set来保存每个引用的标识key。后面将会用到。封装完成就要开始分析了,核心方法是ensureGone:

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)) {
    long startDumpHeap = System.nanoTime();
    long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

    File heapDumpFile = heapDumper.dumpHeap();
//竟然还没回收,那么怀疑是内存泄漏了,所以下一步dump内存快照.hprof下来进一步精确分析
    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);
  }
}

1、如果这个对象作为弱引用,被回收了,那么java虚拟机会自动将他添加到引用队列(ReferenceQueue)当中。

2、这个时候去queue.poll,将拿到弱引用ref(Activity)对应的key,retainedKeys去删除掉这个key(之前有往里面添加)。表明该对象的引用已经释放。

3、再用gone(reference)来确定retainedKeys是否已经删除对应的key了。

4、如果没有删除,则手动再gc一次(之前都是系统回收)。

5、再确认一下,如果还没有回收(即retainedKeys中还有这个key),则dump下来内存(.hprof文件)进行分析。

.hprof内存分析

通过上面的二次确认,可以确定某个object(Activity或者fragment)存在内存泄漏。那么接下来就是dump下来内存并进行analyze。

 File heapDumpFile = heapDumper.dumpHeap();
//竟然还没回收,那么怀疑是内存泄漏了,所以下一步dump内存快照.hprof下来进一步精确分析
    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));

调用ServiceHeapDumpListener#analyze

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

最终调用到HeapAnalyzerService#onHandleIntent,其中HeapAnalyzerService是独立运行在:leakcanary进程:

@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);
  //这里分析heapDump
  AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
  //这里将分析结果发送出去做展示
}

着重在HeapAnalyzer#checkForLeak

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); 
    //1.把.hprof转为Snapshot,这个Snapshot对象就包含了对象引用的所有路径
    HprofParser parser = new HprofParser(buffer);
    //2.精简gcroots??
    Snapshot snapshot = parser.parse();
    deduplicateGcRoots(snapshot);

    Instance leakingRef = findLeakingReference(referenceKey, snapshot);
    //3.找出泄漏的对象
    
    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步:(很多处理逻辑都是haha这个第三方库实现的)

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值相同,那么返回这个泄漏对象,就是已近在快照中定位到了泄漏对象了。

继续看findLeakTrace

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();

  // TODO: check O sources and see what happened to android.graphics.Bitmap.mBuffer
  if (SDK_INT <= N_MR1) {
    retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
  }

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

最后一个步骤是根据上一部得到的泄漏对象找到最短路径,封装在ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);

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

if (leakingRef == null) {
  return noLeak(since(analysisStartNanoTime));
}

总结、RefWatcher工作流程:

  • RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
  • 然后在后台线程检查引用是否被清除,如果没有,调用GC。
  • 如果引用还是未被清除,把 heap 内存 dump 到 文件系统中的一个 .hprof 文件中。
  • 在另外一个进程中,HeapAnalyzerService 通过 HeapAnalyzer 使用HAHA 解析这个文件。
  • 得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄漏。
  • HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否泄漏。如果是,建立导致泄漏的引用链。
  • 引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。

      

以上,就是leakCanary的全部流程。