终于理解~Android 模块化里的资源冲突
作为 Android 开发者,我们常常需要去管理非常多不同的资源文件,编译时这些资源文件会被统一地收集和整合到同一个包下面。根据官方的文档介绍的构建过程可以总结这个过程:
编译器会将源码文件转换成包含了二进制字节码、能运行在 Android 设备上的 DEX 文件,而其他文件则被转换成编译后资源。
APK 打包工具则会将 DEX 文件和编译后资源组合成独立的 APK 文件。
但如果资源的命名发生了碰撞、冲突,会对编译产生什么影响?
事实证明这个影响是不确定的,尤其是涉及到构建外部 Library。
本文将探究一些不同的资源冲突案例,并逐个说明怎样才能安全地命名资源。
先来看个最简单的资源冲突的案例:同一个资源文件中出现两个命名、类型一样的资源定义,比如:
<!--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
资源的时候,有且仅有一个值被解析出来。开发者们必须解决发生的资源冲突,可以选择删除那些内容重复的资源、重命名仍然需要的资源、亦或移动到其他限定条件下的资源文件。
更多关于资源和限定的信息可以参考官方的 文档。
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
资源值。
根据官方的文档:
编译工具会将来自 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 的顺序。再次到官方的文档里找答案:
如果多个 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 间保持一致。
再次看下官方的文档的建议:
当你构建依赖其他 Library 的 App module 时,Library module 们将会被编译成 AAR 文件再添加到 App module 中。所以,每个 Library 都会具备自己的
R
类,用 Library 的包名进行命名。所有包都会创建从 App module 和 Library module 生成的R
类,包括 App module 的包和 Library moudle 的包。