开发者

Android nonTransitiveRClass资源冲突问题浅析

目录
  • 前言
  • 使用前后对比
    • 属性关闭
    • 属性开启
    • 开启并自动迁移
  • 能否解决资源冲突
    • 结语

      前言

      nonTransitiveRClass:非传递性 R 类的属性,在 gradle.properties 文件里使用。

      不少开发者可能听过它,但了解可能仅限于是对 R 文件做了优化,甚至以为它可以解决资源冲突!但它到底做了什么优化、能否解决资源冲突,则鲜少有机会去了解。

      本文通过该属性使用前后对比、在资源冲突场景下的表现等角度去充分解读它。

      使用前后对比

      假使我们的 Project 包含两个子 Module:Common 和 Recommend。

      其中 Common Module 的包名为 com.example.common,提供共通的资源。比如:

      <!-- common/.../strings.XML  -->
      <resources>
        <string name="common_error">发生了错误,请检查链路</string>
      </resources>

      而 Recommend Moudle 的包名为 com.example.recommend,提供其独有的资源。比如:

      <!-- recommend/.../strings.xml  -->
      <resources>
        <string name="recommend_error">没有推荐内容,稍后再试</string>
      </resources>

      当 Recommend Moudle 收到错误的时候,会根据类型展示相应的说明。

      package com.example.recommend
      sealed class Error(val tipId: Int) {
        // 来自 Common 包的资源
        class Common    : Error(R.string.common_error)
        class Recommend  : Error(R.string.recommend_error)
        // 来自 AppCompat 包的资源
        class BarCollapse : Error(R.string.abc_toolbar_collapse_description)
      }

      可以看到即便使用了不同 Module 的资源文件,R 文件的包名也无需进行区分。而这样的写法能否通过编译,其实跟 AGP 的版本、AS 的版本均有关系。

      • 2020 年 8 月发布的 AGP 4.1 将前期用于 R 文件优化的 namespacedRClass 实验性属性替换成了 nonTransitiveRClass(默认 false)。以便其 R 类仅包含库本身中声明的资源,而不包含库的依赖项中的任何资源,从而缩减相应库的 R 类大小。
      • 2022 年 01 月 18 日发布的 android Studio Bumblebee 则将新项目的该属性默认开启。

      属性关闭

      假使将 namespacedRClass 或 nonTransitiveRClass 属性指定为 false,或者没有使用这俩属性、且 AS 处于 Bumblebee 之前的版本,上述的写法都是可以通过编译的。

      原因显而易见 Recommend Module 的 R 文件包含了被依赖的 Common Module 的资源 ID。

      可话虽如此,你真的打开过这种情况下的 R 文件吗?知道它有多庞大吗?

      我们在项目根目录下搜索 R 文件的位置:

       ellisonchan@bogon AndroidTDemo % find . -name "R.*"

       ./recommend/build/intermediates/compile_r_class_jar/debug/R.jar

       ./recommend/build/intermediates/compile_symbol_list/debug/R.txt

      没有找到 R.Java,只有有同名的 txt 和 jar。可以直接打开 txt 或使用 Jar 工具查看。

      先看下 R.txt,实际上它有 *4000+ *行,太过庞大,这里只保留 Recommend Module 自身以及极少量其他被依赖的 Module 的资源定义。

      // R.txt
      // 其他被依赖的 Module 定义的资源
      int anim abc_fade_in 0x0
      int anim abc_fade_out 0x0
      int anim abc_grow_fade_in_from_bottom 0x0
      int anim abc_popup_enter 0x0
      int anim abc_popup_exit 0x0
      ...
      // 以下是 Recoomend Module 定义的资源
      int color black 0x0
      ...
      int color purple_200 0x0
      int color purple_500 0x0
      int color purple_700 0x0
      ...
      int color teal_200 0x0
      int color teal_700 0x0
      ...
      int color white 0x0
      ...
      int drawable ic_launcher_background 0x0
      int drawable ic_launcher_foreground 0x0
      ...
      int mipmap ic_launcher 0x0
      int mipmap ic_launcher_round 0x0
      ...
      int style Theme_AndroidTDemo 0x0
      ...
      int string recommend_error 0x0
      ...
      // 以下是被依赖的 Common Modu开发者_开发入门le 定义的资源
      int string common_error 0x0
      // 其他被依赖的 Module 定义的资源
      ...
      int xml standalone_badge 0x0
      int xml standalone_badge_gravity_bottom_end 0x0
      int xml standalone_badge_gravity_bottom_start 0x0
      int xml standalone_badge_gravity_top_start 0x0
      int xml standalone_badge_offset 0x0

      R.jar 的内容更多,足足 *5000+ *行,因其除了 ID 列表,还包含了各种二级资源类型 class 定义(和上面一样只列出部分内容)。

      // R.jar
      package com.example.recommend;
      public final class R {
       public static final class anim {
        public static int abc_fade_in = 0;
        public static int abc_fade_out = 0;
        public static int abc_grow_fade_in_from_bottom = 0;
        public static int abc_popup_enter = 0;
        public static int abc_popup_exit = 0;
         ...
        }
        ...
       public static final class color {
         ...
        public static int black = 0;
         ...
        public static int purple_200 = 0;
        public static int purple_500 = 0;
        public static int purple_700 = 0;
         ...
        public static int teal_200 = 0;
        public static int teal_700 = 0;
         ...
        public static int white = 0;
        }
       public static final class dimen { ... }
       public static final class drawable {
         ...
        public static int ic_launcher_background = 0;
        public static int ic_launcher_foreground = 0;
         ...
        }
       public static final class id { ... }
       public static final class integer { ... }
       public static final class interpolator { ... }
       public static final class layout { ... }
       public static final class mipmap {
        public static int ic_launcher = 0;
        public static int ic_launcher_round = 0;
        }
       public static final class plurals { ... }
       public static final class string {
         ...
        public static int common_error = 0;
         ...
        public static int recommend_error = 0;
         ...
        }
       public static final class style {
         ...
        public static int Theme_AndroidTDemo = 0;
         ...
        public static int Theme_AppCompat = 0;
         ...
       }
       public static final class styleable { ... }
       public static final class xml {
        public static int standalone_badge = 0;
        public static int standalone_badge_gravity_bottom_end = 0;
        public static int standalone_badge_gravity_bottom_start = 0;
        public static int standalone_badge_gravity_top_start = 0;
        public static int standalone_badge_offset = 0;
        }
      }

      可以看到 Recommend Module 只定义了 10 多个资源,但 R 文件却从其他 Module 导入了近 *3900+ *个资源。

      这里拎出部分资源,看看是从哪个包导进来的。

      abc_fade_in 等 anim 资源:来自于 AppCompat 包。

      Android nonTransitiveRClass资源冲突问题浅析

      standalone_badge 等 xml 资源:来自于 Material 包。

      Android nonTransitiveRClass资源冲突问题浅析

      这些都来源于 build.gradle 的 dependency。

      属性开启

      事实上这些资源中的大部分,我们都是不会使用的。早期的这种不管实际使用,而一股脑将被依赖的 Module 资源 ID 全部囊括进来的作法是不太合适的。

      当将 android.nonTransitiveRClass 属性改为 true,就不会执行上述作法,但上述的写法会发生编译错误:

      Unresolved reference: common_error

      Unresolved reference: abc_toolbar_collapse_description

      很明显,我们应当明确指定 common_error 和 abc_toolbar_collapse_description 资源的 R 文件包名才行。

      sealed class Error(val tipId: Int) {
        class Common    : Error(com.example.common.R.string.common_error)
         ...
        class BarCollapse : Error(androidx.appcompat.R.string.abc_toolbar_collapse_description)
      }

      原因很好理解,依赖包的资源 ID 没有被囊括进自己的 R 文件。新的 R.txt 也显示其仅包括本 Module 定义的资源。

      // R.txt
      int color black 0x0
      int color purple_200 0x0
      int color purple_500 0x0
      int color purple_700 0x0
      int color teal_200 0x0
      int color teal_700 0x0
      int color white 0x0
      int drawable ic_launcher_background 0x0
      int drawable ic_launcher_foreground 0x0
      int mipmap ic_launcher 0x0
      int mipmap ic_launcher_round 0x0
      int string recommend_error 0x0
      int style Theme_AndroidTDemo 0x0

      R.jar 中也是一样。

      // R.jar
      package com.example.recommend;
      public final class R {
       public static final class color {
        public static int black = 0;
        public static int purple_200 = 0;
        public static int purple_500 = 0;
        public static int purple_700 = 0;
        public static int teal_200 = 0;
        public static int teal_700 = 0;
        public static int white = 0;
        }
       public static final class drawable {
        public static int ic_launcher_background = 0;
        public static int ic_launcher_foreground = 0;
        }
       public static final class mipmap {
        public static int ic_launcher = 0;
        public static int ic_launcher_round = 0;
        }
       public static final class string {
        public static int recommend_error = 0;
        }
       public static final class style {
        public static int Theme_AndroidTDemo = 0;
        }
      }

      开启并自动迁移

      上面的示例使用其他包的资源的逻辑极少,手动修改 R 文件不繁琐。但当大型项目开启了 android.nonTransitiveRClass 属性,修改各 R 文件名称的工作量很大、还易错。

      这时候可以采用自 Android Studio Arctic Fox 版本引入的重构工具来自动完成,避免手动启用之后、自己逐步修改的麻烦。

      运行 Menu -> Refactor -> Migrate to Non-transitive R Classes

      Android nonTransitiveRClass资源冲突问题浅析

      这时候 AS 会提醒你将修改如下代码进行迁移。

      Android nonTransitiveRClass资源冲突问题浅析

      选择继续之后,可以看到引用的其他包的 R 包被自动补全了。

       sealed class Error(val tipId: Int) {

           class Common      :python Error(com.example.common.R.string.common_error)

           class Recommend   : Error(R.string.recommend_error)

           class BarCollapse : Error(androidx.appcompat.R.string.abc_toolbar_collapse_description)

       }

      能否解决资源冲突

      现在我们来探讨 android.nonTransitiveRClass 属性能否解决资源冲突的问题。

      假使我们在 Recommend Module 中也定义了名为 common_error 的资源。

      <!-- recommend/.../strings.xml  -->
      <resources>
        <string name="recommend_error">没有推荐内容,稍后再试</string>
        <string name="common_error">发生了错误,请检查推荐链路</string>
      </resources>

      对于 Recommend Module 而言,使用 common_error 资源的地方肯定会覆盖 Common Module 的重复定义,无须多言。

      而对于使用这两者的 App Module 而言,因 Module 引用顺序的不同,其可能会使用 Recommend,也可能使用 Common 的定义。即最终编译进来的只有一份定义。

      如下的 App、Common、Recommend 3 个 Module 的 R.java 文件也说明了这点,3 个 common_error 的数值完全相同。

      Android nonTransitiveRClass资源冲突问题浅析

      // R.java in App Module
      package com.example.tiramisu_demo;
      public final class R {
        ...
       public static final class string {
        public static final int common_error = 2131689515;
         ...
        }
      }
      // Randroid.java in Common Module
      package com.example.common;​
      public final class R {
        ...
       public static final class string {
        public static final int common_error = 2131689515;
         ...
        }
        ...
      }
      // R.java in Recommend Module
      package com.example.recommend;
      public final class R {
        ...
       public static final class string {
        public static final int common_error = 2131689515;
         ...
        }
        ...
      }

      在 App Module 的 Activity 类里测试下效果:

      class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: pythonBundle?) {
           ...
          val dynamicTextView: TextView = findViewById(R.id.dynamic_test)
          handleError(
            Error.Common(),
            dynamicTextView,
            this@MainActivity
           )
         }
      }
      fun handleError(
        error: Error,
        textView: TextView,
        context: Context
      ) {
        error.tipId.let { id ->
          context.getText(id).let { content ->
            textView.text = content
           }
         }
      }
      sealed class Error(val tipId: Int) {
        class Common    : Error(R.string.common_error)
        class Recommend  : Error(R.string.recommend_error)
        class BarCollapse : Error(R.string.abc_toolbar_collapse_description)
      }

      运行下可以看到展示的是 Recommend Module 定义的资源内容:

      Android nonTransitiveRClass资源冲突问题浅析

      之后,我们再使用前面提及的 android.nonTransitiveRClass 自动迁移工具尝试更改下 R 文件的配置问题。

      如下的工具迁移提醒可以看到:只能将待迁移资源的 R 迁移到目前使用来源 Module 的 R,即无法识别多个来源。

      Android nonTransitiveRClass资源冲突问题浅析

      迁移后的代码:

       sealed class Error(val tipId: Int) {

           class Common      : Error(com.example.recommend.R.string.coiXSYqLoimmon_error)

           class Recommend   : Error(com.example.recommend.R.string.recommend_error)

           class BarCollapse : Error(androidx.appcompat.R.string.abc_toolbar_collapse_description)

       }

      初步可以看到 nonTransitiveRClass 属性并不能帮你自动解决资源冲突,只是强制要求你将各 Module 的资源按其所属包名区分开来使用。

      当冲突发生的地方,你可以通过包名进行区分。

      比如让 Common Error 展示 Common Module 下的 common_error 资源。

      sealed class Error(val tipId: Int) {
        class CommonRecommend   : Error(com.example.recommend.R.string.common_error)
        class Common   : Error(comiXSYqLoi.example.common.R.string.common_error)
         ...
      }

      但这种写法真的有用吗?

      再运行下,竟发现没有任何作用,仍然展示的是 Recommend Module 中的资源。

      Android nonTransitiveRClass资源冲突问题浅析

      此刻,你可能已经领悟到:为什么用即便用包名区分了冲突的资源,但仍然没有任何作用?

      这是因为资源冲突导致 AAPT 仍然只打包了一份资源,nonTransitiveRClass 属性只是不再将 common_error 等其他被依赖的资源 ID 囊括到 App 的 R 文件中而已。

      同一份资源 ID,通过 com.example.common.R 来引用,还是 com.example.recommend.R 来引用,没有区别!

      结语

      上面的示例可以看到,没有开启 nonTransitiveRClass 的话,仅仅定义 10 多个资源的 Module 的 R 文件会激增到 4000+ 个 ID。这对编译速度、AAR / APK 体积的影响是可以预见的。

      加上模块化开发的流行,Module 的庞杂必然引发 ID 的大量重复定义,进而导致 R 文件指数膨胀。另外 App 构建的时候,会为项目的每个依赖 Module 生成一个 R.java 文件,然后将这些 R 文件和应用的其他类一起编译。

      这两个因素将极大地拖累多模块的构建效率。

      而当开启了 nonTransitiveRClass 属性,可以保证每个 Module 的 R 文件将只会包含自己声明的资源,依赖项中的资源会被排除在外。这样一来,R 文件大小将会显著减少。

      另外,AGP 会直接生成包含应用的已编译 R.jar,而不会先编译其中间的 R.java 文件。这项优化可以确保,向运行时依赖 Module 中添加新资源时,可以避免重新编译下游 Module。

      这两项变化将大大提升模块化的构建速度,并减小 AAR / APK 的体积~

      另外,我们必须认识到 nonTransitiveRClass 属性跟资源冲突没有关系,它是用来优化 R 文件构成的,不是也不能解决资源冲突。资源冲突仍然要依赖开发者对于资源的规范定义和使用!

      0

      上一篇:

      下一篇:

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      最新开发

      开发排行榜