当前位置: 首页>后端>正文

Android-ASM处理点击事件埋点

ASM(All-purpose Java bytecode manipulation and analysis framework)是一个强大的工具,它允许您修改现有的Java类或动态生成新类1。它提供了广泛的字节码转换和分析算法,可用于构建自定义复杂转换和代码分析工具1。ASM旨在小巧、快速、高性能,适用于编译器等动态系统1

ASM官方网址:ASM (ow2.io)

集成方式:
项目选择新建Java Library模块的buildSrc目录,去掉settings.gradle中的buildSrc的引用声明。buildSrc文件基本配置如下:

plugins {
    `kotlin-dsl`
    `java-gradle-plugin`
    id("maven-publish")
}

dependencies {
    implementation(gradleApi())
    implementation("org.ow2.asm:asm:9.5")
    implementation("org.ow2.asm:asm-util:9.5")
    implementation("org.ow2.asm:asm-commons:9.5")
    implementation("org.ow2.asm:asm-analysis:9.5")
    implementation("org.ow2.asm:asm-tree:9.5")
    implementation("com.android.tools.build:gradle-api:8.1.1")
}

repositories {
    google()
    mavenCentral()
}

gradlePlugin {
    plugins {
        create("asm") {
            id = "com.xxx.asm" //定义插件ID,必填
            implementationClass = "com.xxx.asm.AsmPlugin" //定义插件主类,必填
        }
    }
}

publishing {
    repositories {
        maven {
            group = "com.xxx.asm" //定义插件GroupId,必填
            version = "1.0.0" //定义插件版本,必填
            url = uri("../repo") //插件发布位置,这里发布的是本地
        }
    }
}

对于AGP8.0以下版本,网上多数文章教程是ASM+Gradle-Transform处理字节码,但是AGP8.0及以后的版本不再支持Transform的操作,因此对埋点等事件造成了一定的影响。还有一个就是Kotlin中Lambda使用,在ASM中无法找到对应方法的一个通用规则,暂时还没找到好的方案,只能通过新建一个ViewOnClickListener类继承View.OnClickListener,在onClick方法中插入内容。

新版中,使用project的instrumentation对象代替transform类处理转换,ASM插件声明如下:

  1. AsmExtension & AsmPlugin 类
// 声明插件配置信息
open class AsmExtension {

   // 是否可用
    var enabled: Boolean = false

    var onClickListener: String= null

}

class AsmPlugin : Plugin<Project> {

    override fun apply(target: Project) {
        println("================== AsmPlugin is Running =================")
        val extension = target.extensions.create("AsmConfig", AsmExtension::class.java)
        val androidComponents = target.extensions
            .getByType(AndroidComponentsExtension::class.java)
        androidComponents.onVariants { variant ->
            variant.instrumentation.transformClassesWith(
                ViewOnClickClassVisitorFactory::class.java,
                InstrumentationScope.ALL) { it.enabled.set(extension.enabled) }
            variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
        }
//        target.afterEvaluate {
//            println("=====> 当前配置的参数:enabled=${extension.enabled},onClickListener=${extension.onClickListener}")
//            if (extension.enabled) {
//                println("====> started!")
//                ViewClickHandlerVisitor(Opcodes.ASM9)
//            }
//        }
    }

    interface ViewOnClickParams : InstrumentationParameters {

        @get:Input
        val enabled: Property<Boolean>

    }

    abstract class ViewOnClickClassVisitorFactory : AsmClassVisitorFactory<ViewOnClickParams> {

        override fun createClassVisitor(
            classContext: ClassContext,
            nextClassVisitor: ClassVisitor
        ): ClassVisitor {
            // 如果插件启用,则执行ClassVisitor操作
            return if (parameters.get().enabled.get()) {
                ViewClickHandlerVisitor(Opcodes.ASM9, nextClassVisitor)
            } else {
                TraceClassVisitor(nextClassVisitor, PrintWriter(File("trace_out")))
            }
        }

        // 这里判断需要对哪些类进行浏览,一般建议默认设置为:true
        override fun isInstrumentable(classData: ClassData): Boolean = true
            // classData.className.startsWith("com.xxx.analysis")

    }

}
  1. ViewClickHandlerVisitor类,进行类浏览和方法筛选
