1、方针

运用asm字节码插桩的办法,完结给点击事情加上防抖计算办法耗时的功用

2、api介绍

1、Transform API

Transform API 是 AGP1.5 就引入的特性,Android在构建过程中回将Class转成Dex,此API便是供给了在此过程中刺进自定逻辑字节码的功用,咱们能够运用此API做一些功用,比如无痕埋点,耗时计算等功用。不过此API在AGP7.0已经被抛弃,8.0会被移除,取而代之的是Transform Action

2、Transform Action

Transform Action是有Gradle供给的,直接运用Transform Action会有点麻烦,AGP为咱们封装了一层AsmClassVisitorFactory,咱们一般能够使运用AsmClassVisitorFactory,这样代码量会削减,而且功用还有提升。简略运用的话,整体流程跟Transform API差不多。

然后咱们知道ASM有两套API,core api 和tree api(blog.51cto.com/lsieun/4088…),详细差异能够看下链接,tree api运用会更方便一些,完结一些功用会更简略,不过功用上会比core api差一些。由于Transform API抛弃了,所以接下来都是以Transform Action为例子。

3、完结计划

咱们运用plugin的办法编写插桩代码,然后将它publish到本地,然后在对应工程引用这个plugin

1、新建plugin

这个网上有许多资料,可自行查找,便是配置resources目录,新建 .properties文件,在build.gradle中配置publishing{} 即可。

Android asm字节码插桩点击防抖以及统计方法耗时

moudle的build.gradle文件中增加一下

group "com.trans.test.plugin"
version "1.0.0"
publishing{ //当时项目能够发布到本地文件夹中
    repositories {
        maven {
            url= '../repo' //定义本地maven仓库的地址
        }
    }
    publications {
        PublishAndroidAssetLibrary(MavenPublication) {
            groupId group
            artifactId artifactId
            version version
        }
    }
}

当修改plugin的代码后,记住publish一下,以更新下本地库


运用的module在build.gradle引用一下

apply plugin: com.example.transformaction.AsmPlugin

根目录的setting.grdle中导入本地途径

maven { url('./repo') }

2、编写插桩代码,这儿叙述一下大约的逻辑

1、过滤一切需求的办法

1、正常的点击setOnclickListener(),页面完结OnclickListener接口,重写onClick()办法

2、匿名内部类setOnclickListener()

3、xml点击事情

4、ButterKnife点击事情

2、对办法进行hook插桩

根本逻辑便是,咱们用kotlin完结一个“防抖”的功用,然后将这个功用的调用代码以字节码的办法刺进到需求hook的办法中(详细完结下面会阐明)

4、详细完结过程

1、先完结一个Plugin

class AsmPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        println("我是插件")
        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.onVariants { variant ->
            variant.instrumentation.transformClassesWith(
                MyTestTransform::class.java,
                InstrumentationScope.PROJECT) {params->
                params.config.set(ViewDoubleClickConfig())
            }
            variant.instrumentation.setAsmFramesComputationMode(
                FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
            )
        }
        println("插件刺进完结")
    }
}

从代码能够看出,(Transform API 中咱们运用AppExtension)Transform Action运用AndroidComponentsExtension来获取组件,然后一次刺进咱们班自定义的MyTestTransform来刺进咱们的字节码。

2、完结MyTestTransform

interface DoubleClickParameters : InstrumentationParameters {
    @get:Input
    val config: Property<ViewDoubleClickConfig>
}
abstract class MyTestTransform: AsmClassVisitorFactory<DoubleClickParameters> {
    override fun createClassVisitor(classContext: ClassContext, nextClassVisitor: ClassVisitor): ClassVisitor {
        return TreeTestVisitor(
           nextClassVisitor = nextClassVisitor,
           config = parameters.get().config.get()
       )
        // return CoreClassVisitor(nextClassVisitor)
    }
    override fun isInstrumentable(classData: ClassData): Boolean {
        return true
    }
}

DoubleClickParameters是用来传参的,TreeTestVisitor运用了传参的办法,所以运用AsmClassVisitorFactory泛型用了DoubleClickParameters。以上代码能够看出MyTestTransform内部createClassVisitor需求返回一个ClassVisitor,咱们用两种完结办法(core api 和 tree api )来演示下。

3、TreeTestVisitor(tree api)

