内容简介:一个小插件解决组件化引发的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字段数爆炸的问题》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
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》 这本书的介绍吧!