注册

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

⚽ 前言


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


Hello from the Library!


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


Hello from the App!


不仅是通过布局访问 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 类的时候,会发生冲突的可能。


作者:TechMerger
链接:https://juejin.cn/post/7170562275374268447
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册