Android原理创业Android开发

开源项目之LeakCanary源码分析

2015-11-18  本文已影响4130人  楚云之南

LeakCanary项目是大名鼎鼎的square公司为Java&Android开发提供的一个自动检测内存泄漏的工具,现在很多项目都在引入来提高代码质量,减少不必要的内存泄漏。尽管Java有垃圾回收机制,但是一些无意识的代码经常会导致对象的引用存在超过其生命周期,比如最常见的Activity泄漏,Message泄漏等,本文通过源码分析来了解它是如何做到的。

一、使用简单介绍

public class MainActivity extends Activity {
private static Object leakReference;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main_activity);
    //注册一个Android平台的RefWatcher
    RefWatcher refWatcher = LeakCanary.androidWatcher(getApplicationContext(),
            new ServiceHeapDumpListener(getApplicationContext(), DisplayLeakService.class),
            AndroidExcludedRefs.createAppDefaults().build());
    //将显示结果的Activity设置成可用
    LeakCanary.enableDisplayLeakActivity(getApplicationContext());
    Object obj = new Object();
    //故意将leakRefrence指向obj,使得这个对象的强引用存在时间超过其生命周期
    leakReference = obj;
    refWatcher.watch(obj);
  }
}

这样,我们直接Run app,可以看到通知栏上有内存泄漏的标记:

Paste_Image.png Paste_Image.png Paste_Image.png

点开通知之后可以发现我们这个Object泄漏的路径,是不是很赞?好吧,如果这是你的第一印象,我相信一些爱思考的童鞋马上会反应过来,这个不对吧,为啥要我们自己去调用watch(Object watchedReference)呢?内存泄漏不应该是自动检查的吗?呵呵,这是LeakCanary实现的第一个步骤带来的必然限制,我先卖个关子,嘿嘿。

二、原理

总体流程

LeakCanary中,检测主要分为三步:1.检测一个对象是否是可疑的泄漏对象;2.如果第一步发现可疑对象,dump内存快照,通过分析.hprof文件,确定怀疑的对象是否真的泄漏。3.将分析的结果展示

2.1检测对象是否有泄漏的可能

如果你对ReferenceQueue的机制很熟悉的话,可以使用ReferenceQueue对对象的可达性进行监视,最常见的比如JDK中的WeakHashMapLeakCanary同样使用了这个机制。看看RefWatcher.watch方法源码:

public void watch(Object watchedReference, String referenceName) {
//如果VM正连接到Debuger,忽略这次检测,因为Debugger可能会持有一些在当前上下文中不可见的对象,导致误判
if (debuggerControl.isDebuggerAttached()) {
  return;
}
final long watchStartNanoTime = System.nanoTime();
//对一个Refercen产生一个唯一的Key
String key = UUID.randomUUID().toString();
//放到key集合中
retainedKeys.add(key);
//讲watch传入的对象添加一个弱引用
final KeyedWeakReference reference =
    new KeyedWeakReference(watchedReference, key, referenceName, queue);
//在异步线程上开始分析这个弱引用
watchExecutor.execute(new Runnable() {
  @Override public void run() {
    ensureGone(reference, watchStartNanoTime);
  }
});
}

KeyedWeakReference extends WeakReference<Object> {
    //对每一个弱引用添加标识key
    public final String key;
}

你看到了watch方法其实并没有什么高深的地方,无非是使用一个弱引用连接到你需要检测的对象,然后使用ReferenceQueue来监测这个弱引用可达性的改变,需要注意的是,这里我们通过UUID类为每一个弱引用添加了一个唯一标识。

Android平台上,LeakCanary为我们简单实现了一个AndroidWatchExecutor,里面封装了一个HandlerThread线程来处理这些分析逻辑(HandlerThreadAndroid SDK提供的封装好了LooperThread)。AndroidWatchExecutor中通过Handler

new Runnable() {
  @Override public void run() {
    ensureGone(reference, watchStartNanoTime);
  }
};

提交到线程的消息队列中。我们看看ensureGone里面做了什么。

void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
//清除此时已经到ReferenceQueue中的弱引用
removeWeaklyReachableReferences();
// (1)如果当前检测的对象已经弱可达,那么说明对象已经不会泄漏 
if (gone(reference) || debuggerControl.isDebuggerAttached()) {
  return;
}
//如果当前检测对象还没有改变其可达状态,GC()
gcTrigger.runGc();
//再次判断对象有没有进入队列
removeWeaklyReachableReferences();
//   (2)如果此时还没有检测到入队列,那么有可能这个对象已经泄漏
if (!gone(reference)) {
     //进入第二部,dump内存,分析内存快照
    doNextStep();
}
}

