内容简介:一个小插件解决组件化引发的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… 依赖关系如下图所示:
R 文件生成的大致流程如下图:
其中 processReleaseResources
实际是调用的 aapt
工具来给每个依赖的Library都生成一个最终确定的 R.java
。
可想而知,第一个问题: 拆分的Android Library越多,R 文件越多!
然而,Library 的 R 文件只会在最终编译成 APK 时确定字段常量值,输出 aar 时只有一个R.txt用于记录声明的资源。
假设 common-ui 声明了15个公共drawable资源,则生成的 R 文件中将有 15个相关的用于记录的字段,而且每个依赖于它的上层的library 生成的R都会有这15个同名的字段,如下图:
由此可得,第二个问题: 越底层的依赖所声明资源越多,最终生成的 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字段数爆炸的问题》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。