天天看點

Android 通過ASM實作多次點選攔截

從事Android開發的同學可能都會有這個需求,最近在學ASM相關的知識,拿這個想法練了一下手。大體思路是這樣的,通過位元組碼Hook所有onClick(View view)方法,通過view.setTag(key,value)設定tag為目前時間戳,這樣再次點選的時候就有一個時間差,通過對這個時間差,可以過濾掉多餘的響應操作。

首先我們看一下lamba表達式和普通的setOnClickListener編譯完是什麼樣的。

Android 通過ASM實作多次點選攔截

由截圖可以看到不管我們以哪種方式設定監聽點選,最終都是一個實作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
        )

    }

}
           

這裡還有一些插件開發的常識,這裡就不多說了,百度一下很多。等于一切都配置好了,我們來看下插樁後的代碼長啥樣。

Android 通過ASM實作多次點選攔截

好了到這裡多次點選攔截過濾功能就實作了。

奉上源碼