别搞错了,nonTransitiveRClass 不能解决资源冲突!

Posted TechMerger

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了别搞错了,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 Module 定义的资源
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 包。

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

这些都来源于 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

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

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

    sealed class Error(val tipId: Int) 
        class Common      : 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 的数值完全相同。

// 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;
    ...
  

    
// R.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: Bundle?) 
        ...
        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 自动迁移工具尝试更改下 R 文件的配置问题。

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

迁移后的代码:

sealed class Error(val tipId: Int) 
    class Common      : Error(com.example.recommend.R.string.common_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(com.example.common.R.string.common_error)
    ...

但这种写法真的有用吗?

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

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

这是因为资源冲突导致 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 文件构成的,不是也不能解决资源冲突。资源冲突仍然要依赖开发者对于资源的规范定义和使用!

参考资料

以上是关于别搞错了,nonTransitiveRClass 不能解决资源冲突!的主要内容,如果未能解决你的问题,请参考以下文章

如何用 Git 优雅回退代码,别搞错了!

89%对3年Java的晋级考核,根本不是熟练度,别搞错了...!

mac 下 qt 搭建 ffmpeg

C语言-14(指针)

Git Stash用法

bat隐藏后怎么呼出