Gradle自定义插件(二)ASM字节码插桩打印耗时

前面我们了解了自定义插件的基础流程
我们现在利用ASM字节码框架在每个方法里面自动插入计时方法

引入依赖库

implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'

自定义Transform

class AmsTransform : Transform() {
    //该Transform最终会生成一个Task,这个名字就是Task名字
    override fun getName(): String {
        return "AmsTransform"
    }
   
    /**
     * 指定 Transform 处理的数据, 
     * CONTENT_CLASS 表示处理 java class 文件,
     * CONTENT_RESOURCES, 表示处理 java 的资源
     */
    override fun getInputTypes(): MutableSet {
        return mutableSetOf(QualifiedContent.DefaultContentType.CLASSES)
    }
  // 是否增量编译
    override fun isIncremental(): Boolean {
        return true
    }
  /**
     * Transform 要操作的内容范围
     * 1.PROJECT 只有项目内容
     * 2.SUB_PROJECTS 只有子项目内容
     * 3.EXTERNAL_LIBRARIES 只有外部库
     * 4.TESTED_CODE 当前变量(包括依赖项)测试的代码
     * 5.PROVIDED_ONLY 本地或者员村依赖项
     */
    override fun getScopes(): MutableSet {
        return mutableSetOf(QualifiedContent.Scope.PROJECT)
    }
}

处理的方法在transform,主要参数有input.directoryInputs :表示项目源码类型,input.jarInputs:表示是jar包类型,由于我们在项目代码中插入,因此主要看handleDirecotoryInput方法

@Throws(IOException::class, TransformException::class, InterruptedException::class)
override fun transform(transformInvocation: TransformInvocation?) {
    super.transform(transformInvocation)

    println("**************transform start******************")
    transformInvocation?.let {
        it.outputProvider.deleteAll()

        val inputs = it.inputs
        inputs.forEach { input ->
            //遍历文件目录
            input.directoryInputs.forEach { directoryInput ->
                handleDirecotoryInput(directoryInput,it.outputProvider)
            }
            //遍历jar
            input.jarInputs.forEach { jarInput ->
                val dest = it.outputProvider.getContentLocation(jarInput.name,
                        jarInput.contentTypes, jarInput.scopes, Format.JAR)
            }
        }

    }
    println("**************transform end******************")

}

由于我们不处理外部jar文件,因此jar的就直接copy过去

@Throws(IOException::class)
    private fun transformJar(srcJar: File, destJar: File, status: Status) {
            logger.warn("srcJar:" + srcJar.absolutePath)
            logger.warn("destJar:" + destJar.absolutePath)
            if (!destJar.parentFile.exists()) {
                destJar.parentFile.mkdirs()
            }
            FileUtils.copyFile(srcJar, destJar)
    }

AmsTransform #handleDirecotoryInput,dest 是输出文件路径,isIncremental是判断当前是否是增量编译,如果是增量,则获取改变的文件的状态。如果有新增的,或者是修改的,则重新处理。最后调用transformSingleFile方法处理。如果不是增量,则调用transformDir获取所有文件,最后也是调用transformSingleFile

@Throws(IOException::class)
private fun handleDirecotoryInput(directoryInput: DirectoryInput, outputProvider: TransformOutputProvider) {
                //输出路径
                val dest = outputProvider.getContentLocation(
                    directoryInput.name,
                    directoryInput.contentTypes, directoryInput.scopes,
                    Format.DIRECTORY
                )
                FileUtils.forceMkdir(dest)
                if (isIncremental) {
                    val srcDirPath = directoryInput.file.absolutePath
                    val destDirPath = dest.absolutePath
                    val fileStatusMap = directoryInput.changedFiles
                    for ((inputFile, status) in fileStatusMap) {
                        val destFilePath = inputFile.absolutePath.replace(srcDirPath, destDirPath)
                        val destFile = File(destFilePath)
                        logger.warn("destFilePath:$destFilePath")
                        when (status) {
                            Status.NOTCHANGED -> {
                            }
                            Status.REMOVED -> if (destFile.exists()) {
                                destFile.delete()
                            }
                            Status.ADDED, Status.CHANGED -> {
                                try {
                                    FileUtils.touch(destFile)
                                } catch (e: IOException) {
                                    //maybe mkdirs fail for some strange reason, try again.
//                                    FileUtils.forceMkdirParent(destFile);
                                }
                                //处理单个文件
                                transformSingleFile(inputFile, destFile, srcDirPath)
                            }
                        }
                    }
                } else {
                    transformDir(directoryInput.file, dest)
                }
    }

