4 LeakCanary 自定义配置优化

LeakCanary 2.x 版本提供了高度灵活的配置机制,允许开发者根据项目需求调整内存泄漏检测的触发策略、资源存储路径、用户交互方式以及已知误报过滤规则。本章将详细阐述这些关键配置参数的定义、调优原理及具体实现。

4.1 四类核心配置概览

LeakCanary 的配置涉及两大类:全局默认配置和基于 AppWatcher 的实例配置。所有自定义优化均通过构建 LeakCanary.Config 对象实现。下表列出了本节要优化的四个核心领域。

配置类别关键参数/方法默认值优化目标
触发阈值retryableAnalysisTimeoutMillis30s平衡检测灵敏度与耗时,防止过慢分析阻塞队列
堆转储路径heapDumpDirectoryProvider应用缓存目录指定外部存储或云存储挂载点,避免占满内部空间
通知与静默模式screenshots, silentListener启用通知在CI/Jenkins环境完全静默,仅输出机器可读日志
排除已知泄漏knownLeaks空列表屏蔽框架/第三方库的已知误报,减少干扰

4.2 触发阈值设置

4.2.1 原理

LeakCanary 发现疑似泄漏后不会立即转储堆(Heap Dump),而是等待一个可配置的超时窗口。若在窗口内发生 GC 且对象被回收,则取消转储;否则强制执行。这个超时窗口即 retryableAnalysisTimeoutMillis。

  • 设置过短(如 5s):频繁触发堆转储,导致应用卡顿,且分析结果不稳定。
  • 设置过长(如 120s):泄漏对象在内存储存时可能已被二次回收,导致漏报。

4.2.2 实现代码

在应用入口 Application.onCreate() 中,通过自定义 LeakCanary.Config 覆盖默认值。

// Application.kt
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        if (LeakCanary.isInAnalyzerProcess(this)) {
            // 分析进程不需要重复初始化
            return
        }

        LeakCanary.config = LeakCanary.config.copy(
            // CI环境下使用更保守的超时时间(20s),留足重试时间
            retryableAnalysisTimeoutMillis = 20_000L,
            
            // 在CI流水线中,关闭截图以减少IO
            screenshots = false
        )
        
        // 务必在所有配置之后调用 install
        LeakCanary.install(this)
    }
}

4.3 堆转储路径定制

4.3.1 问题场景

默认堆转储文件存储在应用内部缓存目录 (context.cacheDir)。在 CI 容器中,该目录可能随容器销毁而丢失,且体积大小不受控制(单次高达 30-50MB)。优化目标是将堆转储文件定向至 CI 挂载的持久化卷或自定义外部目录。

4.3.2 实现 Provider

val customDumpDirectoryProvider = HeapDumpDirectoryProvider { context ->

    // 优先使用环境变量指定的路径,兼容Jenkins/K8s挂载
    val ciDumpDir = System.getenv("LEAKCANARY_DUMP_DIR")
    if (!ciDumpDir.isNullOrBlank()) {
        val file = File(ciDumpDir)
        if (!file.exists()) file.mkdirs()
        Log.i("LeakCanary", "堆转储目录切换为CI挂载路径: $ciDumpDir")
        return@HeapDumpDirectoryProvider file
    }

    // 回退:使用应用外部存储,并附带时间戳命名子目录
    val extDir = context.getExternalFilesDir("leakcanary_dumps") 
    if (extDir != null) {
        val datedDir = File(extDir, SimpleDateFormat("yyyy-MM-dd_HH", Locale.US).format(Date()))
        if (!datedDir.exists()) datedDir.mkdirs()
        return@HeapDumpDirectoryProvider datedDir
    }

    // 最终默认
    File(context.cacheDir, "heap_dumps")
}

// 在 config 中应用
LeakCanary.config = LeakCanary.config.copy(
    heapDumpDirectoryProvider = customDumpDirectoryProvider
)

4.4 通知与静默模式

4.4.1 静默模式路径

在 CI 环境中,系统级通知无效且干扰终端输出。必须切换到“静默模式”:只记录结构化日志(JSON格式),不弹出通知、不截屏、不震动。

4.4.2 配置静默监听器

