序、慢慢来才是最快的方法。
LeakCanary是Square的开源库,通过弱引用方式侦查Activity或Fragment对象的生命周期,若发现内存泄漏自动 dump Hprof文件,通过HAHA库得到泄露的最短路径,最后通过Notification展示。
简单说就是在在Activity对象onDestory的时候,新建一个WeakReference对象指向Activity对象,如果Activity对象被垃圾回收的话,WeakReference对象就会进入引用序列的ReferenceQueue。
所以我们只需要在Activity对象OnDestory之后去查看ReferenceQueue序列是否有该WeakReference对象即可。
第一次观察是Activity的onDestory5秒后,如果发现ReferenceQueue对来还没有WeakReference对象,就进入第二次观察,如果有了,就证明没有泄漏,第二次观察跟第一次观察相比区别在于会先进行垃圾回收,在进行ReferenceQueue序列的观察。
LeakCanary 通过以下 2 点实现内存泄漏监控:
- // Java 语法
- LeakCanary.Config config = LeakCanary.getConfig().newBuilder()
- .retainedVisibleThreshold(3)
- .build();
- LeakCanary.setConfig(config);
以下用一个表格总结 LeakCanary 主要的配置项:

使用快手 Koom 加快 Dump 速度。
eakCanary 默认的 Java Heap Dump 使用的是 Debug.dumpHprofData() ,在 Dump 的过程中会有较长时间的应用冻结时间。 快手技术团队在开源框架 Koom 中提出了优化方案:利用 Copy-on-Write 思想,fork 子进程再进行 Heap Dump 操作。
LeakCanary 配置项可以修改 Heap Dump 执行器,示例程序如下:
- // 依赖:
- debugImplementation "com.kuaishou.koom:koom-java-leak:2.2.0"
-
- // 使用默认配置初始化 Koom
- DefaultInitTask.init(application)
- // 自定义 LeakCanary 配置
- LeakCanary.config = LeakCanary.config.copy(
- // 自定义 Heap Dump 执行器
- heapDumper = {
- ForkJvmHeapDumper.getInstance().dump(it.absolutePath)
- }
- )
旧版本的 LeakCanary 需要在 Application 中调用相关初始化 API,而在 LeakCanary v2 版本中却不再需要手动初始化,为什么呢?—— 这是因为 LeakCanary 利用了 ContentProvider 的初始化机制来间接调用初始化 API。
ContentProvider 的常规用法是提供内容服务,而另一个特殊的用法是提供无侵入的初始化机制,这在第三方库中很常见,Jetpack 中提供的轻量级初始化框架 App Startup 也是基于 ContentProvider 的方案。
- internal class MainProcessAppWatcherInstaller : ContentProvider() {
- override fun onCreate(): Boolean {
- // 初始化 LeakCanary
- val application = context!!.applicationContext as Application
- AppWatcher.manualInstall(application)
- return true
- }
- ...
- }
LeakCanary 的初始化工程可以概括为 2 项内容:
- fun manualInstall(
- application: Application,
- retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
- watchersToInstall: List
= appDefaultWatchers(application) - ) {
- checkMainThread()
- ...
- // 初始化 InternalLeakCanary 内部引擎 (已简化为等价代码,后文会提到)
- InternalLeakCanary(application)
- // 注册五种 Android 泄漏场景的监控 Hook 点
- watchersToInstall.forEach {
- it.install()
- }
- }
-
- fun appDefaultWatchers(
- application: Application,
- reachabilityWatcher: ReachabilityWatcher = objectWatcher
- ): List
{ - // 对应 5 种 Android 泄漏场景(后文具体分析)
- return listOf(
- ActivityWatcher(application, reachabilityWatcher),
- FragmentAndViewModelWatcher(application, reachabilityWatcher),
- RootViewWatcher(reachabilityWatcher),
- ServiceWatcher(reachabilityWatcher)
- )
- }
在以上步骤中,当对象的使用生命周期结束后,会交给 ObjectWatcher 监控,现在我们来具体看下它是怎么判断对象发生泄漏的。主要逻辑概括为 3 步:
watchedObject 创建一个 KeyedWeakReference 弱引用,并存储到 retainedUptimeMillis 字段以标记为泄漏;onObjectRetained 告知 LeakCanary 内部发生新的内存泄漏。- val objectWatcher = ObjectWatcher(
- // lambda 表达式获取当前系统时间
- clock = { SystemClock.uptimeMillis() },
- // lambda 表达式实现 Executor SAM 接口
- checkRetainedExecutor = {
- mainHandler.postDelayed(it, retainedDelayMillis)
- },
- // lambda 表达式获取监控开关
- isEnabled = { true }
- )
-
- class ObjectWatcher constructor(
- private val clock: Clock,
- private val checkRetainedExecutor: Executor,
- private val isEnabled: () -> Boolean = { true }
- ) : ReachabilityWatcher {
-
- if (!isEnabled()) {
- // 监控开关
- return
- }
-
- // 被监控的对象映射表
- private val watchedObjects = mutableMapOf
() -
- // KeyedWeakReference 关联的引用队列,用于判断对象是否泄漏
- private val queue = ReferenceQueue
() -
- // 1. 为 watchedObject 对象增加监控
- @Synchronized
- override fun expectWeaklyReachable(
- watchedObject: Any,
- description: String
- ) {
- // 1.1 移除 watchedObjects 中未泄漏的引用对象
- removeWeaklyReachableObjects()
- // 1.2 新建一个 KeyedWeakReference 引用对象
- val key = UUID.randomUUID().toString()
- val watchUptimeMillis = clock.uptimeMillis()
- watchedObjects[key] = KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
- // 2. 五秒后检查引用对象是否出现在引用队列中,否则判定发生泄漏
- // checkRetainedExecutor 相当于 postDelay 五秒后执行 moveToRetained() 方法
- checkRetainedExecutor.execute {
- moveToRetained(key)
- }
- }
-
- // 2. 五秒后检查引用对象是否出现在引用队列中,否则说明发生泄漏
- @Synchronized
- private fun moveToRetained(key: String) {
- // 2.1 移除 watchedObjects 中未泄漏的引用对象
- removeWeaklyReachableObjects()
- // 2.2 依然存在的引用对象被判定发生泄漏
- val retainedRef = watchedObjects[key]
- if (retainedRef != null) {
- retainedRef.retainedUptimeMillis = clock.uptimeMillis()
- // 3. 回调通知 LeakCanary 内部处理
- onObjectRetainedListeners.forEach { it.onObjectRetained() }
- }
- }
-
- // 移除未泄漏对象对应的 KeyedWeakReference
- private fun removeWeaklyReachableObjects() {
- var ref: KeyedWeakReference?
- do {
- ref = queue.poll() as KeyedWeakReference?
- if (ref != null) {
- // KeyedWeakReference 出现在引用队列中,说明未发生泄漏
- watchedObjects.remove(ref.key)
- }
- } while (ref != null)
- }
-
- // 4. Heap Dump 后移除所有监控时间早于 heapDumpUptimeMillis 的引用对象
- @Synchronized
- fun clearObjectsWatchedBefore(heapDumpUptimeMillis: Long) {
- val weakRefsToRemove = watchedObjects.filter { it.value.watchUptimeMillis <= heapDumpUptimeMillis }
- weakRefsToRemove.values.forEach { it.clear() }
- watchedObjects.keys.removeAll(weakRefsToRemove.keys)
- }
-
- // 获取是否有内存泄漏对象
- val hasRetainedObjects: Boolean
- @Synchronized get() {
- // 移除 watchedObjects 中未泄漏的引用对象
- removeWeaklyReachableObjects()
- return watchedObjects.any { it.value.retainedUptimeMillis != -1L }
- }
-
- // 获取内存泄漏对象计数
- val retainedObjectCount: Int
- @Synchronized get() {
- // 移除 watchedObjects 中未泄漏的引用对象
- removeWeaklyReachableObjects()
- return watchedObjects.count { it.value.retainedUptimeMillis != -1L }
- }
- }
-
问题8:LeakCanary 发现泄漏对象后就会触发分析吗?
ObjectWatcher 判定被监控对象发生泄漏后,会通过接口方法 OnObjectRetainedListener#onObjectRetained() 回调到 LeakCanary 内部的管理器 InternalLeakCanary 处理(在前文 AppWatcher 初始化中提到过)。LeakCanary 不会每次发现内存泄漏对象都进行分析工作,而会进行两个拦截:
在前面的工作中,LeakCanary 已经成功生成 .hprof 堆快照文件,并且发送了一个 LeakCanary 内部事件 HeapDump。那么这个事件在哪里被消费的呢?
一步步跟踪代码可以看到 LeakCanary 的配置项中设置了多个事件消费者 EventListener,其中与 HeapDump 事件有关的是 when{} 代码块中三个消费者。不过,这三个消费者并不是并存的,而是会根据 App 当前的依赖项而选择最优的执行策略:
在前面的分析中,我们已经知道 LeakCanary 是通过子线程或者子进程执行 AndroidDebugHeapAnalyzer.runAnalysisBlocking 方法来分析堆快照的,并在分析过程中和分析完成后发送回调事件。
现在我们来阅读 LeakCanary 的堆快照分析过程:
AndroidDebugHeapAnalyzer.kt
- fun runAnalysisBlocking(
- heapDumped: HeapDump,
- isCanceled: () -> Boolean = { false },
- progressEventListener: (HeapAnalysisProgress) -> Unit
- ): HeapAnalysisDone<*> {
- ...
- // 1. .hprof 文件
- val heapDumpFile = heapDumped.file
- // 2. 分析堆快照
- val heapAnalysis = analyzeHeap(heapDumpFile, progressListener, isCanceled)
- val analysisDoneEvent = ScopedLeaksDb.writableDatabase(application) { db ->
- // 3. 将分析报告持久化到 DB
- val id = HeapAnalysisTable.insert(db, heapAnalysis)
- // 4. 发送分析完成事件(返回到上一级进行发送:InternalLeakCanary.sendEvent(doneEvent))
- val showIntent = LeakActivity.createSuccessIntent(application, id)
- val leakSignatures = fullHeapAnalysis.allLeaks.map { it.signature }.toSet()
- val leakSignatureStatuses = LeakTable.retrieveLeakReadStatuses(db, leakSignatures)
- val unreadLeakSignatures = leakSignatureStatuses.filter { (_, read) -> !read}.keys.toSet()
- HeapAnalysisSucceeded(heapDumped.uniqueId, fullHeapAnalysis, unreadLeakSignatures ,showIntent)
- }
- return analysisDoneEvent
- }
-
- private fun analyzeHeap(
- heapDumpFile: File,
- progressListener: OnAnalysisProgressListener,
- isCanceled: () -> Boolean
- ): HeapAnalysis {
- ...
- // Shark 堆快照分析器
- val heapAnalyzer = HeapAnalyzer(progressListener)
- ...
- // 构建对象图信息
- val sourceProvider = ConstantMemoryMetricsDualSourceProvider(ThrowingCancelableFileSourceProvider(heapDumpFile)
- val graph = sourceProvider.openHeapGraph(proguardMapping = proguardMappingReader?.readProguardMapping())
- ...
- // 开始分析
- heapAnalyzer.analyze(
- heapDumpFile = heapDumpFile,
- graph = graph,
- leakingObjectFinder = config.leakingObjectFinder, // 默认是 KeyedWeakReferenceFinder
- referenceMatchers = config.referenceMatchers, // 默认是 AndroidReferenceMatchers
- computeRetainedHeapSize = config.computeRetainedHeapSize, // 默认是 true
- objectInspectors = config.objectInspectors, // 默认是 AndroidObjectInspectors
- metadataExtractor = config.metadataExtractor // 默认是 AndroidMetadataExtractor
- )
- }
-
-
可以看到,堆快照分析最终是交给 Shark 中的 HeapAnalizer 完成的,核心流程是:
Android 开源库 #7 为什么各大厂自研的内存泄漏检测框架都要参考 LeakCanary?因为它是真强啊!