TL;DR
插件名: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 文件中收集常量值:
1 2 3 4 5 6 7 8 9
| @Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { if (value instanceof Integer) { String key = typeName + '.' + name; symbols.putIfAbsent(key, (Integer) value); } return null; }
|
收集 styleable 的 int数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 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()]; } else if (opcode == Opcodes.BIPUSH) { intStack.push(operand); } }
@Override public void visitLdcInsn(Object cst) { if (cst instanceof Integer) { intStack.push((Integer) cst); } }
@Override public void visitInsn(int opcode) { if (opcode >= Opcodes.ICONST_0 && opcode <= Opcodes.ICONST_5) { intStack.push(opcode - Opcodes.ICONST_0); } 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,输出到一个类文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| 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); pushInt(clinit, i); pushInt(clinit, value[i]); clinit.visitInsn(Opcodes.IASTORE); } clinit.visitFieldInsn(Opcodes.PUTSTATIC, RSymbols.R_STYLEABLES_CLASS_NAME, field, "[I"); } clinit.visitInsn(Opcodes.RETURN); clinit.visitMaxs(0, 0); clinit.visitEnd(); writer.visitEnd();
byte[] bytes = writer.toByteArray(); Files.write(dir.toPath().resolve(RSymbols.R_STYLEABLES_CLASS_NAME + ".class"), bytes);
|
确认某个类是否访问了 R:
1 2 3 4 5 6 7 8 9 10 11
| Pattern rClassPattern = Pattern.compile("^(\\w+/)+R\\$[a-z]+"); boolean attemptToVisitR = false
@Override public void visitInnerClass(String name, String outerName, String innerName, int access) { if (!attemptToVisitR && access == 0x19 && rClassPattern.matcher(name).matches()) { attemptToVisitR = true; } }
|
内联int 字面值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @Override public void visitFieldInsn(int opcode, String owner, String fieldName, String fieldDesc) { if (opcode != Opcodes.GETSTATIC || owner.startsWith("java/lang/")) { 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")) { 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); } 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); } }
|