我有个大胆的方案可以提高ARouter和WMRouter的编译速度
最终成果
github 仓库链接地址 github.com/Leifzhang/R…
wmrouter 增量编译
如果使用wmrouter的各位,可以直接用我的插件替换工程内的路由初始化,应该能解决项目编译的问题。基本测试都通过了。
buildscript {
dependencies {
classpath 'com.kronos.plugin:AutoRegister:0.5.5'
}
}
apply plugin: 'router-register'
AutoRegister {
REGISTER_PACKAGE_NAME = "com.sankuai.waimai.router.generated.service"
REGISTER_CLASS_NAME = "com.sankuai.waimai.router.generated.ServiceLoaderInit"
REGISTER_FUNCTION_NAME = "init"
REGISTER_CLASS_FUNCTION_NAME = "init"
}
背景
由于当前项目工程比较庞大,编译一次大概要3-5分钟左右,AGP支持增量编译,但是苦于路由框架的plugin的增量编译一直都是关闭的,所以这方面一直都没有成功。
我自己以前也写过路由组件,然后上一篇文章介绍了那个ClassNotFound异常之后,我还是对注册的逻辑有些不满意的,所以我自己优化了下plugin的实现。
我写了个测试的demo,给一个项目进行增量编译的测试。一个未开启增量编译的plugin编译时间中位数在35s左右。而在忽略了首次编译的情况下,开启增量编译的项目编译时间的中位数在4s左右。
我的优化思路
路由Plugin的原理
原理其实很简单,就是扫描项目的所有.class文件,当class文件的包名符合路由注册生成的包名的标准的情况下,持有这个class名。当扫描完成之后把这些class插入到一个注册类上。
当然两个路由框架的注册机制还是有些差异的,wmrouter在初始化的时候反射了一个不存在代码中的初始化类(com.sankuai.waimai.router.ServiceLoaderInit),然后在transform的最后用asm生成了这个初始化的类。而ARouter则是在一个注册类(com/alibaba/android/arouter/core/LogisticsCenter)的空方法里面插入了注册的方法调用来实现的。
开启编译
对于一个plugin来说,并不是把增量编译写成true就代表增量编译是ok的。我之前写过一篇文章Android Transform增量编译,里面有对增编基础库的一些简单的定义,同时有速度的比较。
@Override
public boolean isIncremental() {
return true;
}
复制代码
获取插入注册类
首先我们需要获取到增量编译的情况下的所有新的.class文件。我们先new一个HashSet去持有这些新增的class。
- **.class **当一个class发生变化和新增的情况下都会触发这个方法,这个时候我们可以记录这个class,插入到hashset中。
- Jar包变化的情况下,我们会重新扫描这个jar包,同时我们根据逻辑判断里面是不是有符合我们要求的class并插入到hashset中。
但是其实只有插入是不够的,我们需要获取到删除的这种情况。
Jar包Class文件Diff
当一个module代码发生变化的情况下,plugin只会通知我们Jar包发生了变化,module内的代码到底发生了什么变化对于我们来说是黑盒的。对于路由注册plugin来说,我们只关心jar内的class是否发生了增减,但是一个puglin的只会通知我们文件发生了修改。如何获取到class的增减呢?
private void diffJar(File dest, JarInput jarInput) {
try {
HashSet oldJarFileName = JarUtils.scanJarFile(dest);
HashSet newJarFileName = JarUtils.scanJarFile(jarInput.getFile());
SetDiff diff = new SetDiff<>(oldJarFileName, newJarFileName);
List removeList = diff.getRemovedList();
Log.info("diffList:" + removeList);
if (removeList.size() > 0) {
JarUtils.deleteJarScan(dest, removeList, deleteCallBack);
}
foreachJar(dest, jarInput);
} catch (Exception e) {
e.printStackTrace();
}
}
复制代码
简单的分析下上面的操作逻辑。
- 先扫描上次编译的文件,将所有的class名字都读取出来。
- 读取这次输入的jar包,同时把class名字都读取出来。
- 用最简单的dif算法,把被删除的class都拿出来。
- 然后扫描删除的class中是否存在路由注册类,用一个HashSet去持有。
- 扫描剩下来的jar包,并修改class。
字节码操作
private void generateInitClass(String directory, HashSet items, HashSet deleteItems) {
String className = Constant.REGISTER_CLASS_CONST.replace('.', '/');
File dest = new File(directory, className + SdkConstants.DOT_CLASS);
if (!dest.exists()) {
try {
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new ClassVisitor(Opcodes.ASM6, writer) {
};
cv.visit(50, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);
TryCatchMethodVisitor mv = new TryCatchMethodVisitor(cv.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC,
Constant.REGISTER_FUNCTION_NAME_CONST, "()V", null, null), null, deleteItems);
mv.visitCode();
for (String clazz : items) {
String input = clazz.replace(".class", "");
input = input.replace(".", "/");
Log.info("item:" + input);
mv.addTryCatchMethodInsn(Opcodes.INVOKESTATIC, input, "init", "()V", false);
}
mv.visitInsn(Opcodes.RETURN);
mv.visitEnd();
cv.visitEnd();
dest.getParentFile().mkdirs();
new FileOutputStream(dest).write(writer.toByteArray());
} catch (Exception e) {
e.printStackTrace();
}
} else {
try {
modifyClass(dest, items, deleteItems);
} catch (IOException e) {
e.printStackTrace();
}
}
}
复制代码
我把Arouter和WMrouter的plugin的优点都结合了一下,当然也有点投机取巧的成分在。
- 首先我在路由组件内部用compileOnly的方式引入了一个注册类,这个注册类在合并的时候并不会被合并到代码内。
- transform的扫描完成之后,去生成好这个类的实现,这样就不会出现项目运行时的classNotFound异常了。
如果将注册类像ARouter一样放在基础库内部,我就要在编译的最后阶段去寻找那个包含有注册类的jar包,然后定位到那个类,对其进行修改。这要需要对所有jar包的进行扫描,这个过程相对来说是耗时的,而且我修改了整个jar包内的class,需要重新覆盖output的jar包。另外我也不需要像美团组件一样,用反射的方式去调用注册类,因为这个类会在最后编译时被生成和修改,而且类名,方法名和compileOnly的完全一样。
回到增编的问题来,当增量编译触发的情况下,这个时候output已经存在了注册类,我们会将新增的HashSet和删除的HashSet,都以参数传输到ClassVisitor上。
class ClassFilterVisitor extends ClassVisitor {
private HashSet classItems
private HashSet deleteItems
ClassFilterVisitor(ClassVisitor classVisitor, HashSet classItems, HashSet deleteItems) {
super(Opcodes.ASM6, classVisitor)
this.classItems = classItems
this.deleteItems = deleteItems
}
@Override
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (name == "register" && desc == "()V") {
TryCatchMethodVisitor methodVisitor = new TryCatchMethodVisitor(super.visitMethod(access, name, desc, signature, exceptions),
classItems, deleteItems)
return methodVisitor
}
return super.visitMethod(access, name, desc, signature, exceptions)
}
}
复制代码
当register方法被触发的时候,替换成我们的MethodVisitor,对这个MethodVisitor进行修改。
public class TryCatchMethodVisitor extends MethodVisitor {
private HashSet deleteItems;
private HashSet addItems;
public TryCatchMethodVisitor(MethodVisitor mv, HashSet addItems, HashSet deleteItems) {
super(Opcodes.ASM5, mv);
this.deleteItems = deleteItems;
this.addItems = addItems;
if (this.addItems == null) {
this.addItems = new HashSet<>();
}
if (this.deleteItems == null) {
this.deleteItems = new HashSet<>();
}
Log.info("deleteItems:" + deleteItems);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
Log.info("visit owner : " + owner);
String className = owner + ".class";
if (!deleteItems.contains(className)) {
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
}
@Override
public void visitCode() {
super.visitCode();
for (String input : addItems) {
input = input.replace(".class", "");
input = input.replace(".", "/");
deleteItems.add(input + ".class");
addTryCatchMethodInsn(Opcodes.INVOKESTATIC, input, "init", "()V", false);
Log.info("visitInsn");
}
Log.info("onCodeInsert");
}
public void addTryCatchMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
/* Label l0 = new Label();
Label l1 = new Label();
Label l2 = new Label();
mv.visitTryCatchBlock(l0, l1, l2, "java/lang/Exception");*/
mv.visitMethodInsn(opcode, owner, name, desc, itf);
/* mv.visitLabel(l1);
Label l3 = new Label();
mv.visitJumpInsn(Opcodes.GOTO, l3);
mv.visitLabel(l2);
mv.visitVarInsn(Opcodes.ASTORE, 1);
mv.visitLabel(l3);*/
}
}
复制代码
首先触发的是visitMethodInsn方法,这个就是之前上一次编译的时候剩下来的注册信息,当owner符合删除类的情况下,我们就会过滤掉这个方法执行。这样就能做到删除的操作了。然后当所有的方法内的函数都被执行完之后,会走visitCode,这个时候我们把,上次收集到的新增的类插入到这个注册类上,这样就能完成整个项目的增量编译了。
总结
如果优化一段代码,首先我们还是要有自己的思考,一个类库虽然稳定了,但是并不代表功能无法更新迭代。举个例子,就比如这个注册类的实现,其实我就分析了两个库的优缺点,找了个折中方案,去对其进行调整,同时也完成了增量的工作。
最后还是要贴上项目链接,其实祖传代码,写的并不是很好,但是这次的plugin还是花了些心思在里面的。
- axios请求封装和异常统一处理
- SpringSecurity中密码加盐与SpringBoot中异常统一处理
- SpringBoot+Vue前后端分离,使用SpringSecurity完美处理权限问题(二)
- SpringBoot+Vue前后端分离,使用SpringSecurity完美处理权限问题(一)
- Java操作MongoDB
- 初识MongoDB分片
- Linux上安装Redis
- SpringBoot+WebSocket实现在线聊天(二)
- SpringBoot中使用Freemarker构建邮件模板
- SpringBoot中发送QQ邮件
- SpringBoot中使用POI,快速实现Excel导入导出
- 代理技术 | 重磅,代理服务器背后的故事(正向、反向代理)
- SpringBoot中自定义参数绑定
- ElementUI中tree控件踩坑记
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 动态规划此一篇就够了 万字总结
- 每日一问第2期 | final, finally, finalize 的区别?
- 当return遇到try、catch、finally时会发生什么?
- 五分钟C语言数据结构 之 二叉树层次遍历
- 为何IntelliJ IDEA比Eclipse好在哪里?
- 五分钟C语言数据结构 之 二叉树中序遍历
- Django 安全之跨站点请求伪造(CSRF)保护
- 五分钟C语言数据结构 之 二叉树先序遍历
- Java 语言基础(常用设计原则和设计模式,常用 Java 8~11 新特性)
- 五分钟C语言数据结构 之 二叉树后序遍历(非递归很重要)
- 5分钟Flink - 自定义Source源
- 9.深入k8s:调度器及其源码分析
- 5分钟Flink - 自定义Data Sink
- 5分钟Flink - 流处理API转换算子集合
- 视频上云/网络穿透/网络映射服务EasyNTS前端组织添加页面出现Vue冲突怎么解决?