class ViewClickHandlerVisitor @JvmOverloads constructor(
    api: Int,
    classVisitor: ClassVisitor= null
) : ClassVisitor(api, classVisitor) {

    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 class】name=$name\nsignature=$signature\nsuperName=$superName\ninterfaces=$interfaces\n\n")
    }

    override fun visitMethod(access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
        println("====>方法信息: $access,$name,$descriptor,$signature,$exceptions")
        // 这里判断,我们检索的onClick方法是公开的且非静态的,参数只有View
        if (name == "onClick"
            && access.and(Opcodes.ACC_PUBLIC) == 1
            && access.and(Opcodes.ACC_STATIC) == 0
            && descriptor == "(Landroid/view/View;)V") {
            println("======> I found the setOnClickListener method!")
            val mv = cv.visitMethod(access, name, descriptor, signature, exceptions)
            return ViewClickMethodAdapter(Opcodes.ASM9, mv, access, name, descriptor)
        }
        return super.visitMethod(access, name, descriptor, signature, exceptions)
    }

}
  1. ViewClickMethodAdapter类,对onClick方法进行修改处理,在方法末尾进行代码插桩。
class ViewClickMethodAdapter(api: Int, methodVisitor: MethodVisitor?, access: Int, name: String?, descriptor: String?) : AdviceAdapter(api, methodVisitor, access, name, descriptor) {

    override fun onMethodExit(opcode: Int) {
        super.onMethodExit(opcode)
        //方法后面追加2333333333333333333输出
        run {
            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;")
            mv.visitLdcInsn("233333333333333333333333333")
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false)
        }
        /// 调用一个类,输出Click事件的接收
        run {
            mv.visitVarInsn(Opcodes.ALOAD, 1)
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/xxx/analysis/AnalysisUtils", "onReceivedClickEvent", "(Landroid/view/View;)V", false);
        }
        mv.visitMaxs(1, 1) //这里可能有写法错误
        mv.visitEnd()
    }

    override fun visitInvokeDynamicInsn(name: String?, descriptor: String?, bootstrapMethodHandle: Handle?, vararg bootstrapMethodArguments: Any?) {
        println("===================================> lambda: $name, $descriptor, $bootstrapMethodHandle, $bootstrapMethodArguments")
        super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, *bootstrapMethodArguments)
    }

}

编译publish即可,主项目引入插件:

plugins { id("com.xxx.asm") }
// 配置插件参数
AsmConfig {
    enabled = true
    onClickListener = ""
}
//即可,当主项目中有类出现 class XXXClass { onClick(View) },插入的代码即会在点击后响应

这是目前对ASM点击事件插桩的处理,但是Kt-Lambda调用点击事件还没法处理,还在继续探索中。还有一个就是无法检索到:android.view.View类。

其实对于Lambda的处理,可以参考生成后的class文件,看看lambda的调用被生成了什么规则的方法。我这边根据参考的结果看,lambda方法会生成为:
ACC_PRIVATE + ACC_STATIC + AnchorClassName + $lambda$Number 这样的方法,并且方法会调用MethodVisitor里面的visitDynamicInsn的方法,里面会调用onClick方法名。因此,大致会有如下的判断代码。

