使用aspectj對app中按鈕的快速點擊進行處理

最近項目進入緊鑼密鼓測試階段,昨天測試提了一個issue,app中按鈕都沒有做快速點擊校驗。

這就涉及到aop面向切面編程了!后端開發Spring對aop應該很熟悉,android開發中可能用到aop的情況沒有后端那么多,但是aop對android開發也是至關重要的!

哪些情況用到aop?

  • 比如針對某一功能進行埋點
  • 全局日志處理
  • 全局異常處理
  • 全局動畫處理等

java aop大致有三種方式

1.jdk動態代理
2.cglib動態代理
3.aspectj

. jdk動態代理 cglib aspectj
作用對象的限制 只能操作實現了接口的類 不能操作被final修飾的類,因為cglib是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法來實現代理. 貌似沒什么限制
基本原理 利用攔截器(攔截器必須實現InvocationHanlder)加上反射機制生成一個實現代理接口的匿名類,在調用具體方法前調用InvokeHandler來處理。 利用ASM開源包,對代理對象類的class文件加載進來,通過修改其字節碼生成子類來處理。 采用基于jvm的ajc(編譯器)和weaver(織入器),在class字節碼中織入aspectj的代碼

項目中我使用的是https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx 它是在aspectj基礎上做了些修改,支持AS的instant run,集成比aspectj更加方便

