AGP 升级问题续集来了,不看血亏,真是骚,你真的了解 R 的

2020-12-26  本文已影响0人  Android阿南

背景

上一篇文章《记录一次 AGP 调研过程中的思考,我从一个事故搞出了一个故事!》发布后有很多小伙伴表示看不懂,本来不想写这篇的,但是恰巧最近大组有同事在做另一个技术需求时他的 R 也炸了,只是他炸的是 javac 阶段,我上篇分析的场景是 ASM 生成字节码阶段,所以我让他将他的R.java文件发我,我尝试编译并与我的字节码做对比发现了一点我以前没意识到的点,所以自己觉得上篇博文还是不够深入,AGP 升级的 R 问题还能继续挖坟考古,所以想来个续集。鉴于有人上篇没看懂,这篇就循序渐进的图文并茂下。所以接下来你先看到的都是案例说明,真正的问题和分析将会在后面统一展开;文章很长但很全面,如果你对前面案例说明不感兴趣,可以直接到后面看分析结论。

这里先提前声明下,下文讨论的变更到底是 AGP 哪个版本开始其实是没有啥意义的,所以我们会选择三个特定版本对比(为什么选这三个版本?因为我同事就是这里最低版本遇到的问题,我是在最高版本炸裂的,我们还有个中间版本,所以就这三了),除非你要兼容 hook AGP,否则只用拿当前最新版本和以前版本做差异分析搞懂原理本质就行,也不要纠结不同版本 AGP 的 task 差异,不要纠结和自己需要深入探寻问题不是很相关的点。将军赶路,不追小兔。

为了方便说明,这里交代下项目情况,我们新建了一个叫做 TestR 的 project,gradle 版本为 6.5,buildToolsVersion 均为 30.0.2,其中的主 module 为 app,app 依赖一个名为 libR 的 android 子 module,两个 module 的 gradle 依赖中均无任何依赖(包括 androidx 等),只有这样生成的 R 才是最原始干净的(否则会多出很多依赖的 R,影响我们主线分析)。主 module 及子 module 均以 java8 编译:


compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
复制代码

命令行反编译 class 的 javap 版本及 JDK 版本如下:

yandeMacBook-Pro:libr yan$ java -version
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)

yandeMacBook-Pro:libr yan$ javap -version
1.8.0_191
复制代码

下面分析全部采用控制变量法,所有环境一致,仅变更 AGP 版本号。

AGP4.1.0 版本的 R(当前最新)

gradle 构建脚本版本如下:

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    ......
    dependencies {
        classpath "com.android.tools.build:gradle:4.1.0"
    }
}
复制代码

libR 子 module 的产物分析

如下是编译后子 module 的产物结构图解:

可以看到,上面 libR 模块中的R.jar里面的R.class内容如下:

//与以前 R.java 的区别
//可以看到子 module 的 R 在子 module 中间产物的类属性都是非 final 且没有初始值的
package cn.yan.libr;

public final class R {
    private R() {
    }

    public static final class id {
        public static int test_layout; //只有定义,没再随机赋值了,依旧保持非 final

        private id() {
        }
    }
    ......
    public static final class string {
        public static int lib_test_string;

        private string() {
        }
    }
}
复制代码

app 主 module 的产物分析

接着我们再来看看 app 主模块下的情况,编译后主 module 的产物结构图解:

上图中先看看build/intermediates/compile_and_runtime_not_namespaced_r_class_jar/debug/cn/yan/libr/R.class下的 libR 的 final R class,如下:

//已知结论
//相对 lib module 下的中间产物来说,这里 lib 的 R 已经变为 final 了,且给了明确的赋值
package cn.yan.libr;

public final class R {
    private R() {
    }

    public static final class id {
        public static final int test_layout = 2130837504; //已赋值具体值

        private id() {
        }
    }
    ......
    public static final class string {
        public static final int lib_test_string = 2131099649;

        private string() {
        }
    }
}
复制代码

然后再来看看build/intermediates/compile_and_runtime_not_namespaced_r_class_jar/debug/cn/yan/testr/R.class下 app 的 final R class,如下:

//已知结论
//这里主 module 的 R 也是 final 且有明确值,此外已经合并了子 module 下的 R 属性到这个类
package cn.yan.testr;

public final class R {
    private R() {
    }
    //来自子模块cn.yan.libr.R$id.class下的属性
    public static final class id {
        public static final int test_layout = 2130837504;

        private id() {
        }
    }
    ......
    //主module和子module的各自属性合并
    public static final class string {
        public static final int app_name = 2131099648;
        public static final int lib_test_string = 2131099649;

        private string() {
        }
    }
}
复制代码

AGP3.5.0 版本的 R

gradle 构建脚本版本如下:


// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    ......
    dependencies {
        classpath "com.android.tools.build:gradle:3.5.0"
    }
}
复制代码

libR 子 module 的产物分析

如下是编译后子 module 的产物结构图解:

可以看到,上面 libR 模块中的R.jar里面的R.class内容如下:

package cn.yan.libr;

public final class R {
    private R() {
    }

    public static final class id {
        public static int test_layout;

        private id() {
        }
    }
    ......
    public static final class string {
        public static int lib_test_string;

        private string() {
        }
    }
}
复制代码

app 主 module 的产物分析

接着我们再来看看 app 主模块下的情况,编译后主 module 的产物结构图解:

