背景
看过我博客的老铁应该知道,我在 18 年五月写过一个小 gradle 插件https://github.com/yanbober/app-tiny-R-gradle-plugin,其作用就是将 app 生成的 R 常量进行内联操作。对,就是前不久很火的滴滴 booster 和字节跳动 ByteX 提供的 R 资源 inline 原理。
这两天因为项目要升级适配 AGP4.1.0 版本,顺手要调研 AGP 4.1.0 构建对子 module 及合成最终 app 的 intermediates 产物 R 变化问题。这个过程中却意外发现了一个有趣且有深度的事情,细思极恐,越想越有趣。好的事故往往都能成为好的故事。
先卖个关子
我们都知道,Android App 构建过程中 aapt2 会将资源生成对应R.class
文件(AGP4.1.0 中间产物直接成为 jar,位于compile_and_runtime_not_namespaced_r_class_jar
目录下),然后最终合并打包到 dex 中,这块不清楚的可以研究下我背景信息里提到的之前做的小项目。
现在我有几个灵魂拷问想问你:
- 资源生成的 R 文件格式是怎么样的?不同 module 下又有什么区别?(答不上的去看我那个小项目的 REDME 吧)
- 使用官方 multidex 方案情况下还会存在 method 或者 field 超过 65535 的情况吗?本质原因是为什么?
- App 资源个数(包括 string 个数等)是否存在上限?为什么?
- 我自己编写了一个 field 超过 65535 的类会有问题吗?能在官方 multidex 场景下使用吗?
上面这四个灵魂拷问你能深入回答下吗?不能的话就请继续往下看,带你玩波有趣的东西。
还原现场
上哪去搞那么多资源能一把搞炸 65535 个 field 呢?懒惰的我来波骚操作,打开我的 IDE,新建一个createR.sh
文件,内容如下(不要在意用的原始 echo,因为懒惰,能用就行):
#!/bin/bash
# encoding=utf-8
# 【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题 未经允许严禁转载 https://blog.csdn.net/yanbober】echo "start generate string_r.xml";file_name="string_r.xml";echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>" >> $file_name;
echo "<resources>" >> $file_name;for((i=1;i<=65536;i++));
doecho "<string name=\"public_r_$i\">TEST-$i</string>" >> $file_name;
doneecho "</resources>" >> $file_name;
echo "generate string_r.xml success!";
保存,敲下回车,执行一把等待中,去厕所带薪拉屎一会,回来文件 OK 了。内容如下,总共 65536 个 string 资源:
为了一次暴露所有问题,直接把这个资源文件扔到主模块的 values 下吧,接着点击一把 Android Studio 的运行,真相了:
上面报错可能出乎你意料了吧?为什么会出现这个错误呢?从 task 执行顺序可以确认,此时还未执行 javac 操作,还在进行 aapt2 的处理,资源合并 task 时抛出了异常,这个资源合并其实会做很多事,其中一个重要的事情就是通过 ASM 生成合并后的 R 文件。
你可能会问,哪里看出来是通过 ASM 生成的?我说我从 AGP 下载时的依赖看到的你信吗?其实很容易验证,你在执行构建时加上-s
就行了,这样出错时会有详细调用栈,你能清晰的看到调用关系是 ASM 在生成字节码。
Class too large 是个什么鬼?你是不是一上来也觉得是类似 AGP 构建时对 multidex 的判断那样,在 AGP 源码里做了一个判断(官方埋雷?)。明确告诉你,不是的,不信你去 AGP 源码搜下,啥也搜不到。那它到底是咋回事呢?
隐秘的真相
现在我们一步一步来揭盖 Class too large 是什么!在执行构建时我们加上-s
可以看到如下堆栈:
这货是在 task 使用 ASM 生成字节码时报的错,所以如上图,直接去 ASM 里面搜一下,果然搜到了哈。ASM 为什么要限制不能超过 0xFFFF 个呢?其实答案很明显了,如果你对 JVM 基础不熟悉的话,不妨继续往下看,我们看下这段 ASM 源码的注释:
//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题 未经允许严禁转载 https://blog.csdn.net/yanbober】package org.objectweb.asm;/*** A {@link ClassVisitor} that generates classes in bytecode form. More* precisely this visitor generates a byte array conforming to the Java class* file format. It can be used alone, to generate a Java class "from scratch",* or with one or more {@link ClassReader ClassReader} and adapter class visitor* to generate a modified class from one or more existing Java classes.* * @author Eric Bruneton*/
public class ClassWriter extends ClassVisitor {/*** Index of the next item to be added in the constant pool.*/int index;/*** Returns the bytecode of the class that was build with this class writer.* * @return the bytecode of the class that was build with this class writer.*/public byte[] toByteArray() {if (index > 0xFFFF) {throw new RuntimeException("Class file too large!");}......}
}
可以看到,index 有个关键的注释Index of the next item to be added in the constant pool.
,能 get 到问题原因了吗?constant pool
啊,哈哈,这特么就真相了。
还不懂?那去补补 JVM 基础吧,周老师的神书前几章就足矣!
这里给两个直达链接简单科普常量池的:
《Java Class文件结构:常量池》
《Java Class文件中的常量池》
灵魂拷问的答案
到此我们整明白了来龙去脉和问题的本质,那我们现在来深度回答下一开始卖关子的问题。
-
资源生成的 R 文件格式是怎么样的?不同 module 下又有什么区别?
去看 https://github.com/yanbober/app-tiny-R-gradle-plugin REDME 吧,很深度解析了。
-
使用官方 multidex 方案情况下还会存在 method 或者 field 超过 65535 的情况吗?本质原因是为什么?
很明显,使用官方 multidex 方案情况下不会出现 method 或者 field 超过 65535 的情况,因为 JVM 这一层就已经限制了玩法规则。本质就是上面隐秘的真相。
-
App 资源个数(包括 string 个数等)是否存在上限?为什么?
存在的,单一类别(string/anim/drawable等)资源最终主 module 合并时生成的单个 class 文件内常量池总数不能超过 65536(不是资源 id,一个 class 还有其他东西占用常量池的),否则无法生成对应的 R,因为 ASM 字节码在生成合并 R 时常量池爆炸了。
-
我自己编写了一个 field 超过 65535 的类会有问题吗?能在官方 multidex 场景下使用吗?
会有问题,无法编译通过,不符合 JVM Class 规范。既然无法编译通过,所以不存在在官方 multidex 场景下的使用,因为到不了分 dex 那一步就阵亡了。
会不会遇到世界末日
你以为故事到这就结束了?到这里不由得虎躯一震反思一下: “这锅会不会和当年 multidex 一样在未来航母级 app 某个时刻翻车呢?” 答案是有可能,但是短期不会,因为想要到达这个瓶颈就需要我们单一类型资源越界,这个其实目前还少有 app 到这个体量,即便航母 app 也不容易达到,除非你杠精一下。
这个问题的本质其实就回到了 google 官方一直对这个 R 的态度了,这玩意一直小变动,却总是不想办法从根源治理。远古 apt 时代子 module 的 R 里面 field 也是 static final 的常量,后来 google 为了加速构建,子 module 搞成非 static final(导致一些注解框架自己造一个 R2),主 module 合成,然后主 module 搞一份 static final 的,同时保留子 module 的非 static final,一直至今都叫R.java
,然后 AGP4.1.0 版本这玩意直接不再出现R.java
,而是一步到位R.jar
的 class jar 了,而且子 module 的非 static final 的属性也不再给随机安插一个数值了,直接不赋值了。玩到这个版本还是没根治啊。
假若将来某一天真的重蹈覆辙 multidex 的道路怎么办?能想到的好方法就是 google 出马优化掉这玩意。否则我们作为三方 app 可能只能骚操作了,目前想到的两个骚操作就是:
- 方案一:类似插件化,把越界资源编成多个 apk,hook 资源加载骗过呗,不过这玩意只是我先 YY 下,因为鬼知道骗过了 Class 常量池上限,会不会资源加载那块也埋雷了,这样就不好玩了。
- 方案二:自己类似 java resources 一样造一套资源,不再参与 aapt2 编译,而是直接参与 java 编译打包,然后自己拖过映射多语言啥的场景,对外保留一个 nameId 获取和 android 的资源管理类对接。这个看起来是可行的,只是包大小和性能一定有影响,不然 google 当年也不会把它搞成
resources.arsc
和 R 索引了。
如上纯属自己的 YY,看看就好,别较真。
总结
歪打正着,本来是要去看别的问题的,一下被带到思考了一下这个问题,还行。可以看到,其实问题不复杂,也不难,稍微跟一下代码就能知道咋回事了,就一点,做事还得静下来,这样才能深度思考,然后才能若有所思。
日拱一卒,功不唐捐。今日已拱,哈哈。
【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题 未经允许严禁转载 https://blog.csdn.net/yanbober】