val ciSilentLeakListener = object : LeakListener {
    override fun onLeakDetected(heapDump: HeapDump, result: AnalysisResult) {
        // 不发送通知,只输出可解析JSON日志
        val logEntry = buildString {
            append("LEAKCANARY_DETECTED")
            append(" | timestamp=").append(System.currentTimeMillis())
            append(" | heapDumpFile=").append(heapDump.heapDumpFile.absolutePath)
            append(" | leakTraceHash=").append(result.signature?.toHexString() ?: "null")
            append(" | retainedHeapSize=").append(result.retainedHeapBytes)
            append(" | analysisDurationMs=").append(result.analysisDurationMs)
        }
        Log.i("LeakCanaryCI", logEntry)
    }

    override fun onLeakNotDetected(heapDump: HeapDump) {
        // 静默忽略未检测到泄漏的场景
        Log.i("LeakCanaryCI", "LEAKCANARY_NOT_DETECTED | file=${heapDump.heapDumpFile.name}")
    }
}

// 集成进Config
LeakCanary.config = LeakCanary.config.copy(
    // 关闭系统通知
    screenshots = false,
    // 替换默认Listener为静默版本
    leakListeners = listOf(ciSilentLeakListener)
)

4.5 排除已知泄漏

4.5.1 已知泄漏对象

Android 框架和主流第三方库(如 Glide、OkHttp、Retrofit)存在一些已知的非问题泄漏或无法修复的泄漏。LeakCanary 允许通过 KnownLeak 对象将其加入白名单,从而不在报告中标记。

4.5.2 排除列表配置

val knownLeaks = listOf(
    KnownLeak(
        knownLeak = "已知Android系统单例泄漏",
        referenceMatchers = listOf(
            ReferenceMatcher(
                referenceMatchers = listOf(
                    IsInstanceOfMatcher("android.app.ActivityThread\$ActivityClientRecord")
                ),
                pattern = ReferenceMatchPattern.EXACT
            )
        ),
        comment = "Android P及以下版本系统bug,已由AOSP标记为不会修复。"
    ),
    KnownLeak(
        knownLeak = "Glide Bitmap复用池", 
        referenceMatchers = listOf(
            ReferenceMatcher(
                referenceMatchers = listOf(
                    IsInstanceOfMatcher("com.bumptech.glide.load.engine.BitmapPoolAdapter")
                ),
                pattern = ReferenceMatchPattern.ANY
            )
        ),
        comment = "Glide内部内存复用机制,非真正泄漏。"
    )
)

// 写入 config
LeakCanary.config = LeakCanary.config.copy(
    // 也可以从 assets 文件加载,或通过 CI 配置文件动态注入
    knownLeaks = knownLeaks
)

4.6 自定义配置优化完整流程图

下图展示了 LeakCanary 从检测到配置生效的完整决策流,红色分支对应本章所优化的路径。

LeakCanary 自定义配置优化流程 应用启动 & LeakCanary.install() 读取 LeakCanary.Config 触发阈值 | 堆转储路径 | 静默模式 | 排除列表 监听 Activity/Fragment onDestroy 对象未被 GC 回收 (泄漏怀疑) 超时 窗口等待? 窗口内 GC回收 →取消 超时触发 堆转储 (Heap Dump) 自定义路径重定向 → CI挂载目录 分析结果 (排除列表过滤) 静默输出结构日志 | 已知泄漏已排除 图 4-1 配置优化流程 泄漏怀疑/堆转储路径 GC回收/正常结束

4.7 常见错误与调试

常见错误 1:在 onCreate() 中修改 LeakCanary.Config 后忘记调用 LeakCanary.install(),导致配置不生效。
解决方法:始终将 LeakCanary.install(this) 作为配置修改后的最后一行。
调试提示:在 CI 日志中 grep LEAKCANARY_DETECTED 可快速定位泄漏事件。若希望强制触发一次堆转储测试配置是否生效,可在测试用例中模拟内存泄漏:
// 测试代码片段
val leakedList = mutableListOf<Activity>()
val act = Robolectric.buildActivity(MyActivity::class.java).create().get()
leakedList.add(act)
// 手动触发 GC 并等待 LeakCanary 检测
Runtime.getRuntime().gc()
Thread.sleep(30_000)
然后检查自定义路径下是否生成 .hprof 文件。
性能优化建议:
- 将 retryableAnalysisTimeoutMillis 设置为 20s-30s 之间,既避免频繁转储,又保证分析时效。
- 在 CI 配置中始终设置 screenshots = false,可减少约 150-300ms 的 IO 开销。
- 排除列表应定期审计,废弃的已知泄漏条目应及时移除,防止真正泄漏被掩盖。