LeakCanary 浅析
前言
平时我们都有用到LeakCanary来分析内存泄露的情况,这里可以来看看LeakCanary是如何实现的,它的内部又有哪些比较有意思的操作。
LeakCanary的使用
官方文档:square.github.io/leakcanary/…
引用方式
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
}
可以看到LeakCanary的新版本中依赖非常简单,甚至不需要你做什么就可以直接使用。
LeakCanary原理
LeakCanary的封装主要是利用ContentProvider,LeakCanary检测内存泄漏主要是监听Activity和Fragment、view的生命周期,配合弱引用和ReferenceQueue。
源码浅析
初始化
首先debugImplementation只是在Debug的包会依赖,在正式包不会把LeakCanary的内容打进包中。
LeakCanary的初始化是使用了ContentProvider,ContentProvider的onCreate会在Application的onCreate之前,它把ContentProvider写在自己的AndroidMainifest中,打包时会进行合并,所以这整个过程都不需要接入端做初始化操作。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.squareup.leakcanary.objectwatcher" >
<uses-sdk android:minSdkVersion="14" />
<application>
<provider
android:name="leakcanary.internal.MainProcessAppWatcherInstaller"
android:authorities="${applicationId}.leakcanary-installer"
android:enabled="@bool/leak_canary_watcher_auto_install"
android:exported="false" />
</application>
</manifest>
这是它在AndroidManifest所定义的,打包的时候会合并所有的AndroidManifest
这就是它自动初始化的操作,也比较明显了,不用过多解释。
使用
先看看它要监测什么,因为LeakCanary 2.x的代码都是kotlin写的,所以这里得分析kotlin,如果不熟悉kt的朋友,我只能说尽量讲慢一些,因为我想看旧版本的能不能用java来分析,但是简单看了下源码上是有一定的差别,所以还是要分析2.x。
fun appDefaultWatchers(
application: Application,
reachabilityWatcher: ReachabilityWatcher = objectWatcher
): List<InstallableWatcher> {
return listOf(
ActivityWatcher(application, reachabilityWatcher),
FragmentAndViewModelWatcher(application, reachabilityWatcher),
RootViewWatcher(reachabilityWatcher),
ServiceWatcher(reachabilityWatcher)
)
}
从这里看到他主要分析Activity、Fragment和Fragment的View、RootView、Service。
看Activity的监听ActivityWatcher
监听Activity调用Destroy时会调用reachabilityWatcher的expectWeaklyReachable方法。
这里可以看看旧版本的做法(正好以前有记录)
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
new ActivityLifecycleCallbacksAdapter() {
@Override public void onActivityDestroyed(Activity activity) {
refWatcher.watch(activity);
}
};
旧版本是调用refWatcher的watch,虽然代码不同,但是思想一样,再看看旧版本的Fragment
private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =
new FragmentManager.FragmentLifecycleCallbacks() {
@Override public void onFragmentViewDestroyed(FragmentManager fm, Fragment fragment) {
View view = fragment.getView();
if (view != null) {
refWatcher.watch(view);
}
}
@Override
public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
refWatcher.watch(fragment);
}
};
这里监听了Fragment和Fragment的View,所以相比于新版本,旧版本只监听Activity、Fragment和Fragment的View
再回到新版本,分析完Activity的监听之后看看Fragment的
最终Destroy之后也是调用到reachabilityWatcher的expectWeaklyReachable。然后看看RootViewWatcher的操作
private val listener = OnRootViewAddedListener { rootView ->
val trackDetached = when(rootView.windowType) {
PHONE_WINDOW -> {
when (rootView.phoneWindow?.callback?.wrappedCallback) {
is Activity -> false
is Dialog -> {
......
}
else -> true
}
}
POPUP_WINDOW -> false
TOOLTIP, TOAST, UNKNOWN -> true
}
if (trackDetached) {
rootView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
val watchDetachedView = Runnable {
reachabilityWatcher.expectWeaklyReachable(
rootView, "${rootView::class.java.name} received View#onDetachedFromWindow() callback"
)
}
......
})
}
}
最终也是调用到reachabilityWatcher的expectWeaklyReachabl。最后再看看Service的。
这边因为只是做浅析,不是源码详细分析,所以我这边就不去一个个分析是如何调用到销毁的这个方法的,我们通过上面的方法得到一个结论,Activity、Fragment和Fragment的View、RootView、Service,他们几个,在销毁时都会调用到reachabilityWatcher的expectWeaklyReachabl。所以这些地方就是检测对象是否泄漏的入口。
然后我们来看看expectWeaklyReachable方法
@Synchronized override fun expectWeaklyReachable(
watchedObject: Any,
description: String
) {
// 先从queue中移除一次已回收对象
removeWeaklyReachableObjects()
// 生成随机数当成key
val key = UUID.randomUUID().toString()
val watchUptimeMillis = clock.uptimeMillis()
// 创建弱引用关联ReferenceQueue
val reference =
KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
......
// 把reference和key 添加到一个Map中
watchedObjects[key] = reference
// 下一步
checkRetainedExecutor.execute {
moveToRetained(key)
}
}
你们运气真好,我正好以前也有记录旧版本的refWatcher的watch方法
public void watch(Object watchedReference, String referenceName) {
......
// 生成随机数当成key
String key = UUID.randomUUID().toString();
// 把key 添加到一个Set中
this.retainedKeys.add(key);
// 创建弱引用关联ReferenceQueue
KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
// 下一步
this.ensureGoneAsync(watchStartNanoTime, reference);
}
通过对比发现,模板的流程是一样的,但是细节不一样,以前是用Set,现在是用Map,这就是我觉得不能拿旧版本代码来分析的原因。
文章写到这里,突然想到一个很有意思的东西,你要是面试时,面试官看过新版本的代码,你看的是旧版本的代码,结果如果问到一些比较深入的细节,你答出来的和他所理解的不同,那就尴尬了,所以面试时得先说清楚你是看过旧版本的代码
看到用一个弱引用生成一个key和对象绑定起来。然后调用ensureGoneAsync方法
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
this.watchExecutor.execute(new Retryable() {
public Result run() {
return RefWatcher.this.ensureGone(reference, watchStartNanoTime);
}
});
}
execute里面会调用到waitForIdle方法。
我们再回到新版本的代码中
checkRetainedExecutor.execute其实是会执行到这里(kt里面的是写得简单,但是不熟的话可以先别管怎么执行的,只要先知道反正执行到这个地方就行)
这里是做了一个延时发送消息的操作,延时5秒,具体代码在这里
写到这里我感觉有点慌了,因为如果不熟kt的朋友可能真会看困,其实如果看不懂这个代码的话没关系,只要我圈出来的地方,我觉是大概能看懂的,然后流程我会说,我的意思是没必要深入去看每一行是什么意思,我们的目的是找出大概的流程(用游戏的说法,我们是走主线任务,不是要全收集)
延迟5秒后会调回到前面的moveToRetained(key)。那不好意思各位,我又要拿旧版本来对比了,因为细节不同。
private void waitForIdle(final Retryable retryable, final int failedAttempts) {
// 使用IdleHandler来实现在闲时才去执行后面的流程
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override public boolean queueIdle() {
postToBackgroundWithDelay(retryable, failedAttempts);
return false;
}
});
}
使用IdleHandler来完成闲时触发,我不记得很早之前的版本是不是也用的IdleHandler,这里使用IdleHandler只能说有好有坏吧,好处是闲时触发确实是一个很好的操作,不好的地方是如果一直有异步消息,就一直不会触发后面的流程。
private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
long delayMillis = initialDelayMillis * exponentialBackoffFactor;
// 根据上下文去计算,这里是5秒
backgroundHandler.postDelayed(new Runnable() {
@Override public void run() {
Retryable.Result result = retryable.run();
if (result == RETRY) {
postWaitForIdle(retryable, failedAttempts + 1);
}
}
}, delayMillis);
}
看到旧版本是先用IdelHanlder,在闲时触发的情况下再去延时5秒,而新版本是直接延时5秒,不使用IdelHandler,我没看过这块具体的文档描述,我猜是为了防止饿死,如果用IdelHanlder的话可能会出现一直不触发的情况。
返回看新版本的moveToRetained
@Synchronized private fun moveToRetained(key: String) {
// 从ReferenceQueue中拿出对象移除
removeWeaklyReachableObjects()
// 经过上一步之后判断Map中还有没有这个key,有的话进入下一步操作
val retainedRef = watchedObjects[key]
if (retainedRef != null) {
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}
private fun removeWeaklyReachableObjects() {
// 从ReferenceQueue中拿出对象,然后从Map中移除
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjects.remove(ref.key)
}
} while (ref != null)
}
moveToRetained主要是从ReferenceQueue中找出弱引用对象,然后移除Map中相应的弱引用对象。弱引用+ReferenceQueue的使用,应该不用多说吧,如果弱引用持有的对象被回收,弱引用会添加到ReferenceQueue中。所以watchedObjects代表的是应该将要被回收的对象,queue表示已经被回收的对象,这步操作就是从queue中找出已经回收的对象,然后从watchedObjects移除相应的对象,剩下的的就是应该被回收却没被回收的对象。如果对象被正常回收,那这整个流程就走完了,如果没被回收,会执行到onObjectRetained(),之后就是Dump操作了,之后的就是内存分析、弹出通知那堆操作了,去分析内存的泄漏这些,因为内容比较多,这篇先大概就先到这里。
总结
浅析,就是只做了简单分析LeakCanary的整个工作过程和工作原理。
原理就是用弱引用和ReferenceQueue去判断应该被回收的对象是否已经被回收。大致的工作流程是:监听Activity、Fragment和Fragment的View、RootView、Service对象的销毁,然后将这些对象放入“应该被回收”的容器中,然后5秒后通过弱引用和ReferenceQueue去判断对象是否已被回收,如果被回收则从容器中删除对应的对象,否则进行内存分析。
至于是如何判断不同对象的销毁和如何分析内存情况找出泄漏的引用链,这其中也是细节满满,但是我个人LeakCanary应该是看过两三次源码了,从一开始手动初始化,到旧版本java的实现方式,到现在用kt去实现,能发现它的核心思想其实是一样的,只不过在不断的优化一些细节和不断的扩展可以监测的对象。
2.10版本的源码我也是第一次看,我是一面看一面写这篇文章,如果有哪里流程是我看错的,希望大佬们能及时提出,我也会及时更正。
链接:https://juejin.cn/post/7167026728596946980
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。