须要了解一点gradle知识,一点groovy语言,简单的ASM知识,这个插件的功能只是用ASM在编译期间插入代码,作简单的方法执行时间统计。java
主要内容android
首先咱们先看一张经典的打包流程图:git
那么问题来了:github
我怎么知道何时生成了.class文件,并且还要是没转成dex?bash
怎么在编译时候插入代码?app
带着这两个问题,往下走:ide
在gradle插件1.5.0-beta1
版本时候,提供了一个Transform API,这个API专门就是为了第三方插件对编译后class文件转为dex以前而提供的,直接撸一个代码,由于是插件因此直接新建一个module,命名为buildSrc
,至于为啥要叫BuildSrc是由于这是Android保留给自定义plugin的名字,须要新建一个放插件的目录,都是用groovy语言写的全部目录层级以下图: 学习
android.tools.build
里面的asm。
而后就能够开始写groovy脚本了,既然前面说了是用Transform API那么就来继承这个API,还须要实现Plugin
这个接口,plugin
这个接口很是重要是用来把咱们这个自定义的插件注册到project的task中,回到transform中,这个类须要实现getName,getInputTypes,getScopes,isIncremental四个抽象方法,还有一个tranform方法:gradle
getInputTypes():限定输入文件的类型(例如:class,jar,dex等)
getScopes():限定文件所在的区域(例如:全部project,只有主工程等)
isIncremental():是否增量更新
getName():在控制台打印的transform名字(只是把这个名字拼接上去而已,例如:transformClassesWith+name+ForDebug)
transform(TransformInvocation transformInvocation):这方法才是真正的插件实现
复制代码
在这里面transform方法中就能获得全部的.class还有jar具体代码以下:ui
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"){
//各类过滤类,关联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文件编译生成的字节码文件:
transform
方法仍是本身定义的
handle
方法,固然都不是啦,是在上面的
MethodTotal
类,在其中作对class类的操做,里面还有一个本身定义的注解,其实就是用来过滤那些方法须要统计耗时用的,下一步就来到了,最喜欢的cv的步骤了(内容比较简单,也有注释就直接上代码了):
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是否为同一个注解,是否须要统计耗时
if (Type.getDescriptor(TimeTotal.class).equals(s)) {
inject = true;
}
return super.visitAnnotation(s, b);
}
@Override
protected void onMethodEnter() {
//方法进入时期
if (inject) {
//这里就是以前使用ASM插件生成的统计时间代码
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) {
//方法结束时期
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;
}
}
复制代码
那么如此一来,整个就关联起来了,这个插件也基本成型,能够直接在app的build.gradle中使用apply plugin :完整的插件类名
,固然也可使用apply plugin:xxx
引用,那咱们就须要在这个buildSrc下面的main中新建resources->META-INF->gradle-plugins
路径(别问为啥要这这样路径,这就规定),而后新建一个插件名.properties
文件,里面使用implementation-class
来关联本身的插件:
apply plugin: 'time-total'
这样就能使用这个插件。
最后附上一张运行结果图:
1.先建立buildSrc文件夹,建立插件
2.使用asm生成代码
3.cv代码到自定义的ClassVisitor😜
4.app的build.gradle引用插件
复制代码
虽然第一次玩这一类东西,可是感受也是收获良多,对gradle又加深了些了解,学习的过程是痛苦的,可是最后作出来倒是欣慰和知足,仅此作个记录。
最后项目github地址:github.com/kgxl/TimeCo…