深入理解 Android 模块化里的资源冲突

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 深入理解 Android 模块化里的资源冲突

翻译自 Understanding resource conflicts in Android

前言

作为 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 中该布局的预览是这样的。

1832b220aa754cd18c504acc7686a560.png

现在我们决定将这个 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 中预览还是实际运行,我们可以看到下面的一个显示结果:

1832b220aa754cd18c504acc7686a560.png不仅是通过布局访问 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 的 attr

R.styleable.CustomStyleable2_freeText:来自 Library2,用于解析 boolean 格式的、名称为 freeText 的 attr

R.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 类的时候,会发生冲突的可能。

鸣谢

本篇文章受到了下面文章或文档的启发和帮助:


Configure your build

Create an Android library

Android Resources collision without warning!

Why Android cannot deal with Resources name conflict between Project and Library?

原文

Understanding resource conflicts in Android

相关文章
|
4月前
|
移动开发 前端开发 weex
Android项目架构设计问题之模块化后调用式通信如何解决
Android项目架构设计问题之模块化后调用式通信如何解决
18 0
|
4月前
|
监控 Java 开发工具
### 绝招揭秘!Android平台GB28181设备接入端如何实现资源占用和性能消耗的极限瘦身?
【8月更文挑战第14天】本文介绍在Android平台优化GB28181标准下设备接入的性能方法,涵盖环境搭建、SDK集成与初始化。重点讲解内存管理技巧如软引用、按需加载资源,以及通过硬件加速解码视频数据和图像缩放来减轻CPU与GPU负担。同时采用线程池异步处理视频流,确保UI流畅性。这些策略有助于提高应用效率和用户体验。
48 0
|
6月前
|
XML Java API
54. 【Android教程】图片资源:Drawable
54. 【Android教程】图片资源:Drawable
86 0
|
7月前
|
Android开发
Android源代码定制:Overlay目录定制|调试Overlay资源是否生效
Android源代码定制:Overlay目录定制|调试Overlay资源是否生效
266 0
|
Java Android开发
Android 保存资源图片到相册最新写法适用于Android10.0及以上
Android 保存资源图片到相册最新写法适用于Android10.0及以上
834 0
|
编解码 监控 前端开发
Android平台GB28181设备接入端如何降低资源占用和性能消耗
Android平台GB28181设备接入端如何降低资源占用和性能消耗?
|
Java Android开发
Android 中使用数组资源文件定义数组
Android 中使用数组资源文件定义数组
159 0
|
存储 设计模式 ARouter
现代化 Android 开发:组件化与模块化的抉择
本文为现代化 Android 开发系列文章第四篇。
262 0
|
存储 Android开发 开发者
关于安卓媒体资源变动监听(ContentResolver)应用
关于安卓媒体资源变动监听(ContentResolver)应用
293 0
|
XML 编解码 C#
.NET MAUI 安卓 UI 资源设置
本文主要介绍使用 MAUI 开发安卓应用时,如何更换和处理 UI 资源:应用名称,图标,主题配色,状态栏,闪屏。
622 0
.NET MAUI 安卓 UI 资源设置