我们可以看到,他与上面 AGP4.1.0 版本最大的区别就是 app module 的构建产物不一样,AGP3.5.0 版本会先生成build/generated/not_namespaced_r_class_sources/debug/r/cn/yan/libr/R.javabuild/generated/not_namespaced_r_class_sources/debug/r/cn/yan/testr/R.java两个源码 java 文件,其中都是 final static 且具有明确值的;接着对他们通过 javac 编译生成了build/intermediates/javac/debug/classes/cn/yan/libr/R.classbuild/intermediates/javac/debug/classes/cn/yan/testr/R.class两个 class 文件,记住,这里 class 是 javac 生成的。

上图中先看看build/intermediates/javac/debug/classes/cn/yan/libr/R.class下的 libR 的 final R class,如下:

package cn.yan.libr;

public final class R {
    private R() {
    }

    public static final class string {
        public static final int lib_test_string = 2131099649;

        private string() {
        }
    }
    ......
    public static final class id {
        public static final int test_layout = 2130837504;

        private id() {
        }
    }
}
复制代码

然后再来看看build/intermediates/javac/debug/classes/cn/yan/testr/R.class下 app 的 final R class,如下:

package cn.yan.testr;

public final class R {
    public R() {
    }
    //主module和子module合并属性
    public static final class string {
        public static final int app_name = 2131099648;
        public static final int lib_test_string = 2131099649;

        public string() {
        }
    }
    ......
    public static final class id {
        public static final int test_layout = 2130837504;

        public id() {
        }
    }
}
复制代码

AGP3.1.2 版本的 R

gradle 构建脚本版本如下:


// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    ......
    dependencies {
        classpath "com.android.tools.build:gradle:3.1.2"
    }
}
复制代码

libR 子 module 的产物分析

如下是编译后子 module 的产物结构图解:

可以看到,上面 libR 模块中的build/intermediates/classes/debug/cn/yan/libr/R.class内容如下:

package cn.yan.libr;

public final class R {
    public R() {
    }

    public static final class string {
        public static int lib_test_string = 2132082689;

        public string() {
        }
    }
    ......
    public static final class id {
        public static int test_layout = 2131492865;

        public id() {
        }
    }
}
复制代码

app 主 module 的产物分析

接着我们再来看看 app 主模块下的情况,编译后主 module 的产物结构图解:


我们可以看到,他与上面 AGP3.5.0 版本最大的区别就是 libR module 的构建产物不一样,AGP3.1.2 版本会在子 module 中先生成build/generated/source/r/debug/cn/yan/libr/R.java源码 java 文件,其中都是非 final 且具有明确值的;接着对他们通过 javac 编译生成了build/intermediates/classes/debug/cn/yan/libr/R.class class 文件,记住,这里子 module 的 class 也是 javac 生成的。其他其实和 AGP 3.5.0 没区别,所以不再给出主 module 的 class 文件。

看懂了吗?到此其实所有东西都没超出我们做 android 以来的认知,这里贴出来只是为了有人说看不懂之前那篇《记录一次 AGP 调研过程中的思考,我从一个事故搞出了一个故事!》文章而已,同时到这里也图文并茂的解释了我github.com/yanbober/ap…的原理。

不同版本 AGP 的 R 特点是什么?

通过上面三个版本的 AGP 产物对比可以看到,google 对于 R 构建问题一直在改进,且确实到最新版本也有一定提升。此外也应证了大家平时写代码时的一个疑问,那就是为什么我们在 libR 模块中通过 R 访问资源 test_layout 时导入的 package 都是cn.yan.libr.R,而在主 module app 中通过 R 访问子 module 资源 test_layout 时导入的 package 是cn.yan.testr.R,两种方式都访问到了子 module 的 id 为 test_layout 的资源,这就是因为主 module 会对子 module 的 R 归并与追加合并机制。往古老了说就是我上一篇最后提到的,很久以前 google 对子模块的 R 也是 static final 的,也说明了 google 一直在改进,但是 R 依旧棘手。

基于上面三个版本的分析,我们可以得出如下表结论,细节其实自己可以去看 AGP 源码,这里就不展开了,我为了偷懒直接通过调用栈来看的,本质一样。下表可以看出 google 的动作(由于本文主线是差异分析,所以不对主 module 覆盖子 module R 特性做说明,也不对R.txt清单做说明):

AGP 版本 子 module R 特点 主 module R 特点
4.1.0 直接在自己模块下通过 ASM 生成属性都是非 final 且无具体初值的 build/intermediates/compile_r_class_jar/debug/R.jar 包,没有源码和 javac 过程。 直接在主module下通过 ASM 一次生成子模块属性都是 final 且有具体初值及合并合并的属性都是 final 且有具体初值的归并产物build/intermediates/compile_and_runtime_not_namespaced_r_class_jar/debug/R.jar,没有源码和 javac 过程。
3.5.0 直接在自己模块下通过 ASM 生成属性都是非 final 且无具体初值的 build/intermediates/compile_only_not_namespaced_r_class_jar/debug/R.jar 包,没有源码和 javac 过程。 先在主module生成子模块属性都是 final 且有具体初值的 build/generated/not_namespaced_r_class_sources/debug/r/cn/yan/libr/R.java,再生成合并的属性都是 final 且有具体初值的 build/generated/not_namespaced_r_class_sources/debug/r/cn/yan/testr/R.java,接着 javac 编译生成 build/intermediates/javac/debug/classes/cn/yan/libr/R.class和build/intermediates/javac/debug/classes/cn/yan/testr/R.class并参与打包。
3.1.2 先在自己模块生成属性都是非 final 且有具体初值的 build/generated/source/r/debug/cn/yan/libr/R.java,接着 javac 编译生成build/intermediates/classes/debug/cn/yan/libr/R.class。 先在主module生成子模块属性都是 final 且有具体初值的 build/generated/source/r/debug/cn/yan/libr/R.java,再生成合并的属性都是 final 且有具体初值的 build/generated/source/r/debug/cn/yan/testr/R.java,接着 javac 编译生成build/intermediates/classes/debug/cn/yan/libr/R.class和build/intermediates/classes/debug/cn/yan/testr/R.class并参与打包。

