前言
需要了解一點gradle知識,一點groovy語言,簡單的ASM知識,這個插件的功能只是用ASM在編譯期間插入代碼,做簡單的方法執(zhí)行時間統(tǒng)計。
主要內(nèi)容
- 自定義插件
- 使用ASM插入代碼
- 統(tǒng)計方法耗時
首先我們先看一張經(jīng)典的打包流程圖:
我們這次要干的就是在.class文件轉(zhuǎn)為Dex之前做代碼插入,來達(dá)到編譯時插入代碼。
那么問題來了:
我怎么知道什么時候生成了.class文件,而且還要是沒轉(zhuǎn)成dex?
怎么在編譯時候插入代碼?
帶著這兩個問題,往下走:
在gradle插件1.5.0-beta1版本時候,提供了一個Transform API,這個API專門就是為了第三方插件對編譯后class文件轉(zhuǎn)為dex之前而提供的,直接擼一個代碼,因為是插件所以直接新建一個module,命名為buildSrc,至于為啥要叫BuildSrc是因為這是Android保留給自定義plugin的名字,需要新建一個放插件的目錄,都是用groovy語言寫的所有目錄層級如下圖:
當(dāng)然還需要新建一個build.gradle里面如下圖:
注:這里面懶的去找asm的依賴,就直接用的
android.tools.build里面的asm。
然后就可以開始寫groovy腳本了,既然前面說了是用Transform API那么就來繼承這個API,還需要實現(xiàn)Plugin這個接口,plugin這個接口非常重要是用來把我們這個自定義的插件注冊到project的task中,回到transform中,這個類需要實現(xiàn)getName,getInputTypes,getScopes,isIncremental四個抽象方法,還有一個tranform方法:
getInputTypes():限定輸入文件的類型(例如:class,jar,dex等)
getScopes():限定文件所在的區(qū)域(例如:所有project,只有主工程等)
isIncremental():是否增量更新
getName():在控制臺打印的transform名字(只是把這個名字拼接上去而已,例如:transformClassesWith+name+ForDebug)
transform(TransformInvocation transformInvocation):這方法才是真正的插件實現(xiàn)
在這里面transform方法中就能得到所有的.class還有jar具體代碼如下:
transformInvocation.inputs.each {
it.directoryInputs.each {
if(it.file.isDirectory()){
it.file.eachFileRecurse {
def fileName=it.name
if(fileName.endsWith(".class")&&!fileName.startsWith("R\$")
&& fileName != "BuildConfig.class"&&fileName!="R.class"){
//各種過濾類,關(guān)聯(lián)classVisitor
handleFile(it)
}
}
}
def dest=transformInvocation.outputProvider.getContentLocation(it.name,it.contentTypes,it.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(it.file,dest)
}
it.jarInputs.each { jarInput->
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
def dest = transformInvocation.outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}
這里面我把處理.class文件提出了個handle方法:
private void handleFile(File file){
def cr=new ClassReader(file.bytes)
def cw=new ClassWriter(cr,ClassWriter.COMPUTE_MAXS)
def classVisitor=new MethodTotal(Opcodes.ASM5,cw)
cr.accept(classVisitor,ClassReader.EXPAND_FRAMES)
def bytes=cw.toByteArray()
//寫回原來這個類所在的路徑
FileOutputStream fos=new FileOutputStream(file.getParentFile().getAbsolutePath()+File.separator+file.name)
fos.write(bytes)
fos.close()
}
這里面最最最主要的就是這個自己定義的MethodTotal類,這個類里面才是真正修改.class的主要邏輯,這里我們來簡單看下Java文件編譯生成的字節(jié)碼文件:
上圖只是隨便截了一段,臥槽,這讓我自己寫,或者自己去修改,告辭,打擾,等我再修煉兩年,那么我又想去改,但是我又寫不來怎么辦呢,這時候就需要了解一下as插件ASM Bytecode Outline,簡單粗暴,一鍵生成修改字節(jié)碼的代碼:
看到這個插件生成了三個,一個是字節(jié)碼,一個是asm添加字節(jié)碼的代碼,還有個是groovy添加字節(jié)代碼,所以我們只需要在Java文件中寫好統(tǒng)計時間的代碼然后使用這個插件生成代碼就行了,那么繼續(xù)走,現(xiàn)在也能生成編寫.class文件的代碼了,那么我們應(yīng)該寫到哪里去,是之前的
transform方法還是自己定義的handle方法,當(dāng)然都不是啦,是在上面的MethodTotal類,在其中做對class類的操作,里面還有一個自己定義的注解,其實就是用來過濾那些方法需要統(tǒng)計耗時用的,下一步就來到了,最喜歡的cv的步驟了(內(nèi)容比較簡單,也有注釋就直接上代碼了):
public class MethodTotal extends ClassVisitor {
public MethodTotal(int i, ClassVisitor classVisitor) {
super(i, classVisitor);
}
@Override
public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
MethodVisitor methodVisitor = cv.visitMethod(i, s, s1, s2, strings);
methodVisitor = new AdviceAdapter(Opcodes.ASM5, methodVisitor, i, s, s1) {
boolean inject;
@Override
public AnnotationVisitor visitAnnotation(String s, boolean b) {
//自定義的注解用來判斷方法上的注解與TimeTotal是否為同一個注解,是否需要統(tǒng)計耗時
if (Type.getDescriptor(TimeTotal.class).equals(s)) {
inject = true;
}
return super.visitAnnotation(s, b);
}
@Override
protected void onMethodEnter() {
//方法進(jìn)入時期
if (inject) {
//這里就是之前使用ASM插件生成的統(tǒng)計時間代碼
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("this is asm input");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitTypeInsn(NEW, "java/lang/Throwable");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Throwable", "<init>", "()V", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Throwable", "getStackTrace", "()[Ljava/lang/StackTraceElement;", false);
mv.visitInsn(ICONST_1);
mv.visitInsn(AALOAD);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StackTraceElement", "getMethodName", "()Ljava/lang/String;", false);
mv.visitVarInsn(ASTORE, 1);
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "com/example/asmdemo/TimeManager", "addStartTime", "(Ljava/lang/String;J)V", false);
}
}
@Override
protected void onMethodExit(int i) {
//方法結(jié)束時期
if (inject) {
//計算方法耗時
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "com/example/asmdemo/TimeManager", "addEndTime", "(Ljava/lang/String;J)V", false);
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, "com/example/asmdemo/TimeManager", "calcuteTime", "(Ljava/lang/String;)V", false);
}
}
};
return methodVisitor;
}
}
那么如此一來,整個就關(guān)聯(lián)起來了,這個插件也基本成型,可以直接在app的build.gradle中使用apply plugin :完整的插件類名,當(dāng)然也可以使用apply plugin:xxx引用,那我們就需要在這個buildSrc下面的main中新建resources->META-INF->gradle-plugins路徑(別問為啥要這這樣路徑,這就規(guī)定),然后新建一個插件名.properties文件,里面使用implementation-class來關(guān)聯(lián)自己的插件:
我這里插件名字叫time-total,所以我在app的build.gradle里面直接
apply plugin: 'time-total'這樣就能使用這個插件。
最后附上一張運行結(jié)果圖:
小小的總結(jié):
1.先創(chuàng)建buildSrc文件夾,創(chuàng)建插件
2.使用asm生成代碼
3.cv代碼到自定義的ClassVisitor??
4.app的build.gradle引用插件
遇到的坑:
- 在groovy下面新建的groovy文件一定要.groovy結(jié)尾不然在編譯時候不會生成在build文件夾里面,導(dǎo)致找不到類。
- 之前在CompileSdkVerison >=28時候會報dexMeger失敗,所以只要改成28一下就沒事了,我猜可能是因為androidx的原因吧。
- 在我們自定義ClassVisitor里通過注解來判斷是否需要統(tǒng)計時,要注意,兩個注解描述要一樣,也就是包名+名字。
感謝
總結(jié)
雖然第一次玩這一類東西,但是感覺也是收獲良多,對gradle又加深了些了解,學(xué)習(xí)的過程是痛苦的,但是最后做出來卻是欣慰和滿足,僅此做個記錄。
最后項目github地址:https://github.com/kgxl/TimeCostPlugin