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插件声明如下:
- 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")
}
}
- 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)
}
}
- 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{}方式调用的插桩。