通过上面表格搭配前面的案例截图,你基本上就能感受到几个主要版本 AGP 对于 R 的一些小动作了。可以看到,随着 AGP 的升级,R 的效率越来越高,我是这么看待的:

可以看到,上面这样的提升其实是值得的,因为我们都知道,子模块的R.java或者R.class其实最终都是没用的,某种程度可以理解成仅仅为了编译通过而已(无论是直接编译成 aar 或者 module 依赖),不会进行最终打包;只有在最后依靠主 module 生成 apk 时才重新生成参与打包的真正 static final 且具备初值的子 module R class 及合并后的主 module R class,所以官方的优化其实对于大型项目来说是有好处的。

不同版本 AGP 的 R class 字节码特点?

好了,从这里我们就要开始上次文章的续集了,也即本文背景信息中说的问题,同事用低版本 AGP 在 javac 时提示 R 炸了,我用高版本 AGP 在 ASM 时提示 R 炸了。

先说下,下面的 class 字节码分析重点依旧关注的是常量池(Constant pool),注意,下文中提到的常量池占用大小并非真的大小,均以 javap 反编译的编号个数为对比维度,依旧秉承将军赶路不追小兔的原则。

AGP4.1.0 的 R 字节码

反编译子模块产物中 ASM 生成的R.jar中非 final 且属性成员无明确初值的R$id.class为例(其他 R class 雷同):


yandeMacBook-Pro:libr yan$ javap -v R\$id.class 
Classfile /Users/yan/work/tmp/TestR/libR/build/intermediates/compile_r_class_jar/debug/cn/yan/libr/R$id.class
  Last modified 1981-1-1; size 241 bytes
  MD5 checksum 2f897e4e6d3128fb19074d53825f5711
public final class cn.yan.libr.R$id
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
//重点!!!class的常量池,这里总共占用#17个
Constant pool:
   #1 = Utf8               cn/yan/libr/R$id
   #2 = Class              #1             // cn/yan/libr/R$id
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               cn/yan/libr/R
   #6 = Class              #5             // cn/yan/libr/R
   #7 = Utf8               id
   #8 = Utf8               test_layout
   #9 = Utf8               I
  #10 = Integer            0
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = NameAndType        #11:#12        // "<init>":()V
  #14 = Methodref          #4.#13         // java/lang/Object."<init>":()V
  #15 = Utf8               ConstantValue
  #16 = Utf8               Code
  #17 = Utf8               InnerClasses
{
  public static int test_layout;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC
    ConstantValue: int 0

}
InnerClasses:
     public static final #7= #2 of #6; //id=class cn/yan/libr/R$id of class cn/yan/libr/R
复制代码

反编译主模块产物中 ASM 生成的R.jar中依赖的子模块自己的属性成员 final 且有明确初值的R$id.class为例(其他 R class 雷同):

yandeMacBook-Pro:libr yan$ javap -v R\$id.class 
Classfile /Users/yan/work/tmp/TestR/app/build/intermediates/compile_and_runtime_not_namespaced_r_class_jar/debug/cn/yan/libr/R$id.class
  Last modified 1981-1-1; size 241 bytes
  MD5 checksum 42e85bbb5479d39b2b093131204f367b
public final class cn.yan.libr.R$id
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
//重点!!!class的常量池,这里总共占用#17个
Constant pool:
   #1 = Utf8               cn/yan/libr/R$id
   #2 = Class              #1             // cn/yan/libr/R$id
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               cn/yan/libr/R
   #6 = Class              #5             // cn/yan/libr/R
   #7 = Utf8               id
   #8 = Utf8               test_layout
   #9 = Utf8               I
  #10 = Integer            2130837504
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = NameAndType        #11:#12        // "<init>":()V
  #14 = Methodref          #4.#13         // java/lang/Object."<init>":()V
  #15 = Utf8               ConstantValue
  #16 = Utf8               Code
  #17 = Utf8               InnerClasses
{
  public static final int test_layout;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 2130837504

}
InnerClasses:
     public static final #7= #2 of #6; //id=class cn/yan/libr/R$id of class cn/yan/libr/R
复制代码

反编译主模块产物中 ASM 生成的R.jar中主模块自己合并的属性成员 final 且有明确初值的R$id.class也是 17,与上面一样,所以不再给出。

可以看到,由于我们 demo 中主 module 无额外的 id 资源,所以子 module 里通过 ASM 生成的 R$id.class 的 class 常量池占用个数为 17,主 module 里也是 17。

AGP3.5.0 的 R 字节码

反编译子模块产物中 ASM 生成的R.jar中非 final 且属性成员无明确初值的R$id.class为例(其他 R class 雷同):

yandeMacBook-Pro:libr yan$ javap -v R\$id.class 
Classfile /Users/yan/work/tmp/TestR/libR/build/intermediates/compile_only_not_namespaced_r_class_jar/debug/cn/yan/libr/R$id.class
  Last modified 2020-11-8; size 241 bytes
  MD5 checksum 4b6b893cff236cdc445f392e0ce41a2b
