Android 通过ASM实现多次点击拦截
程序员文章站
2022-08-15 16:27:03
从事Android开发的同学可能都会有这个需求,最近在学ASM相关的知识,拿这个想法练了一下手。大体思路是这样的,通过字节码Hook所有onClick(View view)方法,通过view.setTag(key,value)设置tag为当前时间戳,这样再次点击的时候就有一个时间差,通过对这个时间差,可以过滤掉多余的响应操作。首先我们看一下lamba表达式和普通的setOnClickListener编译完是什么样的。由截图可以看到不管我们以哪种方式设置监听点击,最终都是一个实现View.OnCl...
从事Android开发的同学可能都会有这个需求,最近在学ASM相关的知识,拿这个想法练了一下手。大体思路是这样的,通过字节码Hook所有onClick(View view)方法,通过view.setTag(key,value)设置tag为当前时间戳,这样再次点击的时候就有一个时间差,通过对这个时间差,可以过滤掉多余的响应操作。
首先我们看一下lamba表达式和普通的setOnClickListener编译完是什么样的。
由截图可以看到不管我们以哪种方式设置监听点击,最终都是一个实现View.OnClickListener接口的静态内部类,由此我们可以Hook所有实现了View.OnClickListener接口的类中的名字为onClick,签名为(Landroid/view/View;)V的方法,在方法前面插入我们想要的代码。具体实现是这样的:
定义一个ClassVisitor:
class MutiClickHandleVisitor(classVisitor: ClassVisitor): ClassVisitor(Opcodes.ASM5,classVisitor) {
private val classFullName = "android/view/View\$OnClickListener"
private var isMatchClass = false
override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<String>
) {
super.visit(version, access, name, signature, superName, interfaces)
isMatchClass = matchClass(interfaces, classFullName)
}
override fun visitMethod(
access: Int,
name: String,
desc: String,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val mv = cv.visitMethod(access, name, desc, signature, exceptions)
if (isMatchClass && matchMethod(name, desc)){
return MutiClickHandleMethodAdapter(mv)
}
return mv
}
private fun matchMethod(name: String, desc: String): Boolean {
return name == "onClick" && desc == "(Landroid/view/View;)V"
}
private fun matchClass(
interfaces: Array<String>,
classFullName: String
): Boolean {
var isMatch = false
// 是否满足实现的接口
for (anInterface in interfaces) {
if (anInterface == classFullName) {
isMatch = true
break
}
}
return isMatch
}
}
其中matchMethod方法就是确保名字和签名符合预期即 name =="onClick" && desc =="(Landroid/view/View;)V"
接着定义一个MethodVisitor:
class MutiClickHandleMethodAdapter(methodVisitor: MethodVisitor) : MethodVisitor(Opcodes.ASM5,methodVisitor) {
override fun visitCode() {
super.visitCode()
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
mv.visitVarInsn(Opcodes.LSTORE, 2)
mv.visitVarInsn(Opcodes.ALOAD, 1)
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/view/View", "getId", "()I", false)
mv.visitVarInsn(Opcodes.ISTORE, 4)
mv.visitVarInsn(Opcodes.ALOAD, 1)
mv.visitVarInsn(Opcodes.ILOAD, 4)
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/view/View", "getTag", "(I)Ljava/lang/Object;", false)
mv.visitVarInsn(Opcodes.ASTORE, 5)
mv.visitVarInsn(Opcodes.ALOAD, 1)
mv.visitVarInsn(Opcodes.ILOAD, 4)
mv.visitVarInsn(Opcodes.LLOAD, 2)
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false)
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/view/View", "setTag", "(ILjava/lang/Object;)V", false)
mv.visitVarInsn(Opcodes.ALOAD, 5)
val l5 = Label()
mv.visitJumpInsn(Opcodes.IFNULL, l5)
mv.visitVarInsn(Opcodes.ALOAD, 5)
mv.visitTypeInsn(Opcodes.CHECKCAST, "java/lang/Long")
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false)
mv.visitVarInsn(Opcodes.LSTORE, 6)
mv.visitVarInsn(Opcodes.LLOAD, 2)
mv.visitVarInsn(Opcodes.LLOAD, 6)
mv.visitInsn(Opcodes.LSUB)
mv.visitLdcInsn(1500L)
mv.visitInsn(Opcodes.LCMP)
mv.visitJumpInsn(Opcodes.IFGE, l5)
mv.visitInsn(Opcodes.RETURN);
mv.visitLabel(l5)
mv.visitFrame(
Opcodes.F_APPEND,
3,
arrayOf<Any>(
Opcodes.LONG,
Opcodes.INTEGER,
"java/lang/Object"
),
0,
null
)
}
}
这里还有一些插件开发的常识,这里就不多说了,百度一下很多。等于一切都配置好了,我们来看下插桩后的代码长啥样。
好了到这里多次点击拦截过滤功能就实现了。
本文地址:https://blog.csdn.net/s127838498/article/details/107968840