Android使用AOP來(lái)解決重復(fù)點(diǎn)擊問(wèn)題

AOP即Aspect Oriented Programming的縮寫,習(xí)慣稱為切面編程;與OOP(面向?qū)ο缶幊?萬(wàn)物模塊化的思想不同,AOP則是將涉及到眾多模塊的某一類問(wèn)題進(jìn)行統(tǒng)一管理,AOP的優(yōu)點(diǎn)是將業(yè)務(wù)邏輯與系統(tǒng)化功能高度解耦,讓我們?cè)陂_發(fā)過(guò)程中可以只專注于業(yè)務(wù)邏輯,其他一些系統(tǒng)化功能(如路由、日志惕耕、權(quán)限控制、攔截器诫肠、埋點(diǎn)司澎、事件防抖等)則由AOP統(tǒng)一處理;

AspectJ簡(jiǎn)介
AOP是一種編程思想,或者說(shuō)方法論,AspectJ則是專為AOP設(shè)計(jì)的一種語(yǔ)言,它支持原生的JAVA,可用于在java中處理AOP的相關(guān)問(wèn)題。

下面非常簡(jiǎn)單的描述下AspectJ中幾個(gè)要點(diǎn)

  • @Aspect
    表示這是一個(gè)切面類栋豫,放在類名上面挤安,把當(dāng)前類標(biāo)識(shí)為一個(gè)切面供容器讀取
@Aspect
public class SingleClickAspect {
        // 里面用AspectJ注解,實(shí)現(xiàn)相應(yīng)方法
}
  • Join Points
    AspectJ中的切點(diǎn),是AspectJ作用到具體某個(gè)位置的說(shuō)明,主要包括三類:

    1丧鸯、函數(shù)(函數(shù)調(diào)用,函數(shù)執(zhí)行,構(gòu)造函數(shù)等)
    2蛤铜、變量(變量get,變量set等)
    3、 代碼塊(靜態(tài)代碼塊,for等)

  • @Pointcuts
    AspectJ中的切面(這種翻譯不一定正確),由點(diǎn)及面,用于說(shuō)明你需要hook哪一類問(wèn)題,比如我需要hook一個(gè)單擊事件SingleClick ,

@Retention(RetentionPolicy.RUNTIME) //注解保留至運(yùn)行時(shí) 
@Target(ElementType.METHOD) //聲明注解作用在方法上面
public @interface SingleClick {
    /* 點(diǎn)擊間隔時(shí)間 */
    long value() default 2000;
}

則:

@Aspect
public class SingleClickAspect {
        /**
         * 定義切點(diǎn)围肥,標(biāo)記切點(diǎn)為所有被@SingleClick注解的方法
         * 注意:這里com.util.click.SingleClick是你自己項(xiàng)目中SingleClick這個(gè)類的全路徑
         * 注意:這里的 * * 表示任意方法
         * (..)表示任意參數(shù)
       */
        @Pointcut("execution(@com.util.click.SingleClick * *(..))")
        public void methodClick() {}// 該方法不會(huì)被執(zhí)行
}
  • advice
    Join Points和Pointcuts用來(lái)說(shuō)明需要hook哪些位置或者流程,advice則用于hook之后指定需要做什么,在切面類中需要定義切面方法用于響應(yīng)響應(yīng)的目標(biāo)方法剿干,切面方法即為通知方法,通知方法需要用注解標(biāo)識(shí)虐先,AspectJ 支持 5 種類型的通知注解:

    @Before: 前置通知, 在方法執(zhí)行之前執(zhí)行
    @After: 后置通知, 在方法執(zhí)行之后執(zhí)行 怨愤。
    @AfterRunning: 返回通知, 在方法返回結(jié)果之后執(zhí)行
    @AfterThrowing: 異常通知, 在方法拋出異常之后
    @Around: 環(huán)繞通知, 圍繞著方法執(zhí)行,around()用的會(huì)比較多,因?yàn)樽杂啥雀?其他的用around()都可以實(shí)現(xiàn)

@Aspect
public class SingleClickAspect {