public final class cn.yan.libr.R$id
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
//重点!!!class的常量池,这里总共占用#17个  
Constant pool:
   #1 = Utf8               cn/yan/libr/R$id
   #2 = Class              #1             // cn/yan/libr/R$id
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               cn/yan/libr/R
   #6 = Class              #5             // cn/yan/libr/R
   #7 = Utf8               id
   #8 = Utf8               test_layout
   #9 = Utf8               I
  #10 = Integer            2131427329
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = NameAndType        #11:#12        // "<init>":()V
  #14 = Methodref          #4.#13         // java/lang/Object."<init>":()V
  #15 = Utf8               ConstantValue
  #16 = Utf8               Code
  #17 = Utf8               InnerClasses
{
  public static int test_layout;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC
    ConstantValue: int 2131427329

}
InnerClasses:
     public static final #7= #2 of #6; //id=class cn/yan/libr/R$id of class cn/yan/libr/R
复制代码

反编译主模块产物中 javac 生成的依赖子模块自己的属性成员 final 且有明确初值的R$id.class为例(其他 R class 雷同):


yandeMacBook-Pro:libr yan$ javap -v R\$id.class 
Classfile /Users/yan/work/tmp/TestR/app/build/intermediates/javac/debug/classes/cn/yan/libr/R$id.class
  Last modified 2020-11-8; size 368 bytes
  MD5 checksum 18fbba9c0a1f51068cbd984810e53eac
  Compiled from "R.java"
public final class cn.yan.libr.R$id
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
//重点!!!class的常量池,这里总共占用#23个
Constant pool:
   #1 = Methodref          #3.#19         // java/lang/Object."<init>":()V
   #2 = Class              #21            // cn/yan/libr/R$id
   #3 = Class              #22            // java/lang/Object
   #4 = Utf8               test_layout
   #5 = Utf8               I
   #6 = Utf8               ConstantValue
   #7 = Integer            2130837504
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               id
  #15 = Utf8               InnerClasses
  #16 = Utf8               Lcn/yan/libr/R$id;
  #17 = Utf8               SourceFile
  #18 = Utf8               R.java
  #19 = NameAndType        #8:#9          // "<init>":()V
  #20 = Class              #23            // cn/yan/libr/R
  #21 = Utf8               cn/yan/libr/R$id
  #22 = Utf8               java/lang/Object
  #23 = Utf8               cn/yan/libr/R
{
  public static final int test_layout;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 2130837504

}
SourceFile: "R.java"
InnerClasses:
     public static final #14= #2 of #20; //id=class cn/yan/libr/R$id of class cn/yan/libr/R
复制代码

反编译主模块产物中 javac 生成的主模块自己合并的属性成员 final 且有明确初值的R$id.class也是 23,与上面一样,所以不再给出。

可以看到,由于我们 demo 中主 module 无额外的 id 资源,所以子 module 里通过 ASM 生成的 R$id.class 的 class 常量池占用个数为 17,而主 module 里是先生成R.java然后通过 javac 编译生成的R.class,所以常量池占用个数变为了 23。我们类成员属性只有一个,ASM 和 javac 的 class 就差了 6 个,至于为什么后面分析,先继续。

AGP3.1.2 的 R 字节码

反编译子模块产物中 javac 生成的非 final 且属性成员无明确初值的R$id.class为例(其他 R class 雷同):

yandeMacBook-Pro:libr yan$ javap -v R\$id.class 
Classfile /Users/yan/work/tmp/TestR/libR/build/intermediates/classes/debug/cn/yan/libr/R$id.class
  Last modified 2020-11-8; size 409 bytes
  MD5 checksum 7ff99374e393fa8059ab57413e787968
  Compiled from "R.java"
public final class cn.yan.libr.R$id
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
//重点!!!class的常量池,这里总共占用#25个
Constant pool:
   #1 = Methodref          #5.#20         // java/lang/Object."<init>":()V
   #2 = Integer            2131492865
   #3 = Fieldref           #4.#21         // cn/yan/libr/R$id.test_layout:I
   #4 = Class              #23            // cn/yan/libr/R$id
   #5 = Class              #24            // java/lang/Object
   #6 = Utf8               test_layout
   #7 = Utf8               I
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               id
  #15 = Utf8               InnerClasses
  #16 = Utf8               Lcn/yan/libr/R$id;
  #17 = Utf8               <clinit>
  #18 = Utf8               SourceFile
  #19 = Utf8               R.java
  #20 = NameAndType        #8:#9          // "<init>":()V
  #21 = NameAndType        #6:#7          // test_layout:I
  #22 = Class              #25            // cn/yan/libr/R
  #23 = Utf8               cn/yan/libr/R$id
  #24 = Utf8               java/lang/Object
  #25 = Utf8               cn/yan/libr/R
{
  public static int test_layout;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC

  public cn.yan.libr.R$id();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/yan/libr/R$id;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #2                  // int 2131492865
         2: putstatic     #3                  // Field test_layout:I
         5: return
      LineNumberTable:
        line 11: 0
}
SourceFile: "R.java"
InnerClasses:
     public static final #14= #4 of #22; //id=class cn/yan/libr/R$id of class cn/yan/libr/R
复制代码

反编译主模块产物中 javac 生成的依赖子模块自己的属性成员 final 且有明确初值的R$id.class为例(其他 R class 雷同):

yandeMacBook-Pro:libr yan$ javap -v R\$id.class 
Classfile /Users/yan/work/tmp/TestR/app/build/intermediates/classes/debug/cn/yan/libr/R$id.class
  Last modified 2020-11-8; size 368 bytes
  MD5 checksum c87e7b3284a1f78f83a2d0a8d3ee5ac8
  Compiled from "R.java"
