记录一次 AGP 调研过程中的思考,我从一个事故搞出了一个故事!

1,434 阅读8分钟

背景

看过我博客的老铁应该知道,我在 18 年五月写过一个小 gradle 插件github.com/yanbober/ap…,其作用就是将 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 中,这块不清楚的可以研究下我背景信息里提到的之前做的小项目。

现在我有几个灵魂拷问想问你:

  1. 资源生成的 R 文件格式是怎么样的?不同 module 下又有什么区别?(答不上的去看我那个小项目的 REDME 吧)
  2. 使用官方 multidex 方案情况下还会存在 method 或者 field 超过 65535 的情况吗?本质原因是为什么?
  3. App 资源个数(包括 string 个数等)是否存在上限?为什么?
  4. 我自己编写了一个 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++));
do
  echo "<string name=\"public_r_$i\">TEST-$i</string>" >> $file_name;
done

echo "</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文件中的常量池》

灵魂拷问的答案

到此我们整明白了来龙去脉和问题的本质,那我们现在来深度回答下一开始卖关子的问题。

  1. 资源生成的 R 文件格式是怎么样的?不同 module 下又有什么区别?

    去看 github.com/yanbober/ap… REDME 吧,很深度解析了。

  2. 使用官方 multidex 方案情况下还会存在 method 或者 field 超过 65535 的情况吗?本质原因是为什么?

    很明显,使用官方 multidex 方案情况下不会出现 method 或者 field 超过 65535 的情况,因为 JVM 这一层就已经限制了玩法规则。本质就是上面隐秘的真相。

  3. App 资源个数(包括 string 个数等)是否存在上限?为什么?

    存在的,单一类别(string/anim/drawable等)资源最终主 module 合并时生成的单个 class 文件内常量池总数不能超过 65536(不是资源 id,一个 class 还有其他东西占用常量池的),否则无法生成对应的 R,因为 ASM 字节码在生成合并 R 时常量池爆炸了。

  4. 我自己编写了一个 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,看看就好,别较真。

总结

歪打正着,本来是要去看别的问题的,一下被带到思考了一下这个问题,还行。可以看到,其实问题不复杂,也不难,稍微跟一下代码就能知道咋回事了,就一点,做事还得静下来,这样才能深度思考,然后才能若有所思。

日拱一卒,功不唐捐。今日已拱,哈哈。