美文网首页
Gradle自定义插件(二)ASM字节码插桩打印耗时

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

作者: 陆元伟 | 来源:发表于2021-08-10 20:38 被阅读0次

    前面我们了解了自定义插件的基础流程
    我们现在利用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<QualifiedContent.ContentType> {
            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<in QualifiedContent.Scope> {
            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方法,一般我们都是在方法上处理,因此主要也是关注这个方法。因为构造器方法<init>和类加载时方法<clinit>方法是编译器自动生成。我们一般不需要在这类方法上面处理。因此过滤掉。方法处理主要是在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<out String>?) {
                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<out String>?): MethodVisitor {
                println("visitMethod name:$name,className:${className}")
                val mv = cv.visitMethod(access,name,desc,signature,exceptions)
                 name?.let {
                      n->
                    isHandler = (n!="<init>"&&n!="<clinit>")
                  }
                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.<init>对应的方法invokeConstructor

    /**
    INVOKESTATIC java/lang/System.currentTimeMillis ()J
    LSTORE 3
    LDC "TAG"
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()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.<init> ()V 构造器
        invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),Method("<init>","()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字节码插桩打印耗时

          本文链接:https://www.haomeiwen.com/subject/scrxtltx.html