public final class cn.yan.libr.R$id
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
//重点!!!class的常量池,这里总共占用#23个
Constant pool:
   #1 = Methodref          #3.#19         // java/lang/Object."<init>":()V
   #2 = Class              #21            // cn/yan/libr/R$id
   #3 = Class              #22            // java/lang/Object
   #4 = Utf8               test_layout
   #5 = Utf8               I
   #6 = Utf8               ConstantValue
   #7 = Integer            2130837504
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               id
  #15 = Utf8               InnerClasses
  #16 = Utf8               Lcn/yan/libr/R$id;
  #17 = Utf8               SourceFile
  #18 = Utf8               R.java
  #19 = NameAndType        #8:#9          // "<init>":()V
  #20 = Class              #23            // cn/yan/libr/R
  #21 = Utf8               cn/yan/libr/R$id
  #22 = Utf8               java/lang/Object
  #23 = Utf8               cn/yan/libr/R
{
  public static final int test_layout;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 2130837504

  public cn.yan.libr.R$id();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/yan/libr/R$id;
}
SourceFile: "R.java"
InnerClasses:
     public static final #14= #2 of #20; //id=class cn/yan/libr/R$id of class cn/yan/libr/R
复制代码

反编译主模块产物中 javac 生成的主模块自己合并的属性成员 final 且有明确初值的R$id.class也是 23,与上面一样,所以不再给出。

可以看到,由于我们 demo 中主 module 无额外的 id 资源,所以子 module 里通过 javac 生成的 R$id.class 的 class 常量池占用个数为 25,而主 module 里是先生成R.java然后通过 javac 编译生成的R.class,所以常量池占用个数变为了 23。我们类成员属性只有一个,子模块 javac 和主模块 javac 的 class 就差了 2 个,至于为什么后面分析,先继续。

到此我们先简单给下对比数据(以R$id.class为例,该 class 仅有明确定义的一个成员属性):

AGP 版本 子moduleR$id.class常量池#个数 主moduleR$id.class常量池#个数
4.1.0 ASM 生成 17 个 ASM 生成 17 个
3.5.0 ASM 生成 17 个 javac 生成 23 个
3.1.2 javac 生成 25 个 javac 生成 23 个

到此其实就已经真相了,同事低版本 AGP 在 javac task 报错R$id.java类太大,我高版本 AGP 在 ASM 合并 task 炸了的本质就是低版本 AGP 生成R.java时就已经越界了,所以这个R.java在参与 javac 时自然就被爆常量池炸了,也就是提醒类文件太大错误;高版本 AGP 没有R.java经过 javac 到R.class,而是直接 ASM 一步到位 class,所以自然在 ASM 生成 class 时爆常量池炸了。

为什么看起来一样的 R 字节码常量池大小不一样?

这里我们先来看下 AGP3.1.5 版本产物为啥差 2 个的问题,子 module 比主 module 常量池多两个是因为子 module 的 R 成员属性是非 final 的,所以其值在字节码会被转换为 static 块初始化,进而常量池多了一个 ref 常量索引,刚好 2 个。如下图对比差异分析结论:

对于 AGP3.5.0 版本 lib module ASM 生成的 R class 比 app module 生成的 R class 小 5 个其实大家应该都知道为啥吧?因为 ASM 写的好情况下生成的是“紧凑型”的 class,咱们知道 JVM 对于 class 字节码是有严格规范和部分松散规范的,javac 是完全标准化的 class 产物,相对于 ASM 的 class 来说很多时候都是比较冗余的(包括 class 大小),否则由于子 module R 为非 final,这个场景下 javac 的 class 字节码会比 final 的多,如下:

到此其实一切都真相大白了。到此为了继续说明 AGP4.1.0 的优势,我们再看一组数据结论(其实就是常量池的理解,理解到位的情况下不用看这组都行),这里对 lib module 的非 final R 产物进行对比:

资源情况 ASM 产物常量池#个数 javac 产物常量池#个数
子模块有1个@+id/xxx R$id类的常量池#17 R$id类的常量池#25
子模块有2个@+id/xxx R$id类的常量池#19 R$id类的常量池#29
子模块有3个@+id/xxx R$id类的常量池#21 R$id类的常量池#33

这里对 app 主 module 的 final R 产物进行对比:

资源情况 ASM 产物常量池#个数 javac 产物常量池#个数
子模块有1个@+id/xxx R$id类的常量池#17 R$id类的常量池#23
子模块有2个@+id/xxx R$id类的常量池#19 R$id类的常量池#25
子模块有3个@+id/xxx R$id类的常量池#21 R$id类的常量池#27

通过上面对比可以发现,无论是 static final 的成员还是非 static final 的成员场景,ASM 生成的字节码常量池占用情况总是小于 javac 占用情况。

可能的上限到底该怎么计算?

到此不得不再呼应下上篇《记录一次 AGP 调研过程中的思考,我从一个事故搞出了一个故事!》最后提到的世界末日问题,并对其进行一个扩展的计算,然后得出相对准确的末日时间。下面以 javac 和 ASM 两个维度分别计算各自的末日,虽然 javac 的末日计算看起来似乎没有啥意义(AGP4.1.0不再使用),但是作为探究还是值得算下的。

对于上面的案例我们对其字节码做进一步的分析。

通过 javac 编译的 R 的非 final 带初值属性个数为 10942 个的字节码部分信息案例:

