Skip to content

利用asm切入项目,实现全局点击事件防止点击抖动即二次点击或多次点击

License

Notifications You must be signed in to change notification settings

bigmanLau/ASM_DedounceClick_Android

Repository files navigation

[简书地址]((https://www.jianshu.com/p/3f6e7dc06b23) GITHUB地址 使用demo 该项目已开源,可以拉到文章底部查看使用方法。

技术背景:

安卓高级开发工程师

需求背景 :

用户点击课程播放,打开多次课程详情页面,典型的点击抖动问题,但是公司项目成熟稳定,不可能重新为每个点击事件添加固定的代理或者装饰类,工作量太大,所以这里我选择了AOP方案,进行class代码切入,为每个点击事件添加自己的点击拦截代码

#####读懂本篇文章需要: 1.自定义gradle插件和熟悉调试技巧,不懂得可以先移步自定义gradle插件教程 2.ASM基本常识和基本操作,不懂的可以先移步ASM教程

#####文章开始

######1.思路分析 我们通常需要拦截点击事件,无非就是监听上次点击的是哪个视图,点击间隔是否超过我们规定的,如果超过了我们就可以让他进行二次点击,否则我们拦截点击。

我们先看下面两段代码:

第一个段代码是我们常见的点击事件,这里我没有用匿名内部类,因为我们需要查看它的字节码(show bytecode outline插件)

image.png

下面我们看第二段代码截图, 第二段代码就是我们要实现的点击拦截代码的插入,这里的这个CheckClick在github上可以看看我的代码,当然你也可以自己去实现

image.png

######2.ASM插入逻辑 上面我们了解到了检测点击的过程,同时我们拿到了两次代码的差异代码 差异代码如下:

            mv.visitVarInsn(ALOAD, 1);
            mv.visitMethodInsn(INVOKEVIRTUAL, "android/view/View", "getId", "()I", false);
            mv.visitMethodInsn(INVOKESTATIC, "com/example/debounceCheck/CheckClick", "checkIsClicked", "(I)Z", false);
            Label l1 = new Label();
            mv.visitJumpInsn(IFEQ, l1);
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLineNumber(10, l2);
            mv.visitInsn(RETURN);
            mv.visitLabel(l1);
            mv.visitLineNumber(12, l1);
            mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);

现在我们要做的就是利用gradle插件的tramsform任务把代码插入到点击事件里面 1.其实关键就在两个地方,一个这ClassVisitor里面你需要过滤点击事件,因为类里面有各种各样的方法,但是我们只需要插入点击事件的方法就行,具体代码如下

public class OnClickClassVisitor extends ClassVisitor implements Opcodes {
    private String mClassName;
    private Project project;

    public OnClickClassVisitor(ClassVisitor  mv, Project project) {
        super(Opcodes.ASM6,mv);
        this.project=project;
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        System.out.println("OnClickClassVisitor:visit----->started:"+name);
        this.mClassName=name;
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        System.out.println("OnClickClassVisitor:visitMethod :"+name);
        MethodVisitor mv=cv.visitMethod(access,name,desc,signature,exceptions);
        System.out.println("OnClickClassVisitor:visitMethod ---------->started:"+this.mClassName);
        if(Utils.isViewOnclickMethod(access,name,desc)){
            System.out.println("OnClickClassVisitor : change method ---->"+name);
            return new OnClickMethodVisitor(mv,this.project);
        }else if(Utils.isListViewOnItemOnclickMethod(access,name,desc)){
            System.out.println("OnClickClassVisitor : change method ---->"+name);
            return  new OnClickMethodVisitor(mv,this.project);
        }
        return mv;
    }
}

关键在这几行

 if(Utils.isViewOnclickMethod(access,name,desc)){
            System.out.println("OnClickClassVisitor : change method ---->"+name);
            return new OnClickMethodVisitor(mv,this.project);
        }else if(Utils.isListViewOnItemOnclickMethod(access,name,desc)){
            System.out.println("OnClickClassVisitor : change method ---->"+name);
            return  new OnClickMethodVisitor(mv,this.project);
        }

这里判断了两个点击类型,一个是普通的view点击事件一个是列表点击事件,具体代码如下

 static boolean isViewOnclickMethod(int access, String name, String desc) {
    return (Utils.isPublic(access) && !Utils.isStatic(access) && !isAbstract(access)) //
        && name.equals("onClick") //
        && desc.equals("(Landroid/view/View;)V");
  }

  static boolean isListViewOnItemOnclickMethod(int access, String name, String desc) {
    return (Utils.isPublic(access) && !Utils.isStatic(access) && !isAbstract(access)) && //
        name.equals("onItemClick") && //
        desc.equals("(Landroid/widget/AdapterView;Landroid/view/View;IJ)V");
  }

这些都比较好理解 2.第二个地方我们过滤完就需要在这个方法里面插入代码了,所以我们需要改写一下

public class OnClickMethodVisitor extends MethodVisitor {
    private  Project project;
    public OnClickMethodVisitor(MethodVisitor mv, Project project) {
        super(Opcodes.ASM6,mv);
        this.project=project;
    }
    @Override
    public void visitCode() {
        super.visitCode();
            mv.visitVarInsn(ALOAD, 1);
            mv.visitMethodInsn(INVOKEVIRTUAL, "android/view/View", "getId", "()I", false);
            mv.visitMethodInsn(INVOKESTATIC, "com/example/debounceCheck/CheckClick", "checkIsClicked", "(I)Z", false);
            Label l1 = new Label();
            mv.visitJumpInsn(IFEQ, l1);
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLineNumber(10, l2);
            mv.visitInsn(RETURN);
            mv.visitLabel(l1);
            mv.visitLineNumber(12, l1);
            mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
        
    }
}

这样我们就基本完成了 我们配置了本地的上传,所以点击编辑器右侧的gradle找到uploadArchives发布代码就能使用 image.png

######3.本地使用方法 1.在项目的build.gradle配置如下 image.png 2.在app模块的build.gradle配置如下 image.png 第一个参数是你自己自定义的点击拦截类 第二个是指定要处理的包名 节省时间

######4.运行效果图 image.png

image.png

######5.使用方法

项目个目录的build.gradle引入下面代码

buildscript {
  repositories {
    maven {
      url "https://plugins.gradle.org/m2/"
    }
  }
  dependencies {
    classpath "gradle.plugin.com.bigman:testgradle:1.0.7"
  }
}

然后在你要使用的模块比如app模块使用此插件

apply plugin: "com.bigman.clickInject"
OnClickExtension{
    checkClass ="com/example/debounceCheck/CheckClick"
}

这里的com/example/debounceCheck/CheckClick就是你自定义的检测点击的方法,可以直接参考demo里的代码

About

利用asm切入项目,实现全局点击事件防止点击抖动即二次点击或多次点击

Resources

License

Stars

Watchers

Forks

Packages

No packages published