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

2018-01-13 11:05:20来源:https://yrom.net/blog/2018/01/12/android-gradle-plugin-for-s作者:Yrom's Blog人点击

分享

插件名: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);
}
}

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台