判断OnClick方法的逻辑

 /**
     * 判断是否是点击方法
     * @param name 方法名称
     * @param desc 方法描述
     * @param access 方法修饰符
     */
    fun isClickMethod(name: String?, desc: String?, access: Int): Boolean {
        return !name.isNullOrEmpty()
            && !desc.isNullOrEmpty()
            && access.and(Opcodes.ACC_PUBLIC) == Opcodes.ACC_PUBLIC
            && name == ViewClickMethodConstants.ViewOnClickMethodName
            && desc == ViewClickMethodConstants.ViewOnClickMethodDescriptor
    }

    /**
     * 查找满足条件的方法
     * 范例:onCreateView$lambda
    override fun visitInvokeDynamicInsn(name: String?, descriptor: String?, bootstrapMethodHandle: Handle?, vararg bootstrapMethodArguments: Any?) {
        // 确定它是点击事件
        if (name == ViewClickMethodConstants.ViewOnClickMethodName
            && descriptor?.endsWith(ViewClickMethodConstants.OnClickListenerDescriptor) == true) {
            println("lambda: visitInvokeDynamicInsn ===================================> \nname = $name, \n" +
                "descriptor = $descriptor,  \n" +
                "bootstrapMethodHandle = ${bootstrapMethodHandle?.name}|${bootstrapMethodHandle?.desc}|${bootstrapMethodHandle?.owner},  \n" +
                "bootstrapMethodArguments = $bootstrapMethodArguments")
            println("bootstrapMethodHandle:")
            println("name=${bootstrapMethodHandle?.name}")
            println("desc=${bootstrapMethodHandle?.desc}")
            println("owner=${bootstrapMethodHandle?.owner}")
            println("isInterface=${bootstrapMethodHandle?.isInterface}")
            println("tag=${bootstrapMethodHandle?.tag}")

            val dynamicNode = InvokeDynamicInsnNode(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments)
            val tag = ClickMethodTagInfo(methodNode.name, name, dynamicNode.desc)
            ViewClickRecordPool.clickLambdaMethodLookup.add(tag)
            println("dynamicNode ================ 动态方法调用")
            println("methodNode ================  ${methodNode.name}")
            println("name ================  ${dynamicNode.name}")
            println("desc ================  ${dynamicNode.desc}")
            println("bsm  ================ ${dynamicNode.bsm?.name}, ${dynamicNode.bsm?.desc}")
            println("bsmArgs  ================ ${dynamicNode.bsmArgs}")
        }
        super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, *bootstrapMethodArguments)
    }
, (Ljava/lang/Object;Landroid/view/View;)V
     * @param name 方法名,onCreateView$lambda    override fun visitMethod(access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
        val mv = cv.visitMethod(access, name, descriptor, signature, exceptions)
        return if (ViewClickRecordPool.isClickMethod(name, descriptor, access)) {
            println("【Method】:$name\ndescriptor = $descriptor\nsignature = $signature\ninterfaces = $exceptions\n\n")
            ViewClickMethodAdapter(api, mv, access, name, descriptor, false)
        } else if (ViewClickRecordPool.isClickLambdaMethod(name, descriptor, access, ViewClickRecordPool.clickLambdaMethodLookup)) {
            println("【Method】:$name\ndescriptor = $descriptor\nsignature = $signature\ninterfaces = $exceptions\n\n")
            ViewClickMethodAdapter(api, mv, access, name, descriptor, true)
        } else {
            val methodNode = MethodNode(api, access, name, descriptor, signature, exceptions)
            ViewClickMethodDetectAdapter(api, mv, access, name, descriptor, methodNode)
        }
    }

     * @param desc 方法描述 (Ljava/lang/Object;Landroid/view/View;)V
     * @param access 方法修饰符 ACC_PRIVATE, ACC_STATIC
     * @param lookup 方法查询标记,看该方法是否已经被lambda标记过
     */
    fun isClickLambdaMethod(name: String?, desc: String?, access: Int, lookup: List<ClickMethodTagInfo>): Boolean {
        if (name.isNullOrEmpty() || desc.isNullOrEmpty()) return false
        val returnType = Type.getReturnType(desc)
        val methodArgTypes = Type.getArgumentTypes(desc)
        return (
            access.and(Opcodes.ACC_PRIVATE) == Opcodes.ACC_PRIVATE //lambda调用的方法一定是一个私有方法
                && access.and(Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC //lambda调用的方法一定是一个静态方法
                && returnType == Type.VOID_TYPE // //lambda调用的方法的返回值是个Void对象
                && methodArgTypes.lastOrNull()?.descriptor == ViewClickMethodConstants.ViewDescriptor //方法参数的匹配,最后一个一定是View对象
                && lookup.any { name.matches("${it.anchorMethodName}\$lambda\$\d+".toRegex()) }
            ) //表示满足Lambda方法命名的匹配
    }

发现Lambda方法有被使用的痕迹,做记录:


当上面一段内容执行完成后,它的整个Lambda方法生成的Java静态方法会被调用,因此会有如下一段代码:
检索Lambda方法在visitMethod中的出现。


最后

目前通过以上的一些操作步骤,我已经完成了对onClick方法插桩操作,包括在kotlin使用的setOnClickListener{}方式调用的插桩。


Android-ASM处理点击事件埋点,第1张
未编译前的代码
Android-ASM处理点击事件埋点,第2张
编译后的class

https://www.xamrdz.com/backend/3wf1935391.html

相关文章: