开源项目之LeakCanary源码分析
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
,可以看到通知栏上有内存泄漏的标记:
点开通知之后可以发现我们这个Object
泄漏的路径,是不是很赞?好吧,如果这是你的第一印象,我相信一些爱思考的童鞋马上会反应过来,这个不对吧,为啥要我们自己去调用watch(Object watchedReference)
呢?内存泄漏不应该是自动检查的吗?呵呵,这是LeakCanary
实现的第一个步骤带来的必然限制,我先卖个关子,嘿嘿。
二、原理
总体流程
在LeakCanary
中,检测主要分为三步:1.检测一个对象是否是可疑的泄漏对象;2.如果第一步发现可疑对象,dump
内存快照,通过分析.hprof
文件,确定怀疑的对象是否真的泄漏。3.将分析的结果展示
2.1检测对象是否有泄漏的可能
如果你对ReferenceQueue
的机制很熟悉的话,可以使用ReferenceQueue
对对象的可达性进行监视,最常见的比如JDK中的WeakHashMap
,LeakCanary
同样使用了这个机制。看看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
线程来处理这些分析逻辑(HandlerThread
是Android SDK
提供的封装好了Looper
的Thread
)。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
进程的另外一个进程中。HeapAnalyzer
是LeakCanary
的hprof
工具类,分析代码:
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公司点赞!