AmsTransform #transformSingleFile,判断如果是class类型文件,则调用traceClass方法。其他文件则不处理。不然编译会出错

    @Throws(IOException::class)
     private fun transformSingleFile(inputFile: File, outputFile: File, srcBaseDir: String) {
        logger.warn("inputFile.getName():" + inputFile.name)
        if (inputFile.name.endsWith("class")) {
            traceClass(inputFile, outputFile)
        } else {
                FileUtils.copyFile(inputFile, outputFile)
        }
    }

AmsTransform #traceClass。主要的处理方法就是 cr.accept(LifecycClassVisitor(cw), ClassReader.EXPAND_FRAMES),而LifecycClassVisitor是什么呢?

//处理文件
  fun traceClass(input: File,output :File ){
      println("traceClass:"+input.absolutePath)
      println("output:"+output.absolutePath)
      if(!output.parentFile.exists()){
          output.parentFile.mkdirs()
      }
      val fis = FileInputStream(input)
      val cr = ClassReader(fis)
      val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES)
      //真正处理字节码,LifecycClassVisitor类里面有被处理类的信息
      cr.accept(LifecycClassVisitor(cw), ClassReader.EXPAND_FRAMES)
      //处理后的字节码
      val bytes = cw.toByteArray()
      val fos = FileOutputStream(output)
      fos.write(bytes)
      fos.close()
}

LifecycClassVisitor继承ClassVisitor类。解析class文件时先是调用visit方法。在该方法里,我们可以获取到类的一些信息,比如类名,父类名,接口信息。解析到类方法时会回调visitMethod方法,一般我们都是在方法上处理,因此主要也是关注这个方法。因为构造器方法和类加载时方法方法是编译器自动生成。我们一般不需要在这类方法上面处理。因此过滤掉。方法处理主要是在TraceMethodVisitor类中。

    class LifecycClassVisitor(cv: ClassVisitor):ClassVisitor(Opcodes.ASM5,cv),Opcodes {

      lateinit var className :String
       var isHandler = true
        //拿到类的信息, 然后对满足条件的类进行过滤
        override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array?) {
            super.visit(version, access, name, signature, superName, interfaces)
            println("visit $name,superName:$superName")
            className = name!!
           
    }
    //类的方法信息, 拿到需要修改的方法,然后进行修改操作
        override fun visitMethod( access: Int, name: String?, desc: String?, signature: String?, exceptions: Array?): MethodVisitor {
            println("visitMethod name:$name,className:${className}")
            val mv = cv.visitMethod(access,name,desc,signature,exceptions)
             name?.let {
                  n->
                isHandler = (n!=""&&n!="")
              }
            if (!isHandler){
                return mv
            }
            return TraceMethodVisitor(Opcodes.ASM5,mv,access,name!!,desc!!,className)
    }

        override fun visitEnd() {
            println("$className ....visitEnd")
            super.visitEnd()
        }
    }

ClassVisitor中还有其他方法,其执行顺序为

visit visitSource? visitOuterClass?(visitAnntation | visitAttribut)
(visitInnerClass | visitField | visitMethod)
visitEnd

TraceMethodVisitor类继承AdviceAdapter类。里面有两个方法onMethodEnter和onMethodExit,看名字就知道。方法执行行和方法执行后回调这两个方法。我们要插入的计时方法就在两个方法里面处理。
但是要如何知道要操作的字节码?

首先我们把要插入的代码先写好,比如我们现在要插入计时方法

