AOP解决快速误点问题

引言

在Android开发过程中,可能机器配置不够,各种响应慢了,界面跳转响应之类有一点延迟,而用户进行了快速点击,就导致了响应多次执行了,这样就很影响实际的体验了。所以也就有了此文,怎么处理好这种快速点击的误操作,怎么把“手残党”扼杀于摇篮之中,就用来了黑科技AOP,插桩的方式来简化判断的逻辑,开放生产力。

AOP介绍

AOP维基介绍
面向侧面的程序设计(aspect-oriented programming,AOP,又译作面向方面的程序设计、观点导向编程、剖面导向程序设计)是计算机科学中的一个术语,指一种程序设计范型。该范型以一种称为侧面(aspect,又译作方面)的语言构造为基础,侧面是一种新的模块化机制,用来描述分散在对象、类或函数中的横切关注点(crosscutting concern)。

侧面的概念源于对面向对象的程序设计的改进,但并不只限于此,它还可以用来改进传统的函数。与侧面相关的编程概念还包括元对象协议、主题(subject)、混入(mixin)和委托。

gradle plugin依赖

为了方便使用,我已经抽离出来了一个aspectj.gradle文件,需要注意的是,目前我是在app-moudle来进行配置的,注意variants的取值在library-moudle的取值是不同的。

repositories {
    jcenter()
    google()
}

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.13'
    }
}

import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

final def log = project.logger
final def variants = project.android.applicationVariants

variants.all { variant ->
    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast {
        String[] args = ["-showWeaveInfo",
                         "-1.5",
                         "-inpath", javaCompile.destinationDir.toString(),
                         "-aspectpath", javaCompile.classpath.asPath,
                         "-d", javaCompile.destinationDir.toString(),
                         "-classpath", javaCompile.classpath.asPath,
                         "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
        log.debug "ajc args: " + Arrays.toString(args)

        MessageHandler handler = new MessageHandler(true);
        new Main().run(args, handler);
        for (IMessage message : handler.getMessages(null, true)) {
            switch (message.getKind()) {
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    log.error message.message, message.thrown
                    break;
                case IMessage.WARNING:
                    log.warn message.message, message.thrown
                    break;
                case IMessage.INFO:
                    log.info message.message, message.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug message.message, message.thrown
                    break;
            }
        }
    }
}

app-moudle的build.gradle只需要再引用一下即可

apply plugin: 'com.android.application'
apply from: "aspectj.gradle"

dependencies {
    //...
    implementation 'org.aspectj:aspectjrt:1.8.13'
}

编写插桩代码

先编写好注解类

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface SingleClick {
}

其中注意方法切入点的包名+类名要声明正确

@Aspect
public class SingleClickAspectj {

    public static final int MIN_CLICK_DELAY_TIME = 500;
    static int TIME_TAG = R.id.click_time;

    @Pointcut("execution(@com.dreamliner.lib.aspectj.sample.annotation.SingleClick * *(..))")//方法切入点
    public void methodAnnotated() {
    }

    @Around("methodAnnotated()")//在连接点进行方法替换
    public void aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        View view = null;
        for (Object arg : joinPoint.getArgs()) {
            if (arg instanceof View) view = ((View) arg);
        }
        if (view != null) {
            Object tag = view.getTag(TIME_TAG);
            long lastClickTime = (tag != null) ? (long) tag : 0;
            long currentTime = System.currentTimeMillis();
            //过滤掉600毫秒内的连续点击
            if (currentTime - lastClickTime > MIN_CLICK_DELAY_TIME) {
                view.setTag(TIME_TAG, currentTime);
                //执行原方法
                joinPoint.proceed();
            }
        }
    }
}

使用就很简单,只需要在onClick之类的方法加上注解即可

public class MainActivity extends AppCompatActivity {

    private long lastTime;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setAct(this);
    }

    //加上注解,编译的时候就会自动生成中间插桩的代码
    @SingleClick
    public void onClick(View v) {
        Log.e("TAG", "click:\t" + (System.currentTimeMillis() - lastTime));
        lastTime = System.currentTimeMillis();
    }
}

原理和逆向分析

基本原理就是,编译过程中,生成class会扫描@Aspect的类指定的一些注解类,然后进行@Around的一些插入处理,目前这个就是直接把原有方法的形参来进行一次遍历然后提取view来设置tag,判断是否xx时间内才响应,最后再执行原有方法。

FAQ

  • 需要注意的是,如果你地用DataBinding之类,然后用了lambda之类的来转换直接返回一些obj回来而没有view对象,这样会导致遍历形参没有找到,压根执行不下去,所以需要注意这些情况的处理。

总结

整体下来,可以看到,采用AOP的方式来进行防止快速点击的误操作是非常简单而有效的,缺点主要是增加了方法数,整体实现起来非常友好。比RxJava等方式来实现相对优雅不少。
ps: 伸手党福利-AspectSample

坚持原创技术分享,您的支持将鼓励我继续创作!