TL;DR

插件名:shrinker

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

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

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

选项methodsfieldsclasses
原始项目22164143672563
应用shrinker 插件2197978052392
应用shrinker 并开启 removeUnusedCode1133533021274

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

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) {
// 都是 int 类型的常量值
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) {
// 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,输出到一个类文件中:

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); // 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:

1
2
3
4
5
6
7
8
9
10
11
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 字面值:

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/")) {
// 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);
}
}