一个小插件解决组件化引发的DEX字段数爆炸的问题

栏目: Java · 发布时间: 7年前

内容简介:一个小插件解决组件化引发的DEX字段数爆炸的问题

插件名:shrinker

项目地址: https://github.com/yrom/shrinker (其实很早之前就已经发布到github上了,不过无人问津→_→)

插件效果:与 removeUnusedCode 同用可以起到最佳效果

这里有一个简单的 测试项目 ,大部分类来自于依赖的support库,结果如下:

选项 methods fields classes
原始项目 22164 14367 2563
应用shrinker 插件 21979 7805 2392
应用shrinker 并开启 removeUnusedCode 11335 3302 1274

如果应用于依赖众多的大型项目则效果惊人。

ps. 其实已经在 b站的APP 上使用很久了,插件稳定、可靠且无副作用。

原理

不论组件化或者说模块化,都有个核心思想:拆分,拆成一个又一个独立的Library。

拆分 Library 引入的问题

举个例子

现一个 APP,它为了实践组件/模块化,拆分出了 common-ui ,business-a, business-b… 依赖关系如下图所示:

一个小插件解决组件化引发的DEX字段数爆炸的问题

R 文件生成的大致流程如下图:

一个小插件解决组件化引发的DEX字段数爆炸的问题

其中 processReleaseResources 实际是调用的 aapt 工具来给每个依赖的Library都生成一个最终确定的 R.java

可想而知,第一个问题: 拆分的Android Library越多,R 文件越多!

然而,Library 的 R 文件只会在最终编译成 APK 时确定字段常量值,输出 aar 时只有一个R.txt用于记录声明的资源。

假设 common-ui 声明了15个公共drawable资源,则生成的 R 文件中将有 15个相关的用于记录的字段,而且每个依赖于它的上层的library 生成的R都会有这15个同名的字段,如下图:

一个小插件解决组件化引发的DEX字段数爆炸的问题

由此可得,第二个问题: 越底层的依赖所声明资源越多,最终生成的 R 文件越庞大 ! 因为这些字段没有得到有效内联,最终生成的DEX字段数就会严重超标。

为了解决组件/模块化进程中出现的上述两个问题, shrinker 应运而生。

解决问题

Android Gradle 构建 工具 引入了 Transform API 给在生成DEX之前处理 class 和资源提供了方便。

shrinker 就是基于这个API,将所有引用到 R文件中字段 的 class (包括 Jar包中的)都进行内联处理。特别的是, R.styleable 这个类中并不只有可被内联的字面值,还有int数组,故而对它做额外的合并处理。

思路: 通过扫描 Transform API 的输入的class,找到所有的 R 类,建立一个符号表;找到所有其它有访问 R 中字段的类,静态访问方式改为内联常量值(值根据字段名从符号表中获取)。

关键方法

为了修改 class,用到了另一个著名的库 asm

从生成的 R 文件中收集常量值:

@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value){
  // 都是 int 类型的常量值
  if (value instanceof Integer) {
    String key = typeName + '.' + name;
    symbols.putIfAbsent(key, (Integer) value);
  }
  return null;
}

收集 styleable 的 int数组:

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions){
  // int数组都在静态初始化方法中
  if (access == Opcodes.ACC_STATIC && "<clinit>".equals(name)) {

    return new MethodVisitor(Opcodes.ASM5) {
      int[] current = null;
      LinkedList<Integer> intStack = new LinkedList<>();

      @Override
      public void visitIntInsn(int opcode, int operand){
        if (opcode == Opcodes.NEWARRAY && operand == Opcodes.T_INT) {
          current = new int[intStack.pop()]; // 弹出栈顶 int 值作为数组长度
        } else if (opcode == Opcodes.BIPUSH) {
          intStack.push(operand); // 入栈一个 int 常量
        }
      }

      @Override
      public void visitLdcInsn(Object cst){
        if (cst instanceof Integer) { 
          intStack.push((Integer) cst); // 入栈一个 int 常量
        }
      }

      @Override
      public void visitInsn(int opcode){
        if (opcode >= Opcodes.ICONST_0 && opcode <= Opcodes.ICONST_5) {
          intStack.push(opcode - Opcodes.ICONST_0); // 入栈一个 int 常量(0~5)
        } else if (opcode == Opcodes.IASTORE) {
          int value = intStack.pop();
          int index = intStack.pop();
          current[index] = value; // 按索引给数组赋值
        }
      }

      @Override
      public void visitFieldInsn(int opcode, String owner, String name, String desc){
        if (opcode == Opcodes.PUTSTATIC) { // 赋值给静态字段,结束数组
          int[] old = styleables.get(name);
          if (old != null && old.length != current.length && !Arrays.equals(old, current)) {
            throw new IllegalStateException("Value of styleable." + name + " mismatched! "
                                            + "Excepted " + Arrays.toString(old)
                                            + " but was " + Arrays.toString(current));
          } else {
            styleables.put(name, current);
          }
          current = null;
          intStack.clear();
        }
      }
    };
  }
  return null;
}