//R的非final带初值属性个数为10942个,再多两个就无法编译成class案例
//此时常量池#为43782,还没到65536呢,为什么再多两个就炸了呢?
Constant pool:
      #1 = Methodref          #21887.#32838 // java/lang/Object."<init>":()V
      #2 = Integer            2131492865
      #3 = Fieldref           #21886.#32839 // Id.MULTI:I
      #4 = Integer            2131492866
      ......
  #43780 = NameAndType        #32830:#21889 // value_5_textview_info:I
  #43781 = Utf8               Id
  #43782 = Utf8               java/lang/Object
{
  public static int MULTI;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC
    ......
  //可以看到此时 static 代码块里面对属性赋值操作因为ldc、putstatic、ldc_w的字节偏移,此时该区域已经65525大小了,马上就炸了
  //所以每多一个非final的属性赋值,这里至少多出4个字节,很快就吃光了static代码块的上限
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #2                  // int 2131492865
         2: putstatic     #3                  // Field MULTI:I
         5: ldc           #4                  // int 2131492866
         7: putstatic     #5                  // Field NONE:I
        ......
      65519: ldc_w         #21884             // int 2131503806
      65522: putstatic     #21885             // Field value_5_textview_info:I
      65525: return
      LineNumberTable:
        line 2: 0
        ......
        line 10943: 65519
}
SourceFile: "Id.java"
复制代码

通过 javac 编译的 R 的 final 带初值属性个数为 10942 个的字节码部分信息案例:

//R的final带初值属性个数为10942个,还可以继续放很多
//此时常量池#为21898,还没到65536呢
Constant pool:
      #1 = Methodref          #3.#21896   // java/lang/Object."<init>":()V
  ......
  #21896 = NameAndType        #21890:#21891 // "<init>":()V
  #21897 = Utf8               Id
  #21898 = Utf8               java/lang/Object
{
  public static final int MULTI;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 2131492865
  ......
  //没有static静态代码快
  public Id();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
}
SourceFile: "Id.java"
复制代码

通过 ASM 生成的 R 的 final 或者非 final 带不带初值属性情况下都是没有 static 块的,均为常量池占用空间,且比 javac 还节约常量池空间,这里不再给出字节码片段,可以看上一章节分析。

关于 static 代码块中的指令占用字节数大小可以参考en.wikipedia.org/wiki/Java_b…计算。关于常量池等 javap 字节码怎么分析这里推荐一篇参考文章《恕我直言,这可能是你见过最详细的class文件结构分析》

有了上面这些背景和案例分析,到此我们就能计算到底何时就是世界末日了。

AGP版本 常规占用计算说明
4.1.0 子 module 的非 final 属性 R 通过 ASM 生成,所以不存在静态代码块赋值,所以每个属性可以按照常量池占用情况计算,一般每增多一个属性常量池标识符会增加#2。主 module 的 final 属性 R 通过 ASM 生成,所以不存在静态代码块赋值,所以每个属性可以按照常量池占用情况计算,一般每增多一个属性常量池标识符会增加#2,只是主 module 的 R 属性会算上所有子 module 的合并个数。所以无论子 module 还是主 module 一般均可以以常量池单维度预估,常量池上限就是属性的个数上限。
3.5.0 子 module 的非 final 属性 R 通过 ASM 生成,所以不存在静态代码块赋值,所以每个属性可以按照常量池占用情况计算,一般每增多一个属性常量池标识符会增加#2。主 module 的 final 属性 R 通过 javac 生成,所以不存在静态代码块赋值,所以每个属性可以按照常量池占用情况计算,一般每增多一个属性常量池标识符会增加#2,只是主 module 的 R 属性会算上所有子 module 的合并个数。所以无论子 module 还是主 module 一般均可以以常量池单维度预估,常量池上限就是属性的个数上限。
3.1.2 子 module 的非 final 属性 R 通过 javac 生成,所以存在静态代码块赋值,所以每个属性的常量池占用其实是小于静态代码块占用大小的,所以计算维度可以采用 static 代码块指令位数和来统计,一般每增多一个属性 static 块至少会多 4 个字节(具体看是 ldc_w 还是 ldc 指令)。主 module 的 final 属性 R 通过 javac 生成,所以不存在静态代码块赋值,所以每个属性可以按照常量池占用情况计算,一般每增多一个属性常量池标识符会增加#2,只是主 module 的 R 属性会算上所有子 module 的合并个数。所以子 module 要以 static 块来计算,一般一个属性增加 4 个及以上字节;主 module 一般以常量池单维度预估,常量池上限就是属性的个数上限。

到此可以发现,随着 AGP 的升级其实这里是有一定效果的。3.1.2 版本的 lib module 通过 javac 生成的 R 可以容纳的资源数是远远小于 3.5.0 版本 lib module 通过 ASM 生成的 R 可以容纳的资源数,粗略估计这一波改进 lib module 可容纳资源数至少翻倍了。

为什么 AGP 中通过 ASM 生成的 R class 是“紧凑型”?

知道这篇发布后肯定有人又会说,ASM 怎么就是“紧凑型”字节码了,所以送佛送到家,这里直接给出一个实例验证,依然采用控制变量法,我们自己写一个 java 文件 javac 生成 class,再用 ASM 生成一个同样效果的 class 文件,然后对比下。

我们编写的 java 类如下:


public class R {
    public static final class id {
        public static int test1 = 1;
        public static int test2 = 2;
    }
}
复制代码

通过 javac 编译后再通过 javap 反编译 id class 看到的字节码如下:

yandeMacBook-Pro:src yan$ javap -v R\$id.class 
Classfile /Users/yan/work/tmp/aaa/src/R$id.class
  .......