class TreeTestVisitor(
    private val nextClassVisitor: ClassVisitor,
    private val config: ViewDoubleClickConfig
) : ClassNode(Opcodes.ASM5) {
    private val extraHookPoints = listOf(
        ViewDoubleClickHookPoint(
            interfaceName = "android/view/View$OnClickListener",
            methodName = "onClick",
            nameWithDesc = "onClick(Landroid/view/View;)V"
        ),
        ViewDoubleClickHookPoint(
            interfaceName = "com/chad/library/adapter/base/listener/OnItemClickListener",
            methodName = "onItemClick",
            nameWithDesc = "onItemClick(Lcom/chad/library/adapter/base/BaseQuickAdapter;Landroid/view/View;I)V"
        ),
        ViewDoubleClickHookPoint(
            interfaceName = "com/chad/library/adapter/base/listener/OnItemChildClickListener",
            methodName = "onItemChildClick",
            nameWithDesc = "onItemChildClick(Lcom/chad/library/adapter/base/BaseQuickAdapter;Landroid/view/View;I)V",
        )
)
    override fun visitEnd(){
         // 这儿便是遍历methods 对办法逐个进行Visitor
         // 这儿咱们要做的便是取出一切的onClick时事情然后逐个刺进对应的字节码
         // 点击事情有几种状况
         // 1、正常的点击setOnclickListener(),页面完结OnclickListener接口,重写onClick()办法
         // 2、匿名内部类setOnclickListener()
         // 3、xml点击事情
         // 4、ButterKnife点击事情
        // 以上其实能够分为三类 
        // 1、运用注解,判别注解  hasAnnotation()
        // 2、经过MethodNode的interfaces数组,来判别是完结onClickLitener接口(包括列表的onItemClickLisener等)
        // 3、lambda表达式的办法
        super.visitEnd()
        val shouldHookMethodList = mutableSetOf<MethodNode>()
        methods.forEach { methodNode ->
            //运用了 ViewAnnotationOnClick 自定义注解的状况
            methodNode.hasAnnotation("Lcom/example/transformsaction/annotation/ViewAnnotationOnClick;") -> {
                shouldHookMethodList.add(methodNode)
            }
            //运用了 Butterknife 注解的状况
            methodNode.hasAnnotation("Lbutterknife/OnClick;") -> {
                shouldHookMethodList.add(methodNode)
            }
            //运用了匿名内部类的状况
            methodNode.isHookPoint() -> {
                shouldHookMethodList.add(methodNode)
            }
            //判别办法内部是否有需求处理的 lambda 表达式
            val dynamicInsnNodes = methodNode.filterLambda {
                val nodeName = it.name
                val nodeDesc = it.desc
                val find = extraHookMethodList.find { point ->
                    nodeName == point.methodName && nodeDesc.endsWith(point.interfaceSignSuffix)
                }
                find != null
            }
            dynamicInsnNodes.forEach {
                val handle = it.bsmArgs[1] as? Handle
                if (handle != null) {
                    //找到 lambda 指向的方针办法
                    val nameWithDesc = handle.name + handle.desc
                    val method = methods.find { it.nameWithDesc == nameWithDesc }!!
                    shouldHookMethodList.add(method)
                }
            }    
        }
        shouldHookMethodList.forEach {
            hookMethod(modeNode = it)
        }
        accept(nextClassVisitor)
    }
    // MethodNode的拓宽办法,判别注解
    // 举个栗子:咱们新定义了一个注解 ViewAnnotationOnClick
    // annotationDesc就对应 ViewAnnotationOnClick的全限制途径,
    // 即:"Lcom/example/transformsaction/annotation/ViewAnnotationOnClick;"
    fun MethodNode.hasAnnotation(annotationDesc: String): Boolean {
    	return visibleAnnotations?.find { it.desc == annotationDesc } != null
	}
    // MethodNode的拓宽办法,匿名内部类
    private fun MethodNode.isHookPoint(): Boolean {
        val myInterfaces = interfaces
        if (myInterfaces.isNullOrEmpty()) {
            return false
        }
        extraHookMethodList.forEach {
            if (myInterfaces.contains(it.interfaceName) && this.nameWithDesc == it.nameWithDesc) {
                return true
            }
        }
        return false
    }
    // MethodNode的拓宽办法,lambda表达式
    fun MethodNode.filterLambda(filter: (InvokeDynamicInsnNode) -> Boolean): List<InvokeDynamicInsnNode> {
        val mInstructions = instructions ?: return emptyList()
        val dynamicList = mutableListOf<InvokeDynamicInsnNode>()
        mInstructions.forEach { instruction ->
            if (instruction is InvokeDynamicInsnNode) {
                if (filter(instruction)) {
                    dynamicList.add(instruction)
                }
            }
        }
        return dynamicList
	}
    // 给过滤后的办法刺进字节码
    private fun hookMethod(modeNode: MethodNode) {
        // 取出描绘
        val argumentTypes = Type.getArgumentTypes(modeNode.desc)
        // 得出对应描绘类型在该办法参数中的位置 
        //(主要是新建ViewDoubleClickCheck。onClick(view:View)有个入参,要取被hook函数的参数传入hook办法中)
        val viewArgumentIndex = argumentTypes?.indexOfFirst {
            it.descriptor == ViewDescriptor
        } ?: -1
        if (viewArgumentIndex >= 0) {
            val instructions = modeNode.instructions
            if (instructions != null && instructions.size() > 0) {
                // 刺进防抖的字节码
                val listCheck = InsnList()
                // 得收支参要取被hook函数的位置
                val index =  getVisitPosition(
                    argumentTypes,
                    viewArgumentIndex,
                    modeNode.isStatic
                )
                //参数
                listCheck.add(
                    VarInsnNode(
                        Opcodes.ALOAD, index
                    )
                )
                // 刺进ViewDoubleClickCheck的调用函数的字节码
                listCheck.add(
                    MethodInsnNode(
                        Opcodes.INVOKESTATIC,
                        "com/example/transformsaction/view/ViewDoubleClickCheck",
                        "onClick",
                        "(Landroid/view/View;)Z"
                    )
                )
                // 由于是刺进的字节码为判别语句,不满足的需求return
                val labelNode = LabelNode()
                listCheck.add(JumpInsnNode(Opcodes.IFNE, labelNode))
                listCheck.add(InsnNode(Opcodes.RETURN))
                listCheck.add(labelNode)
                //将新建的字节码刺进instructions中
                instructions.insert(listCheck)
                // 意图是在办法末尾刺进字节码
                for( node in instructions){
                    //判别是不是办法结束的AbstractInsnNode
                    if(node.opcode == Opcodes.ARETURN || node.opcode == Opcodes.RETURN){
                        System.out.println("找到了")
                        // 创立字节码容器
                        val listEnd = InsnList()
                        // 字节码办法参数
                        listEnd.add(
                            VarInsnNode(
                                Opcodes.ALOAD, index
                            )
                        )
                        // 刺进ToastClick.endClick()
                        listEnd.add(
                            MethodInsnNode(
                                Opcodes.INVOKESTATIC,
                                "com/example/transformsaction/view/ToastClick",
                                "endClick",
                                "()V"
                            )
                        )
                        // 将字节码刺进到结束node之前,运用insertBefore
                        instructions.insertBefore(node,listEnd)
                    }
                }
                // 在办法开端刺进字节码
                val list = InsnList()
                list.add(
                    VarInsnNode(
                        Opcodes.ALOAD, index
                    )
                )
                // 刺进ToastClick.startClick()
                list.add(
                    MethodInsnNode(
                        Opcodes.INVOKESTATIC,
                        "com/example/transformsaction/view/ToastClick",
                        "startClick",
                       "()V"
                    )
                )
                instructions.insert(list)
            }
        }
    }
}