private void removeWeaklyReachableReferences() {
//清除此时已经到队列中的弱引用
KeyedWeakReference ref;
while ((ref = (KeyedWeakReference) queue.poll()) != null) {
  //在key列表中删除这个key,因为这个key映射到的weakreference已经是弱可达了,那么说明此时必然没有强引用指向需要监测的对象
  retainedKeys.remove(ref.key);
}

}

**重点注意:
A> 上述代码中标注(1),(2)处,实际上会存在一点小问题,如果此时对象在finalize()把对象复活,那么此时实际上是存在内存泄漏风险的,但是这里会直接放过这次check
B> 为什么还需要doNextStep()呢?因为我们这里调用GC并不能保证JVM真正会执行GC,只是建议。如果没有走GC的话,所谓对象标记也就不会执行,对象的可达性就不会变化,所以这里只能猜测这个对象很有可能泄漏,所以LeakCanary还需要真正看看此时的内存堆上的情况。
**

这里我们就可以解释为啥LeakCanary在进行内存泄漏时需要手动调用watch去监测一个对象,因为我们需要一个合适的时机去用一个弱引用把这个对象使用ReferenceQueue监测起来。如此一来,LeakCanary最大的一个缺陷就是我需要知道一个对象的确切生命周期,并在我们认为其生命周期应该结束的时间点进行watch。对于Android平台来说,常见的场景就是Activity、Fragment等对象的onDestory中。

你可能已经发现了,本文第一节使用的代码和官方的Demo中直接在Application中注册不同,那么有啥区别呢?

public static RefWatcher refWatcher;
//Application中
@Override public void onCreate() {
    super.onCreate();
    refWatcher = LeakCanary.install(this);
}

Application中注册,实际上仅仅是在Application.ActivityLifecycleCallbacks中的onActivityDestroyed为所有的Activity进行watch,由于这个接口是Android 4.0以后才有的,所以对于4.0以前的版本,我们需要在Activity的基类中调用refWatcher.watch(this)

2.2 分析dump文件

上面的doNextStep()中包含的代码,主要功能是dump当前内存堆的hprof文件,并使用square公司另外一个开源分析内存工具HaHa

Android dump内存接口:
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());

.hprof文件交给一个HeapAnalyzerService处理,注意,这个HeapAnalyzerService是运行在和当前app进程的另外一个进程中。HeapAnalyzerLeakCanaryhprof工具类,分析代码:

public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
try {
  //加载hprof文件
  HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
  HprofParser parser = new HprofParser(buffer);
  Snapshot snapshot = parser.parse();
  //找到泄漏对象的引用
  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));
}
}

//找到泄漏的对象的引用
private Instance findLeakingReference(String key, Snapshot snapshot) {
  //找到内存中所有的KeyedWeakReference引用
  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"));
    // 判断是否是持有检测对象的弱引用,这里由于每一个KeyedWeakReference具有唯一的UUID
    //所以可以根据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);
}

至此,我们已经找到dump中泄漏对象的引用路径,(从泄漏对象到GC根的最短路径),如果存在这样的路径,那么我们可以肯定这个对象已经泄漏,此时LeakCanary会发一个Notification,并设置一个PenddingIntent到展示的Activity

2.3 展示

这里就不多说了,无非就是把泄漏的路径放到Activity中展示一下。

2.4 其他说明

LeakCanary提供了ExcludedRefs来灵活控制是否需要将一些对象排除在考虑之外,因为在Android Framework层自身也存在一些内存泄漏,对于开发者来说这些泄漏是我们无能为力的,所以在AndroidExcludedRefs中定义了很多排除考虑的类。

以上,就是LeakCanary的一些源码分析。

最后补充点感想吧,我之前曾经在公司内部的技术文档上看到了一位同事曾经也提出过可以使用Reference队列来监测Activity的内存泄漏,但是当时有几个技术点没有解决,主要原因是我们进行到第一步之后,没有想到使用dump内存的方式来进行泄漏确认,更没有想到HAHA这个开源项目直接提供了解析hprof文件的功能,还是要为Square公司点赞!

上一篇下一篇

猜你喜欢

热点阅读