從事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
)
}
}
這裡還有一些插件開發的常識,這裡就不多說了,百度一下很多。等于一切都配置好了,我們來看下插樁後的代碼長啥樣。
好了到這裡多次點選攔截過濾功能就實作了。
奉上源碼