我有个大胆的方案能够提升ARouter和WMRouter的编译速度

背景

因为当前项目工程比较庞大,编译一次大概要3-5分钟左右,AGP支持增量编译,可是苦于路由框架的plugin的增量编译一直都是关闭的,因此这方面一直都没有成功。java

我本身之前也写过路由组件,而后上一篇文章介绍了那个ClassNotFound异常以后,我仍是对注册的逻辑有些不满意的,因此我本身优化了下plugin的实现。android

我写了个测试的demo,给一个项目进行增量编译的测试。一个未开启增量编译的plugin编译时间中位数在35s左右。而在忽略了首次编译的状况下,开启增量编译的项目编译时间的中位数在4s左右。git

个人优化思路

路由Plugin的原理

原理其实很简单,就是扫描项目的全部.class文件,当class文件的包名符合路由注册生成的包名的标准的状况下,持有这个class名。当扫描完成以后把这些class插入到一个注册类上。github

固然两个路由框架的注册机制仍是有些差别的,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。ide

  1. **.class **当一个class发生变化和新增的状况下都会触发这个方法,这个时候咱们能够记录这个class,插入到hashset中。
  2. Jar包变化的状况下,咱们会从新扫描这个jar包,同时咱们根据逻辑判断里面是否是有符合咱们要求的class并插入到hashset中。

可是其实只有插入是不够的,咱们须要获取到删除的这种状况。函数

Jar包Class文件Diff

当一个module代码发生变化的状况下,plugin只会通知咱们Jar包发生了变化,module内的代码到底发生了什么变化对于咱们来讲是黑盒的。对于路由注册plugin来讲,咱们只关心jar内的class是否发生了增减,可是一个puglin的只会通知咱们文件发生了修改。如何获取到class的增减呢?post

private void diffJar(File dest, JarInput jarInput) {
        try {
            HashSet<String> oldJarFileName = JarUtils.scanJarFile(dest);
            HashSet<String> newJarFileName = JarUtils.scanJarFile(jarInput.getFile());
            SetDiff diff = new SetDiff<>(oldJarFileName, newJarFileName);
            List<String> removeList = diff.getRemovedList();
            Log.info("diffList:" + removeList);
            if (removeList.size() > 0) {
                JarUtils.deleteJarScan(dest, removeList, deleteCallBack);
            }
            foreachJar(dest, jarInput);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
复制代码

简单的分析下上面的操做逻辑。测试

  1. 先扫描上次编译的文件,将全部的class名字都读取出来。
  2. 读取此次输入的jar包,同时把class名字都读取出来。
  3. 用最简单的dif算法,把被删除的class都拿出来。
  4. 而后扫描删除的class中是否存在路由注册类,用一个HashSet去持有。
  5. 扫描剩下来的jar包,并修改class。

字节码操做

private void generateInitClass(String directory, HashSet<String> items, HashSet<String> 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的优势都结合了一下,固然也有点投机取巧的成分在。

  1. 首先我在路由组件内部用compileOnly的方式引入了一个注册类,这个注册类在合并的时候并不会被合并到代码内。
  2. transform的扫描完成以后,去生成好这个类的实现,这样就不会出现项目运行时的classNotFound异常了。

若是将注册类像ARouter同样放在基础库内部,我就要在编译的最后阶段去寻找那个包含有注册类的jar包,而后定位到那个类,对其进行修改。这要须要对全部jar包的进行扫描,这个过程相对来讲是耗时的,并且我修改了整个jar包内的class,须要从新覆盖output的jar包。另外我也不须要像美团组件同样,用反射的方式去调用注册类,由于这个类会在最后编译时被生成和修改,并且类名,方法名和compileOnly的彻底同样。

回到增编的问题来,当增量编译触发的状况下,这个时候output已经存在了注册类,咱们会将新增的HashSet和删除的HashSet,都以参数传输到ClassVisitor上。

class ClassFilterVisitor extends ClassVisitor {
    private HashSet<String> classItems private HashSet<String> deleteItems ClassFilterVisitor(ClassVisitor classVisitor, HashSet<String> classItems, HashSet<String> 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<String> deleteItems;
    private HashSet<String> addItems;

    public TryCatchMethodVisitor(MethodVisitor mv, HashSet<String> addItems, HashSet<String> 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仍是花了些心思在里面的。

Android 路由注册优化

相关文章
相关标签/搜索