LeakCanary 仓库 Agent 协作指南:ABI 契约、构建测试与工程规范全解读 LeakCanary 仓库 Agent 协作指南ABI 契约、构建测试与工程规范全解读【免费下载链接】leakcanaryA memory leak detection library for Android.项目地址: https://gitcode.com/gh_mirrors/le/leakcanaryLeakCanary 是一个 Android 内存泄漏检测库其底层还包含堆转储分析引擎 Shark二者均发布到 Maven Central 并被大量应用直接依赖。本文以仓库根目录 AGENTS.md 为骨架结合 build.gradle.kts、gradle/libs.versions.toml、config/hooks/pre-push 等源码与配置系统讲解该仓库的模块布局、构建与测试流程、ABI二进制接口契约管理、易踩的工程陷阱以及 Changelog 编写规范。读完本文你将掌握如何在这个多模块 Gradle 仓库中正确执行构建、静态分析与测试改动公共 API 时如何利用 ABI 校验机制让破坏性变更显式化以及哪些被刻意保留的旧版本依赖和性能冻结测试不可随意触碰。仓库全景四层架构与模块布局LeakCanary 仓库是一个典型的多模块 Gradle 工程模块按领域划分为四个组目录。AGENTS.md用一张表概括了各组职责目录内容shark/堆转储解析与分析。纯 JVM 实现除shark-android为 Android 增加特定引用读取器和匹配器外不依赖 Android。object-watcher/观察对象是否被持有retention是最底层可独立使用。leakcanary/Android 库本身以及leakcanary-app*——一个独立 UI 应用不属于库的组成部分。plumber/修复已知 Android 框架泄漏自动安装。samples/示例应用。docs/文档站内容。一个容易误解的结构细节是shark/、leakcanary/等组目录本身不包含任何代码只有具体的模块如:shark:shark、:leakcanary:leakcanary-android才承载代码。这可以从根 settings.gradle 得到印证——它只include叶子模块没有任何组目录自身被 include。全部模块共 39 个涵盖 Android 库、JVM 库、Gradle 插件、桌面应用Shark Explorer与示例应用。这种分层的核心意图在AGENTS.md开篇即被点明公共 API 与字节码是契约contract不是实现细节。因为库被海量应用依赖破坏向后兼容是最后手段。不过轻重有别leakcanary*模块是应用直接依赖的其公共 API 的兼容性最重要shark*使用面小得多只要能换来有意义的改进破坏性变更可以接受。但无论哪种情况ABI 转储api/*.api文件都是让破坏显式化而非意外发生的机制——宁可主动提出变更也不要假定它无伤大雅。构建与测试从本地验证到 CI 全流程工具链前提整个仓库用Java 17构建但全仓库目标字节码是Java 8。这两点都重要仍在 Java 8 上的下游消费者必须能使用这些制品。根 build.gradle.kts 中对除 Gradle 插件运行在 Gradle 自身 JVM 上和 Shark Explorer 桌面应用Compose Multiplatform 制品不支持 Java 8之外的所有子项目统一把sourceCompatibility/targetCompatibility与 KotlinjvmTarget设为JVM_1_8。核心命令AGENTS.md给出了四个最重要的 Gradle 命令./gradlew build # CI 执行的内容 ./gradlew :shark:shark:test # 单个模块的单元测试 ./gradlew detekt # 静态分析pre-push 钩子也会执行 ./gradlew updateKotlinAbi # 任何公共 API 变更后执行 ./gradlew siteDokka # 重新生成 docs/api其中siteDokka的输出docs/api/是git ignored、不入库的。发布流程在发布站点前才重新生成它见 docs/releasing.md所以一次公共 API 变更只需更新 ABI 转储即可。如果手动运行了siteDokka不要编辑它生成的内容——应去修源码里的 KDoc。另外根构建脚本注册了一个自定义 Dokkagfm输出格式DokkaGfmPlugin因为官方插件只自带html与javadoc格式而文档站用的是 GitHub 风格 Markdown。测试矩阵仪器化测试需要真机或模拟器且只覆盖四个模块leakcanary-android、leakcanary-android-core、leakcanary-android-instrumentation、leakcanary-android-test。CI 在每个大版本 Android 一个模拟器上运行它们从minSdk当前为 24见 gradle/libs.versions.toml一直到有系统镜像的最新 API 级别。这意味着只兼容特定 API 级别的改动会在 CI 上失败而不是在本地悄悄通过。钩子与静态分析detekt 在 pre-push 和 CI 中都运行配置位于 config/detekt-config.yml。config/hooks/pre-push 实际执行的是./gradlew detekt --no-configuration-cache发现违规就以非零状态退出并阻止推送。钩子通过根构建脚本中的installGitHooks任务自安装挂在assemble与clean任务上所以全新 clone 在首次构建后即获得钩子。installGitHooks用git rev-parse --git-common-dir解析钩子目录从而在 git worktree.git是文件而非目录下也能正确安装。ABI 即契约公共 API 变更的两套校验机制这是AGENTS.md着墨最多、也最容易被 Agent 搞错的部分。一个任务名两套实现仓库刻意让两套 ABI 校验机制共用任务名这样一条命令就能覆盖整个仓库JVM 模块使用 Kotlin Gradle 插件KGP内置的abiValidation()对应任务即文档常说的checkKotlinAbi/updateKotlinAbiAndroid 库模块KGP 尚不支持JetBrains 跟踪的 KT-83410 问题根 build.gradle.kts 中registerAndroidAbiValidationTasks()基于同一套引擎org.jetbrains.kotlin:abi-tools手工实现了等价的任务对产出完全相同的转储格式。Android 侧的实现细节值得展开AndroidAbiDumpTask收集 release 变体的 class 文件通过ScopedArtifacts钩住ScopedArtifact.CLASSES交给AbiTools.getInstance().printJvmDump输出转储AndroidAbiCheckTask用filesDiff对比参考转储与当前转储差异非空即抛GradleExceptionAndroidAbiUpdateTask则把当前转储覆盖写入api/module.api——即使模块没有公共 API 也会生成一个空文件从而让新增公共 API这件事始终可见于 diff。最关键的命令选择用 Kotlin 系别用 Legacy 系必须使用checkKotlinAbi/updateKotlinAbi绝不使用checkLegacyAbi/updateLegacyAbi。Legacy任务对的名字来自 Kotlin 官方 ABI 校验文档但它们直接来自 KGP因此只存在于 JVM 模块上——会静默跳过每一个 Android 库模块而后者正是已发布模块中的大多数。一次全绿的updateLegacyAbi只覆盖了不到一半的仓库。变更公共 API 的标准流程改动代码后运行./gradlew buildcheckKotlinAbi作为check的一部分会自动执行一旦公共 ABI 与已提交的api/*.api文件不一致就会让构建失败——这就是它存在的意义。失败时运行./gradlew updateKotlinAbi并提交变更后的api/*.api文件但提交前务必先读 diff因为一次意外的 ABI 变更正是这套机制要拦截的。仓库中共有 27 个api/*.api文件如 leakcanary/leakcanary-android-core/api/leakcanary-android-core.api它们就是公共 API 契约的权威快照。无公共 API 的豁免模块build.gradle.kts中modulesWithoutPublicApi显式列出了 12 个不对外发布、不追踪 ABI、也不进文档站的模块包括示例应用leakcanary-android-sample等、独立 UI 应用及其内部管道leakcanary-app*、Shark Explorer 桌面应用与它渲染的堆模型shark-explorer-*、Shark 测试夹具shark-test、shark-hprof-test以及以 zip 形式随 GitHub Release 分发而非作为依赖发布的 CLI 工具shark-cli。判断一个模块是否受 ABI 校验约束直接看它是否在这个列表里。那些读了源码也会踩的坑AGENTS.md明确说本文件记录的是仅靠读源码无法获知的工程约束凡能从代码推导出的内容都应写在代码里而不是这里。以下是最容易踩中的四类。1. 某些依赖版本是被刻意保留的旧版本gradle/libs.versions.toml 中compileOnly的 AndroidX 依赖被钉在 LeakCanary 支持的最低版本上这样应用无需配置 resolution strategy 就能解析到自己的更新版本。文件内的行内注释说明了哪些依赖、为什么。例如androidX-fragment钉在1.0.0而仪器化测试则用1.8.9Fragment#initState()在 1.1.0 的行为变化需要覆盖workManager钉在2.7.0、androidX-startup钉在1.0.0、okio2钉在2.2.2注释都标注了Exposed transitively, avoid increasing。不要为了消除某个警告就去升级它们——那会破坏下游应用的版本解析。2. 性能冻结测试不允许任何回归HprofRetainedHeapPerfTestshark/shark-android/src/test/java/shark/HprofRetainedHeapPerfTest.kt和HprofIOPerfTestshark/shark-android/src/test/java/shark/HprofIOPerfTest.kt冻结了精确数字——每次分析步骤读取的字节数、保留的内存数且带容差。任何改变分析分配或读取方式的改动都会让它们失败这正是目的让内存与 I/O 回归可见。遇到失败应先调查原因再决定是否调整期望值并在 PR 中说明新数字为什么是正确的。从 docs/changelog.md 的近期条目可以看到这条约束的实际威力路径查找遍历的已访问集合从按对象 id 键控的哈希表改为按对象索引的每对象一比特位图后最小堆分析内存在 4.4M 对象堆转储上从 513 MB 降到 385 MB 再降到 193 MB这些优化都伴随相应的性能数字变化被记录。3.gh pr merge --auto在这里不会等 CI仓库虽然开启了 auto-merge但main分支刻意不加保护因此没有必需的状态检查可供 auto-merge 把关。GitHub 看到一个可合并且无等待项的 PR 会立即合并退出码为零且不打印任何信息——看起来就像成功武装了 auto-merge。没有任何机制会在 CI 红着时阻止合并。等待变绿是你的职责而不是平台的。必须显式等待让退出码决定结果gh pr checks number --watch --fail-fast gh pr merge number --mergegh pr checks只有在所有检查通过后才以零退出因此是安全的关键--fail-fast让某个检查失败时立即返回而不是干等其余项。一次 CI 运行耗时约 9 到 13 分钟绝大部分时间花在模拟器矩阵上所以应以分离方式启动该命令——前台调用若在十分钟左右超时往往恰好在最后一个模拟器回报之前被杀死。4. 依赖注入运行时只出现在测试类路径gradle/libs.versions.toml 中dagger-runtime与metro-runtime被显式注释为两个依赖注入运行时只出现在 shark-explorer-core 的测试类路径且不在别处——既没有注解处理器也没有编译器插件参与构建相关约束由DependencyInjectionOwnerTest验证。Changelog 编写规范变更条目写入 docs/changelog.md 的## Unreleased小节下每条以文件顶部图例中的某个标记开头。根据变更是什么选标记而不是按体感大小拿不准时 grep 一个相近的既有条目作参考。图例定义如下标记含义⚠️破坏性变更升级可能破坏构建。API 变更或移除、最低支持版本提高、制品停止发布。行为变更代码仍可编译但 LeakCanary 的行为不同了。崩溃修复以前会崩溃现在不会了。Bug 修复LeakCanary 做错了事但没有崩溃。✨新增此前不存在的 API、制品或能力。改进原本能工作的东西现在工作得更好。新识别的泄漏库或厂商 ROM 中的新泄漏模式。两个关键提醒 在这里表示崩溃修复不是破坏性变更——与 gitmoji 惯例正好相反这正是该惯例常让人犯的错。因为某个变更感觉有影响力就选用 等于告诉读者修复了一个崩溃而实际并没有。破坏性变更用 ⚠️需要多于一条子弹时写成### Breaking change: summary标题加正文。Changelog 记录的是对 LeakCanary 消费者有意义的变化而非每个 diff 的流水账。重构、内部清理、仅测试相关的改动通常不需要条目。通用约定函数参数放不下单行时每个参数独占一行——现有代码对此保持一致且 detekt 不会提醒你。Commit 标题用祈使句描述变更例如 Keep modules without a public API off the documentation site正文在意图不明显时说明为什么。不要向已提交的代码树遗留仅测试用或未使用的代码为到达某处而搭建的脚手架应在 PR 落地前移除。测试用堆转储通过shark-hprof-test提供的dump { }DSL 程序化构建见 docs/dev-env.md而不是提交二进制 fixture绝不手工拼装 hprof 字节。需要大型真实转储时通过HotSpotDiagnosticMXBean.dumpHeap驱动真实 JVM 生成。测试统一使用 JUnit 4 与 AssertJ。仪器化测试依赖libs.assertjCore.android而非libs.assertjCore——因为 AssertJ 3.16 及更高版本无法在 API 24 上加载版本目录中的注释说明了原因见 gradle/libs.versions.toml。所以一个单元测试能用的断言在仪器化测试里可能无法编译。此外仓库还有一条值得单独了解的工程防线根构建脚本为:samples:leakcanary-android-process-sample注册了checkShrunkManifestComponents任务读取 app 的合并清单校验其中声明的每个组件在 R8 混淆后仍保留原名与无参构造器ShrunkManifestComponentsTask解析 mapping 文件实现。背景是 WorkManager/Room 自带的 keep 规则在 R8 full mode 下不再保护无参构造器LeakCanary 曾因此出现运行时崩溃——这条防线确保AGP 生成的 keep 规则已足够这一假设始终为真。分层指南Scoped guides 与 CLAUDE.md 配对子目录可以携带自己的AGENTS.md距离待编辑文件最近的指南生效规则类似.gitignore。指南应优先放在适用面最窄的位置而不是不断膨胀根文件——某个模块的怪癖应放在该模块旁边。由于 Claude Code 读取CLAUDE.md而非AGENTS.md每个AGENTS.md都会配对一个仅含AGENTS.md引用的CLAUDE.md例如仓库根目录的 CLAUDE.md。添加新分层指南时两者要一起添加。实践清单给 Agent 与贡献者的行动指南综合全文在 LeakCanary 仓库中改动代码时建议按此顺序执行修改代码./gradlew build验证构建与checkKotlinAbi公共 API 变更时运行./gradlew updateKotlinAbi审阅api/*.apidiff 后提交。推送前运行./gradlew detektpre-push 钩子会自动执行但提前发现更高效修复所有违规。改动涉及堆分析的内存/IO 时留意HprofRetainedHeapPerfTest与HprofIOPerfTest是否被冻结数字拦截若拦截调查后给出新数字的正当理由。触碰 AndroidX 依赖时先核对 gradle/libs.versions.toml 的行内注释——钉在最低版本的依赖不要随意升级。为 docs/changelog.md 添加条目时按变更是什么选取标记记住 指崩溃修复。合并 PR 前用gh pr checks number --watch --fail-fast gh pr merge number --merge显式等待 CI 变绿勿依赖gh pr merge --auto。这套流程的底层逻辑贯穿全文LeakCanary 把公共 API 与字节码是契约落实成了可执行的构建机制——ABI 转储让破坏性变更必须被显式承认性能冻结测试让资源回归无处遁形预提交钩子让静态分析成为推送的硬门槛。理解这些约定是安全地为这个被大量应用依赖的库做贡献的前提。【免费下载链接】leakcanaryA memory leak detection library for Android.项目地址: https://gitcode.com/gh_mirrors/le/leakcanary创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考