LeakCanary原理分析
LeakCanary的Github地址如下:https://square.github.io/leakcanary/
前言
内存泄漏问题,应该是我们项目开发过程中比较容易遇见的问题了,虽说一两个内存泄漏并不会对应用的产生致命的影响,但是一旦内存泄漏越积越多,而且一直得不到解决,那么恭喜,应用极有可能发生意想不到的OOM问题,应用会变得相当不稳定。
在以前没有LeakCanary这个库的时候,排查内存泄漏问题是个非常耗时且痛苦的过程!
举个例子,如果你认为这个地方可能会出现内存泄漏,那么你需要不断反复的进入这个界面好几次,等感觉差不多了,就把这个时候应用内存快照搞下来,也就是hprof格式的文件,这个文件包含了整个应用的内存使用情况,然后使用MAT工具打开这个文件看看哪些对象存在多个实例,如果它对应的界面已经被销毁,但实例仍存在,那就有可能发生内存泄漏。但是一旦hprof文件没有发现有疑似泄漏的对象,那么你又得去怀疑是不是另一个地方发生了泄漏,或者说是不是操作次数不够等因素导致的,相当痛苦~
回归正题,那么,LeakCanary有什么神奇之处呢?
LeakCanary是一个检测项目内存泄漏的第三方库。
目前已经升级到V2.7版本了,对于2.0以上的版本,LeakCanary的使用可以说是【相当的简单】。LeakCanary对代码的侵入性极低,我们看它的使用就明白了。
想要使用LeakCanary,我们只需要在build.gradle文件中添加入依赖即可:
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
由于使用了debugImplementation引入依赖,因此leakcanary只有在debug模式下才会生效。
仅此而已,我们已经可以在项目中检测内存泄漏了,甚至我们都不需要像其他大部分第三方库一样,需要在Application中对库进行初始化。由此可以看出,它对代码几乎是零侵入的。下面我们开始对LeakCanary进行源码分析。
源码分析(基于V2.7)
使用LeakCanary我们只需要引入它的依赖即可,并不需要我们手动对其进行初始化。这是怎么做到的呢?其实,它是利用了ContentProvider在Application被创建之前被加载的原理,在ContentProvider的onCreate完成了初始化任务。
我们在打包完一个debugApk后,在apk文件中会包含一个合并后的AndroidManifest.xml,如下:
当我们打开这个文件,我们就会看到里面有provider标签的leakcanary相关的代码:
<provider
android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
android:enabled="@ref/0x7f040008"
android:exported="false"
android:authorities="com.example.mhandler.leakcanary-installer" />
我们定位到leakcanary.internal.AppWatcherInstaller这个类:
internal sealed class AppWatcherInstaller : ContentProvider() {
internal class MainProcess : AppWatcherInstaller()
internal class LeakCanaryProcess : AppWatcherInstaller()
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
AppWatcher.manualInstall(application)
return true
}
................................................................
}
首先可以看到,它继承自ContentProvider类,也就是说,AppWatcherInstaller是一个ContentProvider!然后在它的onCreate方法中,通过AppWatcher.manualInstall(application) 方法对LeakCanary进行初始化。
我们继续进入manualInstall方法:
@JvmOverloads
fun manualInstall(
application: Application,
retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
) {
//主线程判断
checkMainThread()
//防止重复Installe
if (isInstalled) {
throw IllegalStateException(
"AppWatcher already installed, see exception cause for prior install call", installCause
)
}
......................................................................
//获取InternalLeakCanary实例加载LeakCanary
LeakCanaryDelegate.loadLeakCanary(application)
//加载watcher
watchersToInstall.forEach {
it.install()
}
}
manualInstall方法大致做了一下几件事:
1、通过Looper检查是否在主线程中,如果不在主线程,将抛出异常。
2、防止LeakCanary重复Installe
3、获取InternalLeakCanary实例加载LeakCanary
4、加载watcher
我们重点看它是如何加载watcher的,首先watchersToInstall这个变量是方法参数传进来的,而watchersToInstall的默认值是由appDefaultWatchers(application) 方法获得的,我们进入该方法:
fun appDefaultWatchers(
application: Application,
reachabilityWatcher: ReachabilityWatcher = objectWatcher
): List<InstallableWatcher> {
return listOf(
ActivityWatcher(application, reachabilityWatcher),
FragmentAndViewModelWatcher(application, reachabilityWatcher),
RootViewWatcher(reachabilityWatcher),
ServiceWatcher(reachabilityWatcher)
)
}
可以看到,它返回了一个InstallableWatcher的集合,也就是观察者的集合,它们包含:
- ActivityWatcher
- FragmentAndViewModelWatcher
- RootViewWatcher
- ServiceWatcher
其中,每个Watcher在构建的时候都传递了objectWatcher这个参数,我们先进入ActivityWatcher,看看它是如何构建的:
class ActivityWatcher(
private val application: Application,
private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {
private val lifecycleCallbacks =
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityDestroyed(activity: Activity) {
//通过objectWatcher监视activity
reachabilityWatcher.expectWeaklyReachable(
activity, "${activity::class.java.name} received Activity#onDestroy() callback"
)
}
}
override fun install() {
//注册了应用activity生命周期的回调
application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
}
override fun uninstall() {
//反注册activity生命周期回调
application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
}
}
这段代码很简单,在ActivityWatcher.install() 方法中,注册了应用Activity生命周期的回调,
当Activity执行了Destroy方法的时候,就通过objectWatcher对这个Activity进行监听,
objectWatcher监听的是啥呢?
我们想一下,当Activity回调了Destroy方法时,是不是就意味着下次GC到来时,这个Activity就需要被回收了,而objectWatcher的作用就是开始对Activity进行监听,如果Activity在GC时没有被回收,那么将会认为它发生了内存泄漏。
那么,objectWatcher是如何对Activity进行监听的呢?
我们继续进入ObjectWatcher的expectWeaklyReachable方法:
ObjectWatcher.kt:
@Synchronized override fun expectWeaklyReachable(
watchedObject: Any,
description: String
) {
if (!isEnabled()) {
return
}
//移除弱引用可达Activity对象,留下不能被GC回收的对象
removeWeaklyReachableObjects()
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
//根据activity键创建弱引用,并绑定到引用队列中
val reference =
KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
SharkLog.d {
"Watching " +
(if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
(if (description.isNotEmpty()) " ($description)" else "") +
" with key $key"
}
//将弱引用reference以键值形式存储在被监视的对象watchedObjects集合中
watchedObjects[key] = reference
// 通过线程池来启动任务
checkRetainedExecutor.execute {
// 获取不能回收的activity
moveToRetained(key)
}
}
首先看两个变量,
- watchedObject:这个是刚刚外部传进来的activity对象,
- watchedObjects:activity对象的集合,在这个队列中的activity正在被监视是否发生内存泄漏
下面我们先总结下这个expectWeaklyReachable 方法的大致流程,内部的子流程等会细说:
1、执行removeWeaklyReachableObjects() ,作用是将已被回收的Activity从watchedObjects中移除,watchedObjects将只留不能被GC回收的Activity对象
2、执行KeyedWeakReference 方法:创建出activity的弱引用reference,并将activity和引用队列queue进行绑定,同时用key来标记这个activity。这样,在activity被GC回收时,它就能自动被添加到引用队列queue上了
3、将弱引用reference以键值形式存储到watchedObjects中
4、通过线程池执行moveToRetained(key) 方法,
好了,expectWeaklyReachable的流程大概就是这样,那么我们现在继续看看内部的一些值得关注的子流程。
首先进入removeWeaklyReachableObjects方法看看:
ObjectWatcher.kt:
private fun removeWeaklyReachableObjects() {
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjects.remove(ref.key)
}
} while (ref != null)
}
可以看到,有一个循环,不断的从queue中取出对象,queue是啥,我看它的定义 :
private val queue = ReferenceQueue<Any>()
很明显,它是一个引用队列ReferenceQueue,要知道它是个什么,就需要对四大引用有一定了解了,这里就不在赘述四大引用的概念。就简单的说明一下吧,我们知道被弱引用(软引用)引用的对象,在GC到来时,就会对对象进行回收,同时,这个对象的引用还会被自动添加到与它关联的引用队列中。
知道这点就行了,也就是说,如果弱引用引用的对象被回收,那么这个弱引用将被添加到引用队列中,如果GC后,引用队列有某个对象的弱引用,就意味着这个对象被回收了,反之亦然。
removeWeaklyReachableObjects 方法内部的循环不断的从引用队列queue中取出引用对象ref,如果ref不为null(意味着它关联的对象已经被回收),就将其从watchedObjects中移除,而watchedObjects是正在被监听的可能发生内存泄漏的对象。
我们继续看回到expectWeaklyReachable 方法的倒数三行,通过线程池启动任务来执行moveToRetained(key)`方法:
ObjectWatcher.kt:
@Synchronized override fun expectWeaklyReachable(
watchedObject: Any,
description: String
) {
..............................................................
// 通过线程池来启动任务
checkRetainedExecutor.execute {
moveToRetained(key)
}
}
@Synchronized private fun moveToRetained(key: String) {
removeWeaklyReachableObjects()
val retainedRef = watchedObjects[key]
if (retainedRef != null) {
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}
这里要知道一点,就是这个线程池会默认延迟5秒才启动任务,且这个延迟的时间是可配置的,我们可以在初始化Leaknary的时候通过manualInstall方法的retainedDelayMillis参数进行配置。延迟5秒后,将执行moveToRetained(key) 方法该方法的流程如下:
1、执行removeWeaklyReachableObjects() 方法,将已被回收的Activity从watchedObjects中移除,watchedObjects将只留不能被GC回收的Activity对象
2、根据key从watchedObjects中取出activity对应的弱引用retainedRef
3、如果retainedRef不为null,保存泄漏对象的时间戳
4、遍历onObjectRetainedListeners集合(这个集合保存的元素,实际上是InternalLeakCanary对象,InternalLeakCanary实际上在manualInstall方法中已经完成了初始化)。然后执行InternalLeakCanary.onObjectRetained() 方法通知InternalLeakCanary发生内存泄漏。
下面我们继续进入InternalLeakCanary类的onObjectRetained方法:
InternalLeakCanary.kt:
override fun onObjectRetained() = scheduleRetainedObjectCheck()
可以看到onObjectRetained又执行了scheduleRetainedObjectCheck() 方法:
InternalLeakCanary.kt:
fun scheduleRetainedObjectCheck() {
if (this::heapDumpTrigger.isInitialized) {
heapDumpTrigger.scheduleRetainedObjectCheck()
}
}
判断到如果堆转储触发器heapDumpTrigger已经被安装,则继续执行heapDumpTrigger.scheduleRetainedObjectCheck() 方法:
HeapDumpTrigger.kt:
fun scheduleRetainedObjectCheck(
delayMillis: Long = 0L
) {
val checkCurrentlyScheduledAt = checkScheduledAt
if (checkCurrentlyScheduledAt > 0) {
return
}
checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
backgroundHandler.postDelayed({
checkScheduledAt = 0
checkRetainedObjects()
}, delayMillis)
}
这个方法中,在后台线程执行了checkRetainedObjects() 方法检查泄漏对象:
private fun checkRetainedObjects() {
val iCanHasHeap = HeapDumpControl.iCanHasHeap()
val config = configProvider()
if (iCanHasHeap is Nope) {
if (iCanHasHeap is NotifyingNope) {
//获取可能存在内存泄漏的对象的总数
var retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount > 0) {
//如果总数大于0,则抖动调用一次GC
gcTrigger.runGc()
//再次获取可能存在内存泄漏的对象的总数
retainedReferenceCount = objectWatcher.retainedObjectCount
}
val nopeReason = iCanHasHeap.reason()
//是否需要进行堆转储
val wouldDump = !checkRetainedCount(
retainedReferenceCount, config.retainedVisibleThreshold, nopeReason
)
if (wouldDump) {
val uppercaseReason = nopeReason[0].toUpperCase() + nopeReason.substring(1)
onRetainInstanceListener.onEvent(DumpingDisabled(uppercaseReason))
//显示发生内存泄漏的通知
showRetainedCountNotification(
objectCount = retainedReferenceCount,
contentText = uppercaseReason
)
}
} else {
SharkLog.d {
application.getString(
R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
)
}
}
return
}
// 再次获取可能存在内存泄漏的对象的总数
var retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount > 0) {
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}
//检查泄漏的对象总数,内部实现:如果数量小于等于5,会返回true
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
val now = SystemClock.uptimeMillis()
val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
onRetainInstanceListener.onEvent(DumpHappenedRecently)
showRetainedCountNotification(
objectCount = retainedReferenceCount,
contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait)
)
// 定时检测对象
scheduleRetainedObjectCheck(
delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
)
return
}
// 关闭持有对象的数量通知
dismissRetainedCountNotification()
val visibility = if (applicationVisible) "visible" else "not visible"
// 堆转储
dumpHeap(
retainedReferenceCount = retainedReferenceCount,
retry = true,
reason = "$retainedReferenceCount retained objects, app is $visibility"
)
}
方法有点长,大致流程是这样的:
1、获取可能存在内存泄漏的对象的总数,如果总数大于0,则进行手动执行一次GC
2、通过checkRetainedCount 方法(如果泄漏数量大于5时返回true),判断是否需要进行堆转储,并把判断结果保存在wouldDump变量中,也就是说,如果泄漏对象大于5个,wouldDump将会置为true,wouldDump为true,将会显示发生内存泄漏的通知
3、再次获取可能存在内存泄漏的对象的总数,如果总数大于0,则进行手动执行一次GC
4、再次调用checkRetainedCount 方法,如果泄漏对象小于等于5个,将直接return,如果大于5个,则往下执行代码
5、判断两次堆转储的时间,如果间隔小于60S,也会直接return,避免频繁进行堆转储
6、如果上面的流程方法都没有被return,将执行dumpHeap 方法进行堆转储操作
dumpHeap 方法就不看了,因为涉及到了square的另一个开源库shark的一些操作,感兴趣的大佬自己去了解吧。另外,dumpHeap 方法执行的过程中,还会执行objectWatcher.clearObjectsWatchedBefore 方法对已经提醒了内存泄漏的引用从watchedObjects中移除。
至此,LeakCanary如何对Activity对象进行内存泄漏的检测已经分析完毕,
另外,还有对Fragment、RootView、Service的监听我们还没有看,但是它们的大体的流程其实是一样的,只是监听的入口稍有不同,后续等我有时间再继续补全这篇文章,感兴趣的大佬可以跟着本篇幅的分析流程继续深入。
|