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

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

内容简介:一个小插件解决组件化引发的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字段数爆炸的问题》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

爆品方法论

爆品方法论

陈轩 / 2019-1-6 / 49

作者利用自己在品牌定位和爆品营销领域十三年的实践历练,结合移动社交媒体、爆品营销策略和社会心理学,精心筛选出上百个经典的营销案例,既分享了爆品内容的炮制方法和营销原理,也分享了爆品内容的推广技巧,告诉读者如何用移动社交媒体来颠覆传统营销模式,如何用互联网思维来玩转营销,实现低成本、高销量、大传播,成功打造市场爆品。一起来看看 《爆品方法论》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

MD5 加密
MD5 加密

MD5 加密工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具