首先在project下的gradle文件中加入aspectjx插件

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        maven { url 'https://maven.aliyun.com/repository/jcenter/' }
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
        maven { url 'https://dl.bintray.com/umsdk/release' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.1'
        classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-beta02"
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        maven { url 'https://maven.aliyun.com/repository/jcenter/' }
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
        maven { url 'https://dl.bintray.com/umsdk/release' }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
ext {
    compileSdkVersion = 28
    minSdkVersion = 17
    targetSdkVersion = 27
    versionCode=5
    versionName="1.4.6"
    testRunner="1.1.1"
    espresso="3.1.1"
    junit="4.12"
    appcompat="1.1.0-alpha01"
    supportLibVersion = "28.0.0"
}

module下的gradle文件添加

apply plugin: 'android-aspectjx'

接下來就可以開始編寫被@AspectJ 修飾的切面類了

AspectHandler.java 用來對點擊事件相關的攔截處理

package com.mjt.pad.common.aspect;

import android.util.Log;

import com.mjt.common.utils.UIUtils;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

/**
 * Copyright:mjt_pad_android
 * Author: liyang <br>
 * Date:2019-05-05 15:41<br>
 * Desc: <br>
 */

@Aspect
public class AspectHandler {
    private static final String TAG = AspectHandler.class.getSimpleName();


    @Before("execution(void android.view.View.OnClickListener.onClick(..))")
    public void beforePoint(JoinPoint joinPoint) {
        Log.e(TAG, "before: " + joinPoint);
    }

  

    @Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
    public void dealWithNormal() {

    }

    @Around("dealWithNormal()")
    public void onViewClicked(ProceedingJoinPoint proceedingJoinPoint) {
        boolean isFastClickPassed = !UIUtils.isFastClickOnlyInAspect();
        Log.e(TAG, "onViewClicked: 捕獲到了,isFastClick=" + !isFastClickPassed);
        if (isFastClickPassed) {
            Log.e(TAG, "onViewClicked: " + proceedingJoinPoint);
            try {
                proceedingJoinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                Log.e(TAG, "onViewClicked: ", throwable);
            }
        }
    }

}

簡單介紹下這個類里面的@Before @Pointcut @Around幾個注解吧,不然完全沒接觸過aspectj的同學會看的一頭霧水

首先說pointcut
@Pointcut相當于你要攔截的某些執行點或者調用點,pointcut可以有call,execution,target,this,within,withincode等等操作符,這些操作符可以結合java的||,&&,!使用

call捕獲的joinpoint是簽名方法的調用點,而execution捕獲的則是執行點。
call和execution的語法

within()的參數是一個類,比如我們可以通過within(A.class)或者!within(A.class)來過濾想要攔截的點
withincode()和within()相似,只不過withincode()接收的參數是方法的signature

target()判斷目標對象是否是某種類型,this()判斷當前執行對象是否是某種類型

call和execution的語法結構:
execution/call([注解] [修飾符] 返回值類型 [類型聲明]方法名(參數列表)[ 異常列表]),被[]括住的是非必須項.

舉例:

execution (* com.mjt..*.*(..))
 1、execution(): 表達式主體。

 2、第一個*號:表示返回類型,*號表示所有的類型。

 3、包名:表示需要攔截的包名,后面的兩個句點表示當前包和當前包的所有子包,com.mjtl包、子孫包下所有類的方法。

 4、第二個*號:表示類名,*號表示所有的類。

 5、*(..):最后這個星號表示方法名,*號表示所有的方法,后面括弧里面表示方法的參數,兩個句點表示任何參數。

@Before在攔截點或者調用點之前調用
@After是在攔截點或者調用點之后調用
被@Around注解的方法,會被織入到攔截方法調用點或這行點之前,

接著我運行項目,編譯通過后,快速的點擊了一個按鈕
log日志顯示

2019-05-08 11:43:41.717 14103-14103/com.mjt.pad.test E/AspectHandler: before: execution(void com.mjt.pad.ui.adapter.ProductAdapter.1.onClick(View))
2019-05-08 11:43:41.718 14103-14103/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=false
2019-05-08 11:43:41.718 14103-14103/com.mjt.pad.test E/AspectHandler: onViewClicked: execution(void com.mjt.pad.ui.adapter.ProductAdapter.1.onClick(View))
2019-05-08 11:43:41.883 14103-14103/com.mjt.pad.test E/AspectHandler: before: execution(void com.mjt.pad.ui.adapter.ProductAdapter.1.onClick(View))
2019-05-08 11:43:41.883 14103-14103/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=true

執行了,before方法,然后進入被@Around修飾的方法,看到第一次點擊判斷不是快速點擊
放過攔截,proceedingJoinPoint.proceed();原方法得到執行!
第二次點擊,判斷是快速點擊,proceedingJoinPoint.proceed()沒有執行,也就是原方法被攔截掉了!
但是這時候我發現如果點擊事件是使用lambda表達式是無法攔截的,因為這里的pointcut的execution是這樣

 @Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
    public void dealWithNormal() {

    }

這個正則的大致意思是,攔截返回類型為void, android.view.View.OnClickListener.onClick()方法,參數(..)表示參數可以是任意數量任意類型

那么接著寫pointcut攔截lambda表達式的點擊事件
于是AspectHandler 切面類被我改成了這樣

package com.mjt.pad.common.aspect;

import android.util.Log;

import com.mjt.common.utils.UIUtils;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

/**
 * Copyright:mjt_pad_android
 * Author: liyang <br>
 * Date:2019-05-05 15:41<br>
 * Desc: <br>
 */

@Aspect
public class AspectHandler {
    private static final String TAG = AspectHandler.class.getSimpleName();


    @Before("dealWithNormal()||dealWithLambda()")
    public void beforePoint(JoinPoint joinPoint) {
        Log.e(TAG, "before: " + joinPoint);
    }

    @Pointcut("execution(void com.mjt..lambda*(android.view.View))")
    public void dealWithLambda() {

    }

    @Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
    public void dealWithNormal() {

    }

    @Around("dealWithNormal()||dealWithLambda()")
    public void onViewClicked(ProceedingJoinPoint proceedingJoinPoint) {
        boolean isFastClickPassed = !UIUtils.isFastClickOnlyInAspect();
        Log.e(TAG, "onViewClicked: 捕獲到了,isFastClick=" + !isFastClickPassed);
        if (isFastClickPassed) {
            Log.e(TAG, "onViewClicked: " + proceedingJoinPoint);
            try {
                proceedingJoinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                Log.e(TAG, "onViewClicked: ", throwable);
            }
        }
    }

}

增加的pointcut對點擊事件采用lambda表達式的攔截
然后我快速的點擊了一個采用lambda表達式方式實現的點擊事件log日志如下

2019-05-08 11:55:05.506 15052-15052/com.mjt.pad.test E/AspectHandler: before: execution(void com.mjt.pad.ui.fragment.print.PrintManagerFragment.lambda$initViews$1(View))
2019-05-08 11:55:05.506 15052-15052/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=false
2019-05-08 11:55:05.506 15052-15052/com.mjt.pad.test E/AspectHandler: onViewClicked: execution(void com.mjt.pad.ui.fragment.print.PrintManagerFragment.lambda$initViews$1(View))
2019-05-08 11:55:05.755 15052-15052/com.mjt.pad.test E/AspectHandler: before: execution(void com.mjt.pad.ui.fragment.print.PrintManagerFragment.lambda$initViews$1(View))
2019-05-08 11:55:05.755 15052-15052/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=true

嗯,lambda方式實現的點擊事件也被攔截到了


接下來,如果某個小伙伴或我這個按鈕不要攔截快速點擊,那怎么辦呢?

嗯,采用自定義注解,如果某個onClick方法請求放過快速點擊攔截,加上這個注解就好了

接著我們寫一個自定義注解就叫Ignore
作用于CLASS,修飾的目標為方法和構造函數

package com.mjt.pad.common.aspect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Copyright:mjt_pad_android
 * Author: liyang <br>
 * Date:2019-05-05 16:35<br>
 * Desc: <br>
 */
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD,ElementType.CONSTRUCTOR})
public @interface Ignore {
}

