简介
LeakCanary是Square公司基于MAT开源的一个工具,用于检测Android的内存泄漏。
官方地址是:https://github.com/square/leakcanary。
使用
引入:
dependencies {
...
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.1'
}
如果只监控Activity,引入此库其实就已经满足要求了。
如果需要监控其他的对象,比如Fragment等,就需要在对于对象的销毁方法中手动监听。
protected void onDestroy() {
super.onDestroy();
AppWatcher.INSTANCE.getObjectWatcher().watch(this,"界面泄漏了");
}
比如上面的代码,就是主动监听Activity对象。
然后运行代码执行销毁操作,等候一段时间,Leakcanary自动生成的图标中就会展示对应抓取的泄漏的信息。如下图所示:
此时我们可以清楚的看到发生泄漏的地方,还有发生泄漏时的描述。
Leakcanary这么神奇,那么它是如何实现的呢?
下面我们分析一些Leakcanary原理。
Leakcanary原理
在使用Leakcanary2.1的时候有个疑问,为什么只需要引入依赖就可以直接监听到Activity和Fragment的泄漏了?
对比1.5版本使用:
首先我们要添加一下依赖:
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}
然后Application创建方法中初始化:
public class ExampleApplication extends Application {
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code...
}
}
LeakCanary.install() 执行后,就会构建 RefWatcher 对象,开始监听 Activity.onDestroy() 回调, 通过RefWatcher.watch() 监测 Activity 引用的泄露情况。发现内存泄露之后进行 heap dump ,利用 Square 公司的另一个库 haha(已废弃)来分析 heap dump 文件,找到引用链之后通知用户。
新版本中直接引用就能监听,到底是如何完成的呢?
在编译好的apk文件的AndroidManifest.xml文件中,我们找到了AppWatcherInstaller$MainProgress的内容提供者:
internal sealed class AppWatcherInstaller : ContentProvider() {
/**
* [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.
*/
internal class MainProcess : AppWatcherInstaller()
/**
* When using the `leakcanary-android-process` artifact instead of `leakcanary-android`,
* [LeakCanaryProcess] automatically sets up the LeakCanary code
*/
internal class LeakCanaryProcess : AppWatcherInstaller() {
override fun onCreate(): Boolean {
super.onCreate()
AppWatcher.config = AppWatcher.config.copy(enabled = false)
return true
}
}
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
InternalAppWatcher.install(application)
return true
}
...
在这个类中,我们发现onCreate生命周期方法中,调用了InternalAppWatcher.install(application)方法,而这个方法就是初始化的方法。我们知道ContentProvider的onCreate方法在Application的onCreate方法之前调用,因此借助ContentProvider完成了LeakCanary的初始化工作。当然这种方式比较消耗性能,但是因为LeakCanary只在debug环境使用,因此没有受此影响。
接着我们看下InternalAppWatcher.install(application)做了哪些操作:
fun install(application: Application) {
SharkLog.logger = DefaultCanaryLog()
SharkLog.d { "Installing AppWatcher" }
checkMainThread()
if (this::application.isInitialized) {
return
}
InternalAppWatcher.application = application
val configProvider = { AppWatcher.config }
ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
onAppWatcherInstalled(application)
}
在初始化方法中最核心的两个操作就是ActivityDestroyWatcher.install和FragmentDestroyWatcher.install,这两个操作是用了监听Activity和Fragment泄漏的操作。这两个操作逻辑大体上一致,我们看看ActivityDestroyWatcher的逻辑。
internal class ActivityDestroyWatcher private constructor(
private val objectWatcher: ObjectWatcher,
private val configProvider: () -> Config
) {
private val lifecycleCallbacks =
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityDestroyed(activity: Activity) {
if (configProvider().watchActivities) {
objectWatcher.watch(
activity, "${activity::class.java.name} received Activity#onDestroy() callback"
)
}
}
}
companion object {
fun install(
application: Application,
objectWatcher: ObjectWatcher,
configProvider: () -> Config
) {
val activityDestroyWatcher =
ActivityDestroyWatcher(objectWatcher, configProvider)
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
}
}
}
ActivityDestroyWatcher 逻辑很简单,install方法中对Application绑定Activity生命周期回调,在Activity销毁的生命周期回调方法onActivityDestroyed中调用ObjectWatcher的watch方法检查是否内存泄漏,因此我们接着看下ObjectWatcher的逻辑:
class ObjectWatcher constructor(
private val clock: Clock,
private val checkRetainedExecutor: Executor,
//泄漏监听
private val onObjectRetainedListeners = mutableSetOf()
/**
*正在被观察的对象,此时还未泄漏
*/
private val watchedObjects = mutableMapOf()
//引用队列 配合弱引用 定位泄漏的对象。
private val queue = ReferenceQueue()
...
通过watch传入的对象会被存放到watchedObjects 中,queue是ReferenceQueue类型的队列,配合弱引用使用,当弱引用一旦变得弱可达,就会立即入队。这将在 finalization 或者 GC 之前发生。也就是说会被GC回收的对象被存放到队列queue中。
来看看watch方法:
/**
* Watches the provided [watchedObject].
*
* @param description Describes why the object is watched.
*/
@Synchronized fun watch(
watchedObject: Any,
description: String
) {
if (!isEnabled()) {
return
}
removeWeaklyReachableObjects() //移除队列中要被GC的引用。
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
val reference =
KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue) //生成当前对象的弱引用对象,并与queue关联。
SharkLog.d {
"Watching " +
(if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
(if (description.isNotEmpty()) " ($description)" else "") +
" with key $key"
}
//将弱引用对象存放到队列中
watchedObjects[key] = reference
checkRetainedExecutor.execute {
moveToRetained(key) //如果当前引用未被移除,仍在watchedObject中,说明未被GC,这时代表泄露,回调onObjectRetained方法。
}
}
代码的逻辑还是比较清晰的,首先调用removeWeaklyReachableObjects方法,目的是移除watchedObject中即将被GC的引用。
removeWeaklyReachableObjects方法
private fun removeWeaklyReachableObjects() {
// 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.
//弱引用一旦弱可达,就会入队,此操作发生在GC之前。
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjects.remove(ref.key)
}
} while (ref != null) //移除要被GC的对象,剩余就就是可能泄露的对象。
}
此方法会多次调用,确保将入队的将被GC掉的对象移除掉,避免重复的Heap dump操作,仍在watchedObject的引用则是代表已经泄漏的,此时会回调监听的onObjectRetained方法,这就是moveToRetained的逻辑:
@Synchronized private fun moveToRetained(key: String) {
removeWeaklyReachableObjects()
val retainedRef = watchedObjects[key]
if (retainedRef != null) {
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}
这个方法最终会回调到InternalLeakCanary的onObjectRetained方法:
override fun onObjectRetained() {
if (this::heapDumpTrigger.isInitialized) {
heapDumpTrigger.onObjectRetained()
}
}
在此方法中调用了HeapDumpTrigger的onObjectRetained方法,
fun onObjectRetained() {
scheduleRetainedObjectCheck(
reason = "found new object retained",
rescheduling = false
)
}
private fun scheduleRetainedObjectCheck(
reason: String,
rescheduling: Boolean,
delayMillis: Long = 0L
) {
val checkCurrentlyScheduledAt = checkScheduledAt
if (checkCurrentlyScheduledAt > 0) {
val scheduledIn = checkCurrentlyScheduledAt - SystemClock.uptimeMillis()
SharkLog.d { "Ignoring request to check for retained objects ($reason), already scheduled in ${scheduledIn}ms" }
return
} else {
val verb = if (rescheduling) "Rescheduling" else "Scheduling"
val delay = if (delayMillis > 0) " in ${delayMillis}ms" else ""
SharkLog.d { "$verb check for retained objects${delay} because $reason" }
}
checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
backgroundHandler.postDelayed({
checkScheduledAt = 0
checkRetainedObjects(reason)//检查泄漏的引用
}, delayMillis)
}
checkRetainedObjects是最后一个检查是否泄露的方法,这里会确认引用是否真的泄露,如果真的泄露,则发起 heap dump,分析 dump 文件,找到引用链,最后通知用户。整体流程和老版本是一致的,但在一些细节处理,以及 dump 文件的分析上有所区别。下面还是通过源码来看看这些区别。
private fun checkRetainedObjects(reason: String) {
val config = configProvider()
// A tick will be rescheduled when this is turned back on.
if (!config.dumpHeap) {
SharkLog.d { "Ignoring check for retained objects scheduled because $reason: LeakCanary.Config.dumpHeap is false" }
return
}
var retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount > 0) {
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}
//检查泄漏次数是否超过5此,不超过不尽兴dump
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
showRetainedCountNotification(
objectCount = retainedReferenceCount,
contentText = application.getString(
R.string.leak_canary_notification_retained_debugger_attached
)
)
scheduleRetainedObjectCheck(
reason = "debugger is attached",
rescheduling = true,
delayMillis = WAIT_FOR_DEBUG_MILLIS
)
return
}
val now = SystemClock.uptimeMillis()
val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
//上一次dump时间和现在时间相隔小于60秒,则使用通知提醒。
if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
showRetainedCountNotification(
objectCount = retainedReferenceCount,
contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait)
)
scheduleRetainedObjectCheck(
reason = "previous heap dump was ${elapsedSinceLastDumpMillis}ms ago (< ${WAIT_BETWEEN_HEAP_DUMPS_MILLIS}ms)",
rescheduling = true,
delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
)
return
}
SharkLog.d { "Check for retained objects found $retainedReferenceCount objects, dumping the heap" }
dismissRetainedCountNotification()
//开始dumpHeap
dumpHeap(retainedReferenceCount, retry = true)
}
checkRetainedObjects方法的逻辑是,判定泄漏次数是否小于5次,小于则不进行dump,大于的话判定上一次dump时间和现在时间相隔是否小于60秒,小于则弹出通知提醒,大于则进行dumpHeap。
private fun dumpHeap(
retainedReferenceCount: Int,
retry: Boolean
) {
saveResourceIdNamesToMemory()
val heapDumpUptimeMillis = SystemClock.uptimeMillis()
KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
val heapDumpFile = heapDumper.dumpHeap()
if (heapDumpFile == null) {
if (retry) {
SharkLog.d { "Failed to dump heap, will retry in $WAIT_AFTER_DUMP_FAILED_MILLIS ms" }
scheduleRetainedObjectCheck(
reason = "failed to dump heap",
rescheduling = true,
delayMillis = WAIT_AFTER_DUMP_FAILED_MILLIS
)
} else {
SharkLog.d { "Failed to dump heap, will not automatically retry" }
}
showRetainedCountNotification(
objectCount = retainedReferenceCount,
contentText = application.getString(
R.string.leak_canary_notification_retained_dump_failed
)
)
return
}
lastDisplayedRetainedObjectCount = 0
lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
HeapAnalyzerService.runAnalysis(application, heapDumpFile)
}
dumpHeap方法使用AndroidHeapDumper类生成dump文件,然后通过HeapAnalyzerService分析dump文件,然后找到最短的路径展示给用户。至于生成dump和分析dump的详细代码有兴趣的可以仔细查阅源码。
到这里Leakcanary的简单逻辑就分析完了。
总结:
1.Leakcanary基于 ContentProvider完成自动初始化工作。
2.Leakcanary对对象的泄漏判定原理是基于弱引用和引用队列来完成的。当弱引用弱可达时,会立刻加入引用队列,然后通过多次调用removeWeaklyReachableObjects方法,移除即将被GC的对象,从而获取到可能泄露的对象。
3.通过checkRetainedObjects方法,检测是否真的泄漏,并且针对泄漏次数是否达到5次来判定是否进行dump。
4.通过AndroidHeapDumper类生成dump文件
5.通过HeapAnalyzerService服务分析dump文件并展现最短路径给用户。