Constant pool:
   #1 = Methodref          #5.#16         // java/lang/Object."<init>":()V
   ......
  #24 = Utf8               R
{
  ......
  //静态代码块有相关指令生成
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_1
         1: putstatic     #2                  // Field test1:I
         4: iconst_2
         5: putstatic     #3                  // Field test2:I
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 4
}
......
复制代码

这种非 final 的明显比较中规中矩,常量池和 static 块均有占用。

我们再来看看 AGP4.1.0 通过 ASM 是怎么生成的,先看下com.android.tools.build:gradle:4.1.0源码的GenerateNamespacedLibraryRFilesTask.kt,如下:

abstract class GenerateNamespacedLibraryRFilesTask @Inject constructor(objects: ObjectFactory) :
    NonIncrementalTask() {
    ......
    private class TaskAction @Inject constructor(private val params: Params) : Runnable {
        override fun run() {
            ......
            //生成 R.jar
            if (params.rJarFile != null) {
                // Generate the R.jar file containing compiled R class and its' inner classes.
                exportToCompiledJava(ImmutableList.of(resources), params.rJarFile.toPath())
            }
            ......
        }
    }
    ......
}
复制代码

上面的 exportToCompiledJava 方法来自com.android.tools.build:builder:4.1.0源码BytecodeRClassWriterKt.kt,如下:

//参数传递资源符号清单和输出R.jar的路径,且资源 id 为非 final
@Throws(IOException::class)
fun exportToCompiledJava(tables: Iterable<SymbolTable>, outJar: Path, finalIds: Boolean = false) {
    //JarFlinger是一个包装工具类,用来生成 jar
    JarFlinger(outJar).use { jarCreator ->
        // NO_COMPRESSION because R.jar isn't packaged into final APK or AAR
        jarCreator.setCompressionLevel(NO_COMPRESSION)
        val mergedTables = tables.groupBy { it.tablePackage }.map { SymbolTable.merge(it.value) }
        mergedTables.forEach { table ->
            //依据资源符号清单生成编译后的 java class
            exportToCompiledJava(table, jarCreator, finalIds)
        }
    }
}
复制代码

继续往下看 exportToCompiledJava 方法实现:

@Throws(IOException::class)
fun exportToCompiledJava(
    table: SymbolTable,
    jarMerger: JarCreator,
    finalIds: Boolean = false
) {
    val resourceTypes = EnumSet.noneOf(ResourceType::class.java)
    //循环依据资源类型生成不同的字节码R内部类,譬如R$id.class
    for (resType in ResourceType.values()) {
        // 通过 ASM 生成对应的R内部类字节码 byte数组
        val bytes = generateResourceTypeClass(table, resType, finalIds) ?: continue
        resourceTypes.add(resType)
        //生成内部类类名,譬如R$id
        val innerR = internalName(table, resType)
        //把ASM字节码byte字节码数组写入class文件并加入 jar 中
        jarMerger.addEntry(innerR + SdkConstants.DOT_CLASS, bytes.inputStream())
    }
    //生成最外部的 R.class 并加入 jar 中
    // Generate and write the main R class file.
    val packageR = internalName(table, null)
    jarMerger.addEntry(
        packageR + SdkConstants.DOT_CLASS,
        generateOuterRClass(resourceTypes, packageR).inputStream())
}

//依据资源类型拼接生成类名,譬如R$id
private fun internalName(table: SymbolTable, type: ResourceType?): String {
    val className = if (type == null) "R" else "R$${type.getName()}"

    return if (table.tablePackage.isEmpty()) {
        className
    } else {
        "${table.tablePackage.replace(".", "/")}/$className"
    }
}
复制代码

上面代码的重点是 generateResourceTypeClass 方法,我们继续跟进去,如下:

//最核心的东西来了,这就是生成 R$id.class 的ASM操作
private fun generateResourceTypeClass(
    table: SymbolTable, resType: ResourceType, finalIds: Boolean): ByteArray? {
    val symbols = table.getSymbolByResourceType(resType)
    if (symbols.isEmpty()) {
        return null
    }
    val cw = ClassWriter(COMPUTE_MAXS)
    val internalName = internalName(table, resType)
    cw.visit(
            Opcodes.V1_8,
            ACC_PUBLIC + ACC_FINAL + ACC_SUPER,
            internalName, null,
            "java/lang/Object", null)

    cw.visitInnerClass(
            internalName,
            internalName(table, null),
            resType.getName(),
            ACC_PUBLIC + ACC_FINAL + ACC_STATIC)

    for (s in symbols) {
        cw.visitField(
                ACC_PUBLIC + ACC_STATIC + if (finalIds) ACC_FINAL else 0,
                s.canonicalName,
                s.javaType.desc,
                null,
                if (s is Symbol.StyleableSymbol) null else s.intValue
        )
                .visitEnd()

        if (s is Symbol.StyleableSymbol) {
            val children = s.children
            for ((i, child) in children.withIndex()) {
                cw.visitField(
                        ACC_PUBLIC + ACC_STATIC + if (finalIds) ACC_FINAL else 0,
                        "${s.canonicalName}_${canonicalizeValueResourceName(child)}",
                        "I",
                        null,
                        i)
            }
        }
    }

    // Constructor
    val init = cw.visitMethod(ACC_PRIVATE, "<init>", "()V", null, null)
    init.visitCode()
    init.visitVarInsn(ALOAD, 0)
    init.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)
    init.visitInsn(RETURN)
    init.visitMaxs(0, 0)
    init.visitEnd()

    // init method
    if (resType == ResourceType.STYLEABLE) {
        val method = Method("<clinit>", "()V")
        val clinit = GeneratorAdapter(ACC_PUBLIC.or(ACC_STATIC), method, null, null, cw)
        clinit.visitCode()
        for (s in symbols) {
            s as Symbol.StyleableSymbol
            val values = s.values
            clinit.push(values.size)
            clinit.newArray(INT_TYPE)

            for ((i, value) in values.withIndex()) {
                clinit.dup()
                clinit.push(i)
                clinit.push(value)
                clinit.arrayStore(INT_TYPE)
            }

            clinit.visitFieldInsn(PUTSTATIC, internalName, s.canonicalName, "[I")
        }
        clinit.returnValue()
        clinit.endMethod()
    }

    cw.visitEnd()

    return cw.toByteArray()
}
复制代码