    @Pointcut("execution(@com.util.click.SingleClick * *(..))")
    public void methodClick() {}// 該方法不會(huì)被執(zhí)行

    @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(); //執(zhí)行完成目標(biāo)方法
        System.out.println("around after..............");

    }

它們是按什么順序執(zhí)行的呢蛹批?創(chuàng)建點(diǎn)擊事件

TextView textView.setOnClickListener(new View.OnClickListener() {
                @SingleClick(1500)// 添加點(diǎn)擊注釋
                @Override
                public void onClick(View v) {
                    if (flag) {
                        System.out.println("throw an exception................");
                        throw new RuntimeException();
                    }
                    System.out.println("執(zhí)行onClick................");
                }
            });

點(diǎn)擊后,執(zhí)行正常情況結(jié)果:

around before............
before................
執(zhí)行onClick................
around after..............
after.................
afterReturning.................

執(zhí)行異常情況結(jié)果:

around before............
before................
throw an exception................
around after..............
after.................
afterThrowing.................

對(duì)于@Around這個(gè)advice篮愉,不管它有沒(méi)有返回值,但是必須要在方法內(nèi)部猪勇,調(diào)用一下joinPoint.proceed();否則颠蕴,OnClickListener中的onClick()將沒(méi)有機(jī)會(huì)被執(zhí)行,從而也導(dǎo)致了 @Before這個(gè)advice不會(huì)被觸發(fā)椅您。

AOP處理android中的重復(fù)點(diǎn)擊
AOP用于處理某一類獨(dú)立的問(wèn)題,非常契合屏蔽重復(fù)點(diǎn)擊的需求,我們只需要hook住原先的點(diǎn)擊事件(轉(zhuǎn)確的說(shuō)是點(diǎn)擊事件后的處理流程),判斷是不是重復(fù)點(diǎn)擊,是則過(guò)濾掉不讓它執(zhí)行,否則就正常執(zhí)行;

集成
1.引入Aspectj
在Android中進(jìn)行AspectJ的實(shí)現(xiàn),建議使用Hujiang大神的框架gradle_plugin_android_aspectjx,可以非常方便的集成和配置AspectJ在Android中的環(huán)境

  • 在項(xiàng)目根目錄下的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.添加一個(gè)自定義注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SingleClick {
    /* 點(diǎn)擊間隔時(shí)間 */
    long value() default 1000;
}
  • 添加自定義注解的原因是马僻,方便管理哪些方法使用了重復(fù)點(diǎn)擊的AOP韭邓,同時(shí)可以在注解中傳入點(diǎn)擊時(shí)間間隔祠汇,更加靈活可很。

3.封裝一個(gè)重復(fù)點(diǎn)擊判斷工具類

public final class XClickUtil {

    /**
     * 最近一次點(diǎn)擊的時(shí)間
     */
    private static long mLastClickTime;
    /**
     * 最近一次點(diǎn)擊的控件ID
     */
    private static int mLastClickViewId;

    /**
     * 是否是快速點(diǎn)擊
     *
     * @param v  點(diǎn)擊的控件
     * @param intervalMillis  時(shí)間間期(毫秒)
     * @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;

    /** 
     * 定義切點(diǎn)袜茧,標(biāo)記切點(diǎn)為所有被@SingleClick注解的方法
     * 注意:這里me.baron.test.annotation.SingleClick需要替換成
     * 你自己項(xiàng)目中SingleClick這個(gè)類的全路徑哦
     */
    @Pointcut("execution(@me.baron.test.annotation.SingleClick * *(..))")
    public void methodAnnotated() {}

    /** 
     * 定義一個(gè)切面方法笛厦,包裹切點(diǎn)方法
     */
    @Around("methodAnnotated()")
    public void aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        // 取出方法的參數(shù)
        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);
        // 判斷是否快速點(diǎn)擊
        if (!XClickUtil.isFastDoubleClick(view, singleClick.value())) {
            // 不是快速點(diǎn)擊裳凸,執(zhí)行原方法
            joinPoint.proceed();
        }
    }
}

使用方法