接著改動AspectHandler切面類

package com.mjt.pad.common.aspect;

import android.util.Log;

import com.mjt.common.utils.UIUtils;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

/**
 * Copyright:mjt_pad_android
 * Author: liyang <br>
 * Date:2019-05-05 15:41<br>
 * Desc: <br>
 */

@Aspect
public class AspectHandler {
    private static final String TAG = AspectHandler.class.getSimpleName();

    private volatile boolean isIgnored = false;

    @Before("execution(@com.mjt.pad.common.aspect.Ignore void com.mjt..*.onClick(..))")
    public void checkIgnore(JoinPoint joinPoint) {
        isIgnored = true;
        Log.e(TAG, "checkIgnore: " + joinPoint);
    }

    @Pointcut("execution(void com.mjt..lambda*(android.view.View))")
    public void dealWithLambda() {

    }

    @Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
    public void dealWithNormal() {

    }

    @Around("dealWithNormal()||dealWithLambda()")
    public void onViewClicked(ProceedingJoinPoint proceedingJoinPoint) {
        boolean isFastClickPassed = !UIUtils.isFastClickOnlyInAspect();
        Log.e(TAG, "onViewClicked: 捕獲到了,isFastClick=" + !isFastClickPassed+",isIgnored="+isIgnored);
        if (isIgnored||isFastClickPassed) {
            Log.e(TAG, "onViewClicked: " + proceedingJoinPoint);
            try {
                proceedingJoinPoint.proceed();
                isIgnored=false;
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                Log.e(TAG, "onViewClicked: ", throwable);
            }
        }
    }

}

對@Before方法進行了修改

 @Before("execution(@com.mjt.pad.common.aspect.Ignore void com.mjt..*.onClick(..))")
    public void checkIgnore(JoinPoint joinPoint) {
        isIgnored = true;
        Log.e(TAG, "checkIgnore: " + joinPoint);
    }

"execution(@com.mjt.pad.common.aspect.Ignore void com.mjt..*.onClick(..))"匹配的是被我們自定義注解@Ignore修飾的 com.mjt包及其子包下的所有onClick方法

然后我們找一個onClick方法加上@Ignore注解看看起作用沒

    @Ignore
    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.llyPart:
            ...

接著找到這個被Ignore修飾的點擊事件,快速點擊兩下

log日志如下

2019-05-08 12:19:06.925 16912-16912/com.mjt.pad.test E/AspectHandler: checkIgnore: execution(void com.mjt.pad.ui.fragment.RemarkFragment.onClick(View))
2019-05-08 12:19:06.925 16912-16912/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=false,isIgnored=true
2019-05-08 12:19:06.926 16912-16912/com.mjt.pad.test E/AspectHandler: onViewClicked: execution(void com.mjt.pad.ui.fragment.RemarkFragment.onClick(View))
2019-05-08 12:19:07.086 16912-16912/com.mjt.pad.test E/AspectHandler: checkIgnore: execution(void com.mjt.pad.ui.fragment.RemarkFragment.onClick(View))
2019-05-08 12:19:07.087 16912-16912/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=true,isIgnored=true
2019-05-08 12:19:07.087 16912-16912/com.mjt.pad.test E/AspectHandler: onViewClicked: execution(void com.mjt.pad.ui.fragment.RemarkFragment.onClick(View))

可以看到,是快速點擊,但是原方法也得到了執行

嗯 對項目的全局處理點擊事件大致就是這樣,aspectj相關的東西有很多

關于aspectj的具體使用請查看 aspectj官方文檔
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容