欢迎关注我的公众号 Android高效开发,注重 Android 工程效率与开发体验,涵盖基础架构、Kotlin Multiplatform、Gradle 构建优化等话题,同时也聊聊海外工作生活,推送“二分电台” Podcast 的最新节目。
『构建指北』是探索 Android 构建相关的一系列文章,涵盖了 Gradle、Android Gradle Plugin、Kotlin Script 等工具,以及相关架构上的应用。以发现问题解决问题为出发点,传递新知提高生产效率为落脚点。
本文虽然是较早前碰到的问题,讨论的工具是 AAPT1,但其调试手段,使用的方法和最终产出的库均可适用于 AAPT2,大家可以查看 github.com/2BAB/Seal 仓库获取更多最新用法)
最近做一些 SDK 升级时,有些包引入后会有诸如此类的报错:
AndroidManifest.xml:22:9-40 Error: Attribute application@theme value=(@style/AppTheme) from AndroidManifest.xml:22:9-40 is also present at [some:libraries:version] AndroidManifest.xml:9:18-62 value=(@style/AnotherTheme). Suggestion: add 'tools:replace="android:theme"' to element at AndroidManifest.xml:18:5-65:19 to override.
这是一个很常见的错误了,照着提示做 replace 就 OK 了。但是当我加上 replace 的代码后,发现依旧报错:
Multiple entries with same key: @android:theme=REPLACE and android:theme=REPLACE.
百思不得其解,查看了一下依赖库的 AndroidManifest.xml 源码,发现它也设置了tools:replace="android:theme",而 Manifest Merger 把这个视为冲突抛了出来。
思考
如果只是跟着 官方的 Manifest Merge,这个问题恐怕无解。StackOverflow 上也有人问过这个问题,但是没有更多的解法回复。
为什么依赖库会想不开去设置 replace 属性呢?很大的一个可能是:他也碰到了他的依赖库和他的 Manifest 有冲突的情况。那么我们能做什么?我们始终还是想要把他的某些属性给替换掉的(theme/allowBackup/...),不管他是出于什么样的目的,都不能阻止我想打出包的心!
解法
通过简单的观察和源码查看,我们发现 merge 是发生在 process${variant}Manifest 这个 Task。那么就得想办法在执行这个任务之前 Precheck 一下所有依赖的 AndroidManifest.xml,然后写个脚本或者插件来清理不需要的内容。
Seal - A gradle plugin to do precheck of Android Manifest.
我写了一个简易的插件来做这件事,目前支持几个功能:
seal {
// 0. manifest 合并之前进行操作的两个例子
beforeMerge("Remove description attr for library input Manifest.")
.tag("application")
.attr("android:description")
.deleteAttr()
beforeMerge("Remove problematic replace attr for library input Manifest.")
.tag("application")
.attr("tools:replace")
.deleteAttr()
// 对合并后的 manifest 操作的完整示例(1-5)。
// 1. 这样的操作十分危险,请尽可能地指定详细的属性和值
afterMerge("Remove all uses-feature tags.")
.tag("uses-feature")
.deleteTag()
// 2. 这样的操作十分危险,请尽可能地指定详细的值
afterMerge("Remove all custom permission tags.")
.tag("permission")
.attr("android:protectionLevel")
.deleteTag()
// 3. 这是我们推荐删除 tag 的方式
afterMerge("Remove invalid service tag.")
.tag("service")
.attr("android:name")
.value("me.xx2bab.seal.sample.library.LegacyService")
.deleteTag()
// 你应该尽可能使用 "tools:remove" 或 "tools:replace" 来替代 "deleteAttr"
// 4. 删除属性和它的值
afterMerge("Remove application's allowBackup attr.")
.tag("application")
.attr("android:allowBackup")
.deleteAttr()
// 你应该尽可能使用 "tools:remove" 或 "tools:replace" 来替代 "deleteAttr"
// 5. 你也能指定值作为查询参数的一部分
// afterMerge("Remove application's allowBackup attr.")
// .tag("application")
// .attr("android:allowBackup")
// .value("true")
// .deleteAttr()
}
整体配置分为三个部分:
- 选择
beforeMerge(ruleName: String)或afterMerge(ruleName: String)作为 hook 的入口 - 通过传入
tag(name: String)attr(name: String)value(name: String)作为指定的查询参数 (目前还未支持正则表达式),请尽可能的精确以确保定位到相应的元素 - 选择
deleteTag()或deleteAttr()中的一个来指定删除的类型,需要注意的是,只有一个删除方法会被执行,不要调用超过一个deleteXXX方法
更多配置信息,请参考 Github 仓库内的说明,欢迎大家提 PR 和 ISSUE。
欢迎评论点赞,以及关注我的公众号 Android高效开发。