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<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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容