因爲當前項目工程比較龐大,編譯一次大概要3-5分鐘左右,AGP支持增量編譯,可是苦於路由框架的plugin的增量編譯一直都是關閉的,因此這方面一直都沒有成功。java
我本身之前也寫過路由組件,而後上一篇文章介紹了那個ClassNotFound異常以後,我仍是對註冊的邏輯有些不滿意的,因此我本身優化了下plugin的實現。android
我寫了個測試的demo,給一個項目進行增量編譯的測試。一個未開啓增量編譯的plugin編譯時間中位數在35s左右。而在忽略了首次編譯的狀況下,開啓增量編譯的項目編譯時間的中位數在4s左右。git
原理其實很簡單,就是掃描項目的全部.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
可是其實只有插入是不夠的,咱們須要獲取到刪除的這種狀況。函數
當一個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();
}
}
複製代碼
簡單的分析下上面的操做邏輯。測試
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的優勢都結合了一下,固然也有點投機取巧的成分在。
若是將註冊類像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仍是花了些心思在裏面的。