private void initView() {
    btTest = findViewById(R.id.bt_test);
    btTest.setOnClickListener(new View.OnClickListener() {
        // 如果需要自定義點(diǎn)擊時(shí)間間隔姨谷,自行傳入毫秒值即可
        // @SingleClick(2000)
        @SingleClick
        @Override
        public void onClick(View v) {
            // do something
        }
    });
}

遇到的坑
監(jiān)聽系統(tǒng)的onClick()方法時(shí)梦湘,有時(shí)會(huì)多次調(diào)用切點(diǎn)的方法捌议,導(dǎo)致onClick()方法失效,

@Pointcut("execution(* android.view.View.OnClickListener.onClick(..))")
public void methodAnnotated() {}

原因:onClick()方法中又調(diào)用了onClick()方法引有,被判定為重復(fù)點(diǎn)擊轿曙,所以點(diǎn)擊事件沒(méi)有執(zhí)行,例如:

@Override
public void onClick(View v) {
    super.onClick(v);// 重復(fù)調(diào)用
}
if (posListener != null) {
        btnPositive.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dialog.dismiss();
                posListener.onClick(view);// 重復(fù)調(diào)用
            }
        });

參考文章:
Android-如何優(yōu)雅的處理重復(fù)點(diǎn)擊
AOP在Android中的應(yīng)用-過(guò)濾重復(fù)點(diǎn)擊
Spring AOP @Before @Around @After 等 advice 的執(zhí)行順序

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市您单,隨后出現(xiàn)的幾起案子虐秦,更是在濱河造成了極大的恐慌,老刑警劉巖蜈彼,帶你破解...
    沈念sama閱讀 221,548評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件幸逆,死亡現(xiàn)場(chǎng)離奇詭異还绘,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)抚太,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門尿贫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)帅霜,“玉大人呼伸,你說(shuō)我怎么就攤上這事括享≌浯伲” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)穴翩。 經(jīng)常有香客問(wèn)我,道長(zhǎng)歉嗓,這世上最難降的妖魔是什么背蟆? 我笑而不...
    開封第一講書人閱讀 59,618評(píng)論 1 296
  • 正文 為了忘掉前任志珍,我火速辦了婚禮垛叨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘舔株。我一直安慰自己,他們只是感情好惭等,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評(píng)論 6 397
  • 文/花漫 我一把揭開白布辞做。 她就那樣靜靜地躺著秤茅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪九杂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,246評(píng)論 1 308
  • 那天放仗,我揣著相機(jī)與錄音诞挨,去河邊找鬼呢蛤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛达罗,可吹牛的內(nèi)容都是我干的粮揉。 我是一名探鬼主播抚笔,決...
    沈念sama閱讀 40,819評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼殊橙,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼狱从!你這毒婦竟也來(lái)了季研?” 一聲冷哼從身側(cè)響起与涡,我...
    開封第一講書人閱讀 39,725評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤驼卖,失蹤者是張志新(化名)和其女友劉穎鸿秆,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體桥胞,經(jīng)...
    沈念sama閱讀 46,268評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡埠戳,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了颗圣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片在岂。...
    茶點(diǎn)故事閱讀 40,488評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蔽午,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出抽莱,到底是詐尸還是另有隱情骄恶,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評(píng)論 5 350
  • 正文 年R本政府宣布虐呻,位于F島的核電站,受9級(jí)特大地震影響偶惠,放射性物質(zhì)發(fā)生泄漏朗涩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評(píng)論 3 333
  • 文/蒙蒙 一扒腕、第九天 我趴在偏房一處隱蔽的房頂上張望瘾腰。 院中可真熱鬧覆履,春花似錦、人聲如沸栖雾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至治泥,卻和暖如春遮精,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背准脂。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工意狠, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留疮胖,地道東北人闷板。 一個(gè)月前我還...
    沈念sama閱讀 48,897評(píng)論 3 376
  • 正文 我出身青樓遮晚,卻偏偏與公主長(zhǎng)得像县遣,于是被迫代替她去往敵國(guó)和親汹族。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容