long start = System.currentTimeMillis () 
//方法的代码执行
//打印的方法
Log.d("“TAG”,(System.currentTimeMillis () -start)+"ms")

如果看对应的字节码?
如果是Kotlin,在as里面的Tools->kotlin->show kotlin bytecode就可以看到对应的字节码。如果是java文件,则要用javap -c class文件路径就可以显示出字节码
每条字节码ASM都有对应的方法
如下方法

 long start = System.currentTimeMillis () 

对应的字节码

INVOKESTATIC java/lang/System.currentTimeMillis ()J //调用静态方法
LSTORE 1 //存储在局部变量表中

对于XSTORE N字节码,X表示类型,N表示存储在局部变量表中的 哪个位置。比如上面那个LSTORE 1,表示存储Long,在局部变量表第1个位置,对于成员方法,因为第0个位置是this。但是我们用字节码插桩时不能直接写死。newLocal(Type)方法返回值就是个局部变量表位置的索引值,至于在哪个索引值,ASM框架会自动去计算

class TraceMethodVisitor(api:Int?,
                         methodVisitor : MethodVisitor?,
                         access:Int?,
                         name:String?,
                         descriptor:String?,val className:String) :AdviceAdapter(api!!, methodVisitor, access!!, name, descriptor){

    var timeLocalIndex = 0
   
    /**
    /* 
     *  INVOKESTATIC java/lang/System.currentTimeMillis ()J
        LSTORE 1
     */
    override fun onMethodEnter() {
        super.onMethodEnter()
        println("onMethodEnter name:$name,className:${className}")
        //INVOKESTATIC java/lang/System.currentTimeMillis ()J
        invokeStatic(Type.getType("Ljava/lang/System;"),
            Method("currentTimeMillis","()J"))
        timeLocalIndex = newLocal(Type.LONG_TYPE)
        //LSTORE
        storeLocal(timeLocalIndex, Type.LONG_TYPE)
    }

XLOAD N 字节码和XSTORE N一样,只不过store是把数据存储至局部变量表,load是把局部变量表的数据推送至操作数栈顶。
还有注意的就是构造器方法,INVOKESPECIAL java/lang/StringBuilder.对应的方法invokeConstructor

/**
INVOKESTATIC java/lang/System.currentTimeMillis ()J
LSTORE 3
LDC "TAG"
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder. ()V
LLOAD 3
LLOAD 1
LSUB
INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
LDC "ms"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKESTATIC android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I
POP
RETURN
 */
override fun onMethodExit(opcode: Int) {
    super.onMethodExit(opcode)
    //INVOKESTATIC java/lang/System.currentTimeMillis ()J
    // 调用System.currentTimeMillis()
    invokeStatic(Type.getType("Ljava/lang/System;"),
        Method("currentTimeMillis","()J"))
    val index = newLocal(Type.LONG_TYPE)
    //存入局部变量表index 位置
    storeLocal(index,Type.LONG_TYPE)
    //LDC "TAG"
    //TAG字符串推送至栈顶
    visitLdcInsn("TAG")
    // NEW java/lang/StringBuilder
    newInstance(Type.getType("Ljava/lang/StringBuilder;"))
    dup()
    //INVOKESPECIAL java/lang/StringBuilder. ()V 构造器
    invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),Method("","()V"))
    //LLOAD 3
    //这里LLOAD就是我们之前存储的index位置,把index位置值推送至栈顶
    loadLocal(index,Type.LONG_TYPE)
    //LLOAD 1
    //把timeLocalIndex位置值推送至栈顶
    loadLocal(timeLocalIndex,Type.LONG_TYPE)
    //LSUB
    //栈顶两个值相减  
    math(SUB,Type.LONG_TYPE)
    // INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
    invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),Method("append","(J)Ljava/lang/StringBuilder;"))
    // LDC "ms"
    visitLdcInsn("ms")
    //INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"))
    //INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),Method("toString","()Ljava/lang/String;"))
    //INVOKESTATIC android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I
    invokeStatic(Type.getType("Landroid/util/Log;"),Method("d","(Ljava/lang/String;Ljava/lang/String;)I"))
    //POP
    pop()

    }
}

添加插件,编译完在app->build->intermediates->transform->AmsTransform目录下面的class文件里面就可以方法已经被插入了计时代码

Java字节码指令大全
深入探索编译插桩技术(四、ASM 探秘)
Hunter

你可能感兴趣的:(Gradle自定义插件(二)ASM字节码插桩打印耗时)