AGP 升级问题续集来了,不看血亏,真是骚,你真的了解 R 的 Class 常量池?,花了19998买的学习教程

在这里插入图片描述

可以看到,上面 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 构建脚本版本如下:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题 未经允许严禁转载 https://blog.csdn.net/yanbober】

// 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 调研过程中的思考,我从一个事故搞出了一个故事!》文章而已,同时到这里也图文并茂的解释了我https://github.com/yanbober/app-tiny-R-gradle-plugin的原理。

不同版本 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 的效率越来越高,我是这么看待的:

  • 3.1.2 版本主、子模块都要先生成R.java,且主、子的 R 类成员属性无论 final 还是非 final 都具有明确的初值,然后通过 javac 编译生成对应 class。

  • 进化到 3.5.0 版本,子模块不再需要R.java,一步生成了R.jar,且子模块的 R 成员属性都是非 final 且变为没有明确初值了。这看起来很小的一步改进,其实提升了一点编译速度和优化了一些问题,反正都是生成文件,原来生成 java 再生成 class,现在 ASM 一步到位 class。

  • 再进化到 4.1.0 版本,主子模块都不再生成R.java,除了具备 3.5.0 的优点外还对主 module 也进行了提升。

可以看到,上面这样的提升其实是值得的,因为我们都知道,子模块的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 雷同):

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题 未经允许严禁转载 https://blog.csdn.net/yanbober】

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

#12 = Utf8 ()V

#13 = NameAndType #11:#12 // “”😦)V

#14 = Methodref #4.#13 // java/lang/Object.“”😦)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

#12 = Utf8 ()V

#13 = NameAndType #11:#12 // “”😦)V

#14 = Methodref #4.#13 // java/lang/Object.“”😦)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

#12 = Utf8 ()V

#13 = NameAndType #11:#12 // “”😦)V

#14 = Methodref #4.#13 // java/lang/Object.“”😦)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 雷同):

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题 未经允许严禁转载 https://blog.csdn.net/yanbober】

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.“”😦)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

#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 // “”😦)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.“”😦)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

#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

#18 = Utf8 SourceFile

#19 = Utf8 R.java

#20 = NameAndType #8:#9 // “”😦)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.“”😦)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.“”😦)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

#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 // “”😦)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.“”😦)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.“”😦)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.“”😦)V

#21896 = NameAndType #21890:#21891 // “”😦)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.“”😦)V

4: return

LineNumberTable:

line 1: 0

}

SourceFile: “Id.java”

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

关于 static 代码块中的指令占用字节数大小可以参考https://en.wikipedia.org/wiki/Java_bytecode_instruction_listings计算。关于常量池等 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 类如下:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题 未经允许严禁转载 https://blog.csdn.net/yanbober】

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.“”😦)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, 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)

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

相信它会给大家带来很多收获:

img

当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

  • 无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,这四个字就是我的建议!!!
  • 我希望每一个努力生活的IT工程师,都会得到自己想要的,因为我们很辛苦,我们应得的。

当我们在抱怨环境,抱怨怀才不遇的时候,没有别的原因,一定是你做的还不够好!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

bolTable.merge(it.value) }

mergedTables.forEach { table ->

//依据资源符号清单生成编译后的 java class

exportToCompiledJava(table, jarCreator, finalIds)

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-h80FB2Nb-1712330774206)]

[外链图片转存中…(img-VEhugiXg-1712330774207)]

[外链图片转存中…(img-guWt6tOS-1712330774207)]

[外链图片转存中…(img-LIxghhpc-1712330774207)]

[外链图片转存中…(img-rwkpPYTE-1712330774208)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

相信它会给大家带来很多收获:

[外链图片转存中…(img-vCh057xS-1712330774208)]

当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

  • 无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,这四个字就是我的建议!!!
  • 我希望每一个努力生活的IT工程师,都会得到自己想要的,因为我们很辛苦,我们应得的。

当我们在抱怨环境,抱怨怀才不遇的时候,没有别的原因,一定是你做的还不够好!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 18
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值