本文正在参加「金石方案 . 分割6万现金大奖」

本文翻译自 Understanding resource conflicts in Android,原作者:Adam Campbell

⚽ 前言

作为 Android 开发者,咱们常常需求去办理十分多不同的资源文件,编译时这些资源文件会被一致地收集和整合到同一个包下面。依据官方的《Configure your build》文档介绍的构建进程能够总结这个进程:

  1. 编译器会将源码文件转换成包含了二进制字节码、能运行在 Android 设备上的 DEX 文件,而其他文件则被转换成编译后资源。
  2. 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 中该布局的预览是这样的。

终于理解~Android 模块化里的资源冲突

现在咱们决定将这个 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 中预览仍是实践运行,咱们能够看到下面的一个显现成果:

终于理解~Android 模块化里的资源冲突

不仅是经过布局访问 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

这将带来两个好处:

  1. 大大降低了称号抵触的概率。

  2. 清晰资源掩盖的意图。

    比方也在 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