Android Gradle 构建指北 #4 AAR 和 tools:replace 冲突

975 阅读3分钟

欢迎关注我的公众号 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()

}

整体配置分为三个部分:

  1. 选择 beforeMerge(ruleName: String)afterMerge(ruleName: String) 作为 hook 的入口
  2. 通过传入 tag(name: String) attr(name: String) value(name: String) 作为指定的查询参数 (目前还未支持正则表达式),请尽可能的精确以确保定位到相应的元素
  3. 选择 deleteTag()deleteAttr() 中的一个来指定删除的类型,需要注意的是,只有一个删除方法会被执行,不要调用超过一个 deleteXXX 方法

更多配置信息,请参考 Github 仓库内的说明,欢迎大家提 PR 和 ISSUE。

欢迎评论点赞,以及关注我的公众号 Android高效开发