我有個大膽的方案能夠提升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 路由註冊優化

相關文章
相關標籤/搜索