终于理解~Android 模块化里的资源冲突
本文翻译自 Understanding resource conflicts in Android,原作者:Adam Campbell
⚽ 前言
作为 Android 开发者,我们常常需要去管理非常多不同的资源文件,编译时这些资源文件会被统一地收集和整合到同一个包下面。根据官方的《Configure your build》文档介绍的构建过程可以总结这个过程:
编译器会将源码文件转换成包含了二进制字节码、能运行在 Android 设备上的 DEX 文件,而其他文件则被转换成编译后资源。
APK 打包工具则会将 DEX 文件和编译后资源组合成独立的 APK 文件。
但如果资源的命名发生了碰撞、冲突,会对编译产生什么影响?
事实证明这个影响是不确定的,尤其是涉及到构建外部 Library。
本文将探究一些不同的资源冲突案例,并逐个说明怎样才能安全地命名资源。
🇦🇷 App module 内资源冲突
先来看个最简单的资源冲突的案例:同一个资源文件中出现两个命名、类型一样的资源定义,比如:
<!--strings.xml-->
<resources>
<string name="hello_world">Hello World!</string>
<string name="hello_world">Hello World!</string>
</resources>
试图去编译的话,会导致显而易见的错误提示:
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:mergeDebugResources'.
> /.../strings.xml: Error: Found item String/hello_world more than one time
类似的,另一种常见冲突是在多个文件里定义冲突的资源:
<!--strings.xml-->
<resources>
<string name="hello_world">Hello World!</string>
</resources>
<!--other_strings.xml-->
<resources>
<string name="hello_world">Hello World!</string>
</resources>
我们会收到类似的编译错误,而这次的错误将列出所有发生冲突的具体文件位置。
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:mergeDebugResources'.
> [string/hello_world] /.../other_strings.xml
[string/hello_world] /.../strings.xml: Error: Duplicate resources
Android 平台上资源的运作方式变得愈加清晰。我们需要为 App module 指定在类型、名称、设备配置等限定组合下的唯一资源。也就是说,当 App module 引用 string/hello_world
资源的时候,有且仅有一个值被解析出来。开发者们必须解决发生的资源冲突,可以选择删除那些内容重复的资源、重命名仍然需要的资源、亦或移动到其他限定条件下的资源文件。
更多关于资源和限定的信息可以参考官方的《App resources overview》 文档。
🇩🇪 Library 和 App module 的资源冲突
下面这个案例,我们将研究 Library module 定义了一个和 App module 重复的资源而引发的冲突。
<!--app/../strings.xml-->
<resources>
<string name="hello">Hello from the App!</string>
</resources>
<!--library/../strings.xml-->
<resources>
<string name="hello">Hello from the Library!</string>
</resources>
当你编译上面的代码的时候,发现竟然通过了。从我们上个章节的发现来看,我们可以推测 Android 肯定采用了一个规则,去确保在这种场景下仍能够找到一个独有的 string/hello
资源值。
根据官方的《Create an Android library》文档:
编译工具会将来自 Library module 的资源和独立的 App module 资源进行合并。如果双方均具备一个资源 ID 的话,将采用 App 的资源。
这样的话,将会对模块化的 App 开发造成什么影响?比如我们在 Library 中定义了这么一个 TextView 布局:
<!--library/../text_view.xml-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello"
xmlns:android="http://schemas.android.com/apk/res/android" />
AS 中该布局的预览是这样的。
现在我们决定将这个 TextView 导入到 App module 的布局中:
<!--app/../activity_main.xml-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:context=".MainActivity"
>
<include layout="@layout/text_view" />
</LinearLayout>
无论是 AS 中预览还是实际运行,我们可以看到下面的一个显示结果:
不仅是通过布局访问 string/hello
的 App module 会拿到 “Hello from the App!”,Library 本身拿到的也是如此。基于这个原因,我们需要警惕不要无意覆盖 Lbrary 中的资源定义。
🇧🇷 Library 之间的资源冲突
再一个案例,我们将讨论下当多个 Library 里定义了冲突的资源,会发生什么。
首先来看下如下的布局,如果这样写的话会产生什么结果?
<!--library1/../strings.xml-->
<resources>
<string name="hello">Hello from Library 1!</string>
</resources>
<!--library2/../strings.xml-->
<resources>
<string name="hello">Hello from Library 2!</string>
</resources>
<!--app/../activity_main.xml-->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello" />
string/hello
将会被显示成什么?
事实上这取决于 App build.gradle
文件里依赖这些 Library 的顺序。再次到官方的《Create an Android library》文档里找答案:
如果多个 AAR 库之间发生了冲突,依赖列表里第一个列出(在依赖关系块的顶部)的资源将会被使用。
假使 App module 有这样的依赖列表:
dependencies {
implementation project(":library1")
implementation project(":library2")
...
}
最后 string/hello
的值将会被编译成 Hello from Library 1!
。
那么如果这两个 implementation 代码调换顺序,比如 implementation project(":library2")
在前、 implementation project(":library1")
在后,资源值则会被编译成 Hello from Library 2!
。
从这种微妙的变化可以非常直观地看到,依赖顺序可以轻易地改变 App 的资源展示结果。
🇪🇸 自定义 Attributes 的资源冲突
目前为止讨论的示例都是针对 string 资源的使用,然而需要特别留意的是自定义 attributes 这种有趣的资源类型。
看下如下的 attr 定义:
<!--app/../attrs.xml-->
<resources>
<declare-styleable name="CustomStyleable">
<attr name="freeText" format="string"/>
</declare-styleable>
<declare-styleable name="CustomStyleable2">
<attr name="freeText" format="string"/>
</declare-styleable>
</resources>
大家可能都认为上面的写法能通过编译、不会报错,而事实上这种写法必将导致下面的编译错误:
Execution failed for task ':app:mergeDebugResources'.
> /.../attrs.xml: Error: Found item Attr/freeText more than one time
但如果 2 个 Library 也采用了这样的自定义 attr 写法:
<!--library1/../attrs.xml-->
<resources>
<declare-styleable name="CustomStyleable">
<attr name="freeText" format="string"/>
</declare-styleable>
</resources>
<!--library2/../attrs.xml-->
<resources>
<declare-styleable name="CustomStyleable2">
<attr name="freeText" format="string"/>
</declare-styleable>
</resources>
事实上它却能够通过编译。
然而,如果我们进一步将 Library2 的 attr 做些调整,比如改为 <attr name="freeText" format="boolean"/>
。再次编译,它竟然又失败了,而且出现了更多令人费解的错误:
* What went wrong:
Execution failed for task ':app:mergeDebugResources'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
> Android resource compilation failed
/.../library2/build/intermediates/packaged_res/debug/values/values.xml:4:5-6:25: AAPT: error: duplicate value for resource 'attr/freeText' with config ''.
/.../library2/build/intermediates/packaged_res/debug/values/values.xml:4:5-6:25: AAPT: error: resource previously defined here.
/.../app/build/intermediates/incremental/mergeDebugResources/merged.dir/values/values.xml: AAPT: error: file failed to compile.
上面错误的一个重点是: mergeDebugResources/merged.dir/values/values.xml: AAPT: error: file failed to compile
。
到底是怎么回事呢?
事实上 values.xml
的编译指的是为 App module 生成 R
类。编译期间,AAPT 会尝试在 R 类里为每个资源属性生成独一无二的值。而对于 styleable 类型里的每个自定义 attr,都会在 R 类里生成 2 个的属性值。
第一个是 styleable 命名空间属性值(位于 R.styleable
包下),第二个是全局的 attr 属性值(位于 R.attr
包下)。对于这个探讨的特殊案例,我们则遇到了全局属性值的冲突,并且由于此冲突造成存在 3 个属性值:
R.styleable.CustomStyleable_freeText
:来自 Library1,用于解析string
格式的、名称为freeText
的 attrR.styleable.CustomStyleable2_freeText
:来自 Library2,用于解析boolean
格式的、名称为freeText
的 attrR.attr.freeText
:无法被成功解析,源自我们给它赋予了来自 2 个 Library 的数值,而它们的格式不同,造成了冲突
前面能通过编译的示例是因为 Library 间同名的 R.attr.freeText
格式也相同,最终为 App module 编译到的是独一无二的数值。需要注意:每个 module 具备自己的 R 类,我们不能总是指望属性的数值在 Library 间保持一致。
再次看下官方的《Create an Android library》文档的建议:
当你构建依赖其他 Library 的 App module 时,Library module 们将会被编译成 AAR 文件再添加到 App module 中。所以,每个 Library 都会具备自己的
R
类,用 Library 的包名进行命名。所有包都会创建从 App module 和 Library module 生成的R
类,包括 App module 的包和 Library moudle 的包。
📝 结语
所以我们能从上面的这些探讨得到什么启发?
是资源编译过程的复杂和微妙吗?
确实是的。但是作为开发者,我们能为自己和团队做的是:解释清楚定义的资源想要做什么,也就是说可以加上名称前缀。我们最喜欢的官方文档《Create an Android library》也提到了这宝贵的一点:
通用的资源 ID 应当避免发生资源冲突,可以考虑使用前缀或其他一致的、对 module 来说独一无二的命名方案(抑或是整个项目都是独一无二的命名)。
根据这个建议,比较好的做法是在我们的项目和团队中建立一个模式:在 module 中的所有资源前加上它的 module 名称,例如library_help_text
。
这将带来两个好处:
大大降低了名称冲突的概率。
明确资源覆盖的意图。
比如也在 App module 中创建
library_help_text
的话,则表明开发者是有意地覆盖 Library module 中的某些定义。有的时候我们的确会想去覆盖一些其他资源,而这样的编码方式可以明确地告诉自己和团队,在编译的时候会发生预期的覆盖。
抛开内部开发不谈,至少是所有公开的资源都应该加上前缀,尤其是作为一个供应商或者开源项目去发布我们的 library。
可以往的经验来看,Google 自己的 library 也没有对所有的资源进行恰当地前缀命名。这将导致意外的副作用:依赖我们发行的 library 可能会因为命名冲突引发 App 编译失败。
Not a great look!
例如,我们可以看到 Material Design library 会给它们的颜色资源统一地添加 mtrl
的前缀。可是 styleable 下嵌套的 attribute resources 却没有使用 material
之类的前缀。
所以你会看到:假使一个 module 依赖了 Material library,同时依赖的另一个 library 中包含了与 Material library 一样名称的 attribute,那么在为这个 moudle 生成 R 类的时候,会发生冲突的可能。
🙏 鸣谢
本篇文章受到了下面文章或文档的启发和帮助:
📚 原文
作者:TechMerger
来源:juejin.cn/post/7170562275374268447