InsnList

刺进字节码的时分是对过滤后的shouldHookMethodList逐个进行字节码刺进,也便是调用 InsnList的insert办法,简略说下InsnList,InsnList供给许多刺进字节码的办法:

add(final AbstractInsnNode insnNode)
末尾刺进一个AbstractInsnNode
add(final InsnList insnList)
末尾刺进一组InsnList
insert(final AbstractInsnNode insnNode)
头部刺进一个AbstractInsnNode
insert(final InsnList insnList)
头部刺进一组InsnList
insert(final AbstractInsnNode previousInsn, final AbstractInsnNode insnNode)
在previousInsn后刺进一个AbstractInsnNode
insert(final AbstractInsnNode previousInsn, final InsnList insnList)
在previousInsn后刺进一组InsnList
insertBefore(final AbstractInsnNode nextInsn, final AbstractInsnNode insnNode)
在previousInsn前刺进一个AbstractInsnNode
insertBefore(final AbstractInsnNode nextInsn, final InsnList insnList)
在previousInsn前刺进一组InsnList

以上仅仅部分办法,有兴趣的能够去看下源码。

4、CoreClassVisitor (core api)

class CoreClassVisitor(nextVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM7, nextVisitor) {
    override fun visitMethod(
        access: Int,
        name: String,
        desc: String,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        val methodVisitor = super.visitMethod(access, name, desc, signature, exceptions)
        return MyClickVisitor(Opcodes.ASM7,methodVisitor,access,name,desc)
    }
}

便是重写一下visitMethod函数,然后将详细的逻辑传入到MyClickVisitor去完结,这儿只演示在办法刺进防抖的完结