合并 styleable,输出到一个类文件中:

ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
writer.visit(Opcodes.V1_6,
             Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_SUPER,
             RSymbols.R_STYLEABLES_CLASS_NAME,
             null,
             "java/lang/Object",
             null);
for (String name : symbols.getStyleables().keySet()) {
  writer.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, name, "[I", null, null);
}

Map<String, int[]> styleables = symbols.getStyleables();
MethodVisitor clinit = writer.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
clinit.visitCode();

for (Map.Entry<String, int[]> entry : styleables.entrySet()) {
  final String field = entry.getKey();
  final int[] value = entry.getValue();
  final int length = value.length;
  pushInt(clinit, length);
  clinit.visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_INT);
  for (int i = 0; i < length; i++) {
    clinit.visitInsn(Opcodes.DUP);                  // dup
    pushInt(clinit, i);
    pushInt(clinit, value[i]);
    clinit.visitInsn(Opcodes.IASTORE);              // iastore
  }
  clinit.visitFieldInsn(Opcodes.PUTSTATIC, RSymbols.R_STYLEABLES_CLASS_NAME, field, "[I");
}
clinit.visitInsn(Opcodes.RETURN);
clinit.visitMaxs(0, 0); // auto compute
clinit.visitEnd();
writer.visitEnd();

byte[] bytes = writer.toByteArray();
Files.write(dir.toPath().resolve(RSymbols.R_STYLEABLES_CLASS_NAME + ".class"), bytes);

确认某个类是否访问了 R:

Pattern rClassPattern = Pattern.compile("^(\\w+/)+R\\$[a-z]+");
boolean attemptToVisitR = false
// 字段都是定义在 R 的内部类
@Override
public void visitInnerClass(String name, String outerName, String innerName,int access){
  if (!attemptToVisitR
      && access == 0x19 /*ACC_PUBLIC | ACC_STATIC | ACC_FINAL*/
      && rClassPattern.matcher(name).matches()) {
    attemptToVisitR = true;
  }
}

内联int 字面值:

@Override
public void visitFieldInsn(int opcode, String owner, String fieldName,
                           String fieldDesc) {
  if (opcode != Opcodes.GETSTATIC || owner.startsWith("java/lang/")) {
    // skip!
    this.mv.visitFieldInsn(opcode, owner, fieldName, fieldDesc);
    return;
  }
  String typeName = owner.substring(owner.lastIndexOf('/') + 1);
  String key = typeName + '.' + fieldName;
  if (rSymbols.containsKey(key)) {
    Integer value = rSymbols.get(key);
    if (value == null)
      throw new UnsupportedOperationException("value of " + key + " is null!");
    pushInt(this.mv, value); // 内联字面值
  } else if (owner.endsWith("/R$styleable")) { // 合并 styleable
    this.mv.visitFieldInsn(opcode, RSymbols.R_STYLEABLES_CLASS_NAME, fieldName, fieldDesc);
  } else {
    this.mv.visitFieldInsn(opcode, owner, fieldName, fieldDesc);
  }
}
static void pushInt(MethodVisitor mv,int i){
  if (0 <= i && i <= 5) {
    mv.visitInsn(Opcodes.ICONST_0 + i); // ICONST_0 ~ ICONST_5
  } else if (i <= Byte.MAX_VALUE) {
    mv.visitIntInsn(Opcodes.BIPUSH, i);
  } else if (i <= Short.MAX_VALUE) {
    mv.visitIntInsn(Opcodes.SIPUSH, i);
  } else {
    mv.visitLdcInsn(i);
  }
}

以上所述就是小编给大家介绍的《一个小插件解决组件化引发的DEX字段数爆炸的问题》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Seasoned Schemer

The Seasoned Schemer

Daniel P. Friedman、Matthias Felleisen / The MIT Press / 1995-12-21 / USD 38.00

drawings by Duane Bibbyforeword and afterword by Guy L. Steele Jr.The notion that "thinking about computing is one of the most exciting things the human mind can do" sets both The Little Schemer (form......一起来看看 《The Seasoned Schemer》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换