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 的执行顺序

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容