需要了解一点gradle知识,一点groovy语言,简单的ASM知识,这个插件的功能只是用ASM在编译期间插入代码,做简单的方法执行时间统计。
主要内容
首先我们先看一张经典的打包流程图:
我们这次要干的就是在.class文件转为Dex之前做代码插入,来达到编译时插入代码。
那么问题来了:
我怎么知道什么时候生成了.class文件,而且还要是没转成dex?
怎么在编译时候插入代码?
带着这两个问题,往下走:
在gradle插件 1.5.0-beta1
版本时候,提供了一个Transform API,这个API专门就是为了第三方插件对编译后class文件转为dex之前而提供的,直接撸一个代码,因为是插件所以直接新建一个module,命名为 buildSrc
,至于为啥要叫BuildSrc是因为这是Android保留给自定义plugin的名字,需要新建一个放插件的目录,都是用groovy语言写的所有目录层级如下图:
当然还需要新建一个build.gradle里面如下图:
注:这里面懒的去找asm的依赖,就直接用的android.tools.build
里面的asm。
然后就可以开始写groovy脚本了,既然前面说了是用Transform API那么就来继承这个API,还需要实现 Plugin
这个接口, plugin
这个接口非常重要是用来把我们这个自定义的插件注册到project的task中,回到transform中,这个类需要实现getName,getInputTypes,getScopes,isIncremental四个抽象方法,还有一个tranform方法:
getInputTypes():限定输入文件的类型(例如:class,jar,dex等) getScopes():限定文件所在的区域(例如:所有project,只有主工程等) isIncremental():是否增量更新 getName():在控制台打印的transform名字(只是把这个名字拼接上去而已,例如:transformClassesWith+name+ForDebug) transform(TransformInvocation transformInvocation):这方法才是真正的插件实现 复制代码
在这里面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"){ //各种过滤类,关联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文件编译生成的字节码文件:
,简单粗暴,一键生成修改字节码的代码:
看到这个插件生成了三个,一个是字节码,一个是asm添加字节码的代码,还有个是groovy添加字节代码,所以我们只需要在Java文件中写好统计时间的代码然后使用这个插件生成代码就行了,那么继续走,现在也能生成编写.class文件的代码了,那么我们应该写到哪里去,是之前的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:stuck_out_tongue_winking_eye: 4.app的build.gradle引用插件 复制代码