作者:mobiledu2502902041 | 来源:互联网 | 2023-08-22 16:50
从事Android开发的同学可能都会有这个需求,最近在学ASM相关的知识,拿这个想法练了一下手。大体思路是这样的,通过字节码Hook所有onClick(Viewview)方法,通过
从事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 ) { 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? ): 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, 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( Opcodes.LONG, Opcodes.INTEGER, "java/lang/Object" ), 0, null ) } }
这里还有一些插件开发的常识,这里就不多说了,百度一下很多。等于一切都配置好了,我们来看下插桩后的代码长啥样。
好了到这里多次点击拦截过滤功能就实现了。
奉上源码