class MyClickVisitor(api: Int, methodVisitor: MethodVisitor?, access: Int, name: String?,
                     val descriptor: String?
) : AdviceAdapter(api, methodVisitor,
    access,
    name, descriptor
) {
    // 注解缓存
    var visibleAnnotations: ArrayList<AnnotationNode>? = null
    // 获取注解 参阅了tree api
    override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {
        val annotation = AnnotationNode(descriptor)
        if(null == visibleAnnotations){
            visibleAnnotations = ArrayList()
        }
        if (visible) {
            println("增加注解:"+ descriptor)
            visibleAnnotations?.add(annotation)
        }
        return annotation
    }
    override fun onMethodEnter() {
        val viewArgumentIndex = argumentTypes?.indexOfFirst {
            it.descriptor == ViewDescriptor
        } ?: -1
        println("打印注解列表长度"+visibleAnnotations?.size)
        // 
        if (matchMethod(name, descriptor) || matchExitMethod()) {
            println("阻拦一个")
            mv.visitVarInsn(ALOAD, getVisitPosition(
                argumentTypes,
                viewArgumentIndex,
                access and Opcodes.ACC_STATIC != 0
            ))
            mv.visitMethodInsn(
                INVOKESTATIC,
                "com/example/transformsaction/view/ViewDoubleClickCheck",
                "onClick",
                "(Landroid/view/View;)Z",
                false
            )
            val label0 = Label()
            mv.visitJumpInsn(IFNE, label0)
            mv.visitInsn(RETURN)
            mv.visitLabel(label0)
        }
        super.onMethodEnter()
    }
    private fun matchMethod(name: String, desc: String?): Boolean {
        println("阻拦判别$name  $desc")
        return  (name == "onClick" && desc == "(Landroid/view/View;)V")
                || (name == "onItemClick" && desc == "(Lcom/chad/library/adapter/base/BaseQuickAdapter;Landroid/view/View;I)V")
                || (name == "onItemChildClick" && desc == "(Lcom/chad/library/adapter/base/BaseQuickAdapter;Landroid/view/View;I)V")
    }
    private fun matchExitMethod(): Boolean {
        return (hasCheckViewAnnotation() || hasButterKnifeOnClickAnnotation())
    }
    private fun hasCheckViewAnnotation(): Boolean {
        return hasAnnotation("Lcom/example/transformsaction/annotation/ViewAnnotationOnClick;")
    }
    private fun hasButterKnifeOnClickAnnotation(): Boolean {
        return hasAnnotation("Lbutterknife/OnClick;")
    }
    fun hasAnnotation(annotationDesc: String): Boolean {
        var value = visibleAnnotations?.find { it.desc == annotationDesc } != null
        println("判别注解:"+ value)
        return value
    }
}

完结逻辑根本跟TreeTestVisitor差不多,无非便是一个是用InsnList,另一个运用MethodVisitor的api进行刺进。有一点便是lambda表达式的hook,我没想到好的办法,参阅了tree api,重写visitInvokeDynamicInsn办法,调用时就创立一个InvokeDynamicInsnNode,保存到缓存列表里,然后经过判别保存的列表是否包括对应的接口以及办法,就能够判别对应的lambda是否是方针办法:

override fun visitInvokeDynamicInsn(
  name: String?,
  descriptor: String?,
  bootstrapMethodHandle: Handle?,
  vararg bootstrapMethodArguments: Any?
) {
  println("增加lambda:"+ descriptor+"  "+name)
  instructions.add(
      InvokeDynamicInsnNode(
          name, descriptor?.split(")")?.get(1) ?: descriptor, bootstrapMethodHandle, *bootstrapMethodArguments
      )
  )
}
fun filterLambda(filter: (InvokeDynamicInsnNode) -> Boolean): List<InvokeDynamicInsnNode> {
    val mInstructions = instructions
    val dynamicList = mutableListOf<InvokeDynamicInsnNode>()
    mInstructions.forEach { instruction ->
        if (instruction is InvokeDynamicInsnNode) {
            if (filter(instruction)) {
                dynamicList.add(instruction)
            }
        }
    }
    return dynamicList
}

但是获取到是在onMethodEnter之后,没找到像InsnList一样在各个位置刺进字节码的api,后续再优化吧。

比照一下刺进之前和之后的代码吧:

Android asm字节码插桩点击防抖以及统计方法耗时

Android asm字节码插桩点击防抖以及统计方法耗时

完美!!!

5、总结

根本逻辑是如上述所示,完结字节码插桩,主要考虑两个个问题:

1、字节码插桩位置在哪?

便是怎样去过滤对应的办法,运用tree api能够经过MethodNode内部的变量来过滤,即visibleAnnotations(注解),interfaces(完结的接口),instructions(可用于判别lambda表达式对应的概要害信息,以点击事情为例,lambdab表达式办法终究会被转成一个静态办法,办法名类似于“onCreatelambdalambda0”,要害信息会被放在instructions中,所以能够经过instructions判别。

2、怎样把字节码刺进进去 ?

便是对asm API的调用了,这个慢慢学习吧

6、道谢

本文是参阅了 /post/704232… ASM 字节码插桩:完结双击防抖,感谢大佬,原文有源码,可自行下载。

我看往后,想对自己理的解做一下整理,所以才有此文。自己的代码地址gitee.com/wlr123/tran…

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。