Android使用AOP来解决重复点击问题
AOP即Aspect Oriented Programming的缩写,习惯称为切面编程;与OOP(面向对象编程)万物模块化的思想不同,AOP则是将涉及到众多模块的某一类问题进行统一管理,AOP的优点是将业务逻辑与系统化功能高度解耦,让我们在开发过程中可以只专注于业务逻辑,其他一些系统化功能(如路由、日志、权限控制、拦截器、埋点、事件防抖等)则由AOP统一处理;
AspectJ简介
AOP是一种编程思想,或者说方法论,AspectJ则是专为AOP设计的一种语言,它支持原生的JAVA,可用于在java中处理AOP的相关问题。
下面非常简单的描述下AspectJ中几个要点
-
@Aspect
表示这是一个切面类,放在类名上面,把当前类标识为一个切面供容器读取
@Aspect
public class SingleClickAspect {
// 里面用AspectJ注解,实现相应方法
}
-
Join Points
AspectJ中的切点,是AspectJ作用到具体某个位置的说明,主要包括三类:1、函数(函数调用,函数执行,构造函数等)
2、变量(变量get,变量set等)
3、 代码块(静态代码块,for等) -
@Pointcuts
AspectJ中的切面(这种翻译不一定正确),由点及面,用于说明你需要hook哪一类问题,比如我需要hook一个单击事件SingleClick ,
@Retention(RetentionPolicy.RUNTIME) //注解保留至运行时
@Target(ElementType.METHOD) //声明注解作用在方法上面
public @interface SingleClick {
/* 点击间隔时间 */
long value() default 2000;
}
则:
@Aspect
public class SingleClickAspect {
/**
* 定义切点,标记切点为所有被@SingleClick注解的方法
* 注意:这里com.util.click.SingleClick是你自己项目中SingleClick这个类的全路径
* 注意:这里的 * * 表示任意方法
* (..)表示任意参数
*/
@Pointcut("execution(@com.util.click.SingleClick * *(..))")
public void methodClick() {}// 该方法不会被执行
}
-
advice
Join Points和Pointcuts用来说明需要hook哪些位置或者流程,advice则用于hook之后指定需要做什么,在切面类中需要定义切面方法用于响应响应的目标方法,切面方法即为通知方法,通知方法需要用注解标识,AspectJ 支持 5 种类型的通知注解:@Before: 前置通知, 在方法执行之前执行
@After: 后置通知, 在方法执行之后执行 。
@AfterRunning: 返回通知, 在方法返回结果之后执行
@AfterThrowing: 异常通知, 在方法抛出异常之后
@Around: 环绕通知, 围绕着方法执行,around()用的会比较多,因为自由度高,其他的用around()都可以实现
@Aspect
public class SingleClickAspect {
@Pointcut("execution(@com.util.click.SingleClick * *(..))")
public void methodClick() {}// 该方法不会被执行
@Before("methodClick()")
public void before(){
System.out.println("before................");
}
@After("methodClick()")
public void after(){
System.out.println("after.................");
}
@AfterReturning("methodClick()")
public void afterReturning(JoinPoint joinPoint) {
System.out.println("afterReturning.................");
}
@AfterThrowing("methodClick()")
public void afterThrowing(JoinPoint joinPoint) {
System.out.println("afterThrowing...................");
}
@Around("methodClick()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("around before............");
joinPoint.proceed(); //执行完成目标方法
System.out.println("around after..............");
}
它们是按什么顺序执行的呢?创建点击事件
TextView textView.setOnClickListener(new View.OnClickListener() {
@SingleClick(1500)// 添加点击注释
@Override
public void onClick(View v) {
if (flag) {
System.out.println("throw an exception................");
throw new RuntimeException();
}
System.out.println("执行onClick................");
}
});
点击后,执行正常情况结果:
around before............
before................
执行onClick................
around after..............
after.................
afterReturning.................
执行异常情况结果:
around before............
before................
throw an exception................
around after..............
after.................
afterThrowing.................
对于@Around这个advice,不管它有没有返回值,但是必须要在方法内部,调用一下joinPoint.proceed();否则,OnClickListener中的onClick()将没有机会被执行,从而也导致了 @Before这个advice不会被触发。
AOP处理android中的重复点击
AOP用于处理某一类独立的问题,非常契合屏蔽重复点击的需求,我们只需要hook住原先的点击事件(转确的说是点击事件后的处理流程),判断是不是重复点击,是则过滤掉不让它执行,否则就正常执行;
集成
1.引入Aspectj
在Android中进行AspectJ的实现,建议使用Hujiang大神的框架gradle_plugin_android_aspectjx,可以非常方便的集成和配置AspectJ在Android中的环境
- 在项目根目录下的build.gradle中,添加依赖:
dependencies {
classpath 'com.android.tools.build:gradle:3.3.1'
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
}
- 在app或其他任何用到该AOP功能的module目录下的build.gradle中,都需添加:
apply plugin: 'android-aspectjx'
dependencies {
......
implementation 'org.aspectj:aspectjrt:1.8.9'
}
2.添加一个自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SingleClick {
/* 点击间隔时间 */
long value() default 1000;
}
- 添加自定义注解的原因是,方便管理哪些方法使用了重复点击的AOP,同时可以在注解中传入点击时间间隔,更加灵活。
3.封装一个重复点击判断工具类
public final class XClickUtil {
/**
* 最近一次点击的时间
*/
private static long mLastClickTime;
/**
* 最近一次点击的控件ID
*/
private static int mLastClickViewId;
/**
* 是否是快速点击
*
* @param v 点击的控件
* @param intervalMillis 时间间期(毫秒)
* @return true:是,false:不是
*/
public static boolean isFastDoubleClick(View v, long intervalMillis) {
int viewId = v.getId();
// long time = System.currentTimeMillis();
long time = SystemClock.elapsedRealtime();
long timeInterval = Math.abs(time - mLastClickTime);
if (timeInterval < intervalMillis && viewId == mLastClickViewId) {
Log.e("isFastDoubleClick", "true");
return true;
} else {
mLastClickTime = time;
mLastClickViewId = viewId;
Log.e("isFastDoubleClick", "false");
return false;
}
}
}
4.编写Aspect AOP处理类
@Aspect
public class SingleClickAspect {
private static final long DEFAULT_TIME_INTERVAL = 5000;
/**
* 定义切点,标记切点为所有被@SingleClick注解的方法
* 注意:这里me.baron.test.annotation.SingleClick需要替换成
* 你自己项目中SingleClick这个类的全路径哦
*/
@Pointcut("execution(@me.baron.test.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;
break;
}
}
if (view == null) {
return;
}
// 取出方法的注解
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
if (!method.isAnnotationPresent(SingleClick.class)) {
return;
}
SingleClick singleClick = method.getAnnotation(SingleClick.class);
// 判断是否快速点击
if (!XClickUtil.isFastDoubleClick(view, singleClick.value())) {
// 不是快速点击,执行原方法
joinPoint.proceed();
}
}
}
使用方法
private void initView() {
btTest = findViewById(R.id.bt_test);
btTest.setOnClickListener(new View.OnClickListener() {
// 如果需要自定义点击时间间隔,自行传入毫秒值即可
// @SingleClick(2000)
@SingleClick
@Override
public void onClick(View v) {
// do something
}
});
}
遇到的坑
监听系统的onClick()方法时,有时会多次调用切点的方法,导致onClick()方法失效,
@Pointcut("execution(* android.view.View.OnClickListener.onClick(..))")
public void methodAnnotated() {}
原因:onClick()方法中又调用了onClick()方法,被判定为重复点击,所以点击事件没有执行,例如:
@Override
public void onClick(View v) {
super.onClick(v);// 重复调用
}
if (posListener != null) {
btnPositive.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dialog.dismiss();
posListener.onClick(view);// 重复调用
}
});
参考文章:
Android-如何优雅的处理重复点击
AOP在Android中的应用-过滤重复点击
Spring AOP @Before @Around @After 等 advice 的执行顺序