你可能会说 ASM 我不熟悉啊,上面这个方法的代码我很懵逼啊,那我帮你把上面代码转成一个你看得懂的例子,如下:

//用 AGP 里面 ASM 的写法生成我们这一小节上面 javac 案例的 id class 字节码
class RDump implements Opcodes {
    public static byte[] dump() throws Exception {
        ClassWriter cw = new ClassWriter(COMPUTE_MAXS);
        String internalName = "R$id";
        cw.visit(
                Opcodes.V1_8,
                ACC_PUBLIC + ACC_FINAL + ACC_SUPER,
                internalName, null,
                "java/lang/Object", null);

        cw.visitInnerClass(
                internalName,
                "R",
                "id",
                ACC_PUBLIC + ACC_FINAL + ACC_STATIC);

        cw.visitField(ACC_PUBLIC + ACC_STATIC + 0, "test1", "I", null, 1).visitEnd();
        cw.visitField(ACC_PUBLIC + ACC_STATIC + 0, "test2", "I", null, 2).visitEnd();

        // Constructor
        MethodVisitor init = cw.visitMethod(ACC_PRIVATE, "<init>", "()V", null, null);
        init.visitCode();
        init.visitVarInsn(ALOAD, 0);
        init.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        init.visitInsn(RETURN);
        init.visitMaxs(0, 0);
        init.visitEnd();

        cw.visitEnd();

        return cw.toByteArray();
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        FileOutputStream stream = new FileOutputStream("t1.class");
        stream.write(RDump.dump());
        stream.flush();
        stream.close();
    }
}
复制代码

这货被 javap 反编译后果然即便是非 final 成员也没有 static 静态块的指令了,直接体现在常量池占用上了。

此时你可能说,这段 ASM 就是这么写的,怎么就体现出了前面你说的“紧凑型”呢?小伙子,继续往下看,我们对这一小节那个 class 通过 ASM Bytecode Outline 插件傻瓜式生成一下 ASM code,如下:

这段代码生成的 class 反编译后就和 javac 编译后的一样,非 final 在常量池和 static 块均有占坑。

这下体会到我前面说的“紧凑型”的原因了吗?其实就是 ASM 写法的问题,还是那句话,JVM Class 字节码规范是强制的,但也不是绝对强制的,具体取决于你想让 ASM 怎么生成而已。

Google 还在继续搞事情?

上一篇也说过,很久以前(Eclipse 时代)lib module 的 R 也是 final 的,不信的可以看tools.android.com/tips/non-co…,那时候 google 为了构建效率考量,对子 module 的 R 改为非 final,但主 module 的 R 依旧为 final。随着时间的迁移,上面 AGP 无论怎么升级,可以看到 google 的小动作不断,但是子 module 非 final,主 module final 的规则一直没改变。

但是最近 google 开始搞事情了,如果你的 AS 比较新(譬如 4.1 版本),你稍微留意下会发现,你主 module 下面对 R id 的 switch 写法被警告了(建议你替换为 if-else),如下:

有些人开启了 lint 的情况下使用新版 AS 编译可能还会直接 lint 报错Resource lDs will be non-final in Android Gradle Plugin version 5.0,这个问题其实是 google 的一种提前警告,可以通过如下配置暂时越过:


android {
    lintOptions {
        disable 'NonConstantResourceId'
    }
}
复制代码

如果你项目用了 butterknife 的话,你会发现,AS 也会给你一个提示,具体为Non-final resource IDs have been supported for a few years now. Apply the plugin and use R2 as detailed in the readme. Or, even better, stop using Butter Knife and use view binding!。什么鬼?butterknife 要寿终正寝了?赶紧去 butterknife 的 issues 下面搜一把,果然,鸡王已经给出了同样答复,如下:

看来官方会在 AGP5.0 版本正式要将主 module 的 R 成员属性也搞成非 final 了,官方也在持续折腾中啊,看来这一路过来都是在给 AGP5.0 版本摇旗助威啊。

总结

到此可以发现,我们上篇文章最后的设想其实是成立的,而且 google 可能也意识到了一定的问题,无论是出于包大小考虑还是出于 R 不够用考虑,或者是出于构建速度考虑,个人还是相信 google 的这次变更不是偶然,应该是出于上面某种场景的必然选择。

路还很远,我这算是因为解决棘手问题速战速决,通过 gradle task 执行顺序扫了一眼,加上上面的对比分析,大致也猜到了 AGP 这块的变更特点,同时也会发现 AGP 几乎每个版本构建中间产物多少总要发生点变化的,R 只是其中一点而已。其他的暂时没空分析,感兴趣的可以自己继续深挖下 AGP 源码或者自己构建对比分析下。

作者:工匠若水
原文链接:https://juejin.cn/post/6893121577727524871

上一篇下一篇

猜你喜欢

热点阅读