Android 面向切面程式設計 AOP 解決連續點選開啟重複頁面問題

boredream發表於2019-03-03

先介紹概念

比如我希望在所有頁面啟動的時候加一個埋點~
希望在所有按鈕點選的時候加個快速重複點選的判斷~等等
這樣在專案中同一種型別的所有程式碼處,統一加入邏輯處理的方法,叫做 面向切面程式設計 AOP

而這些我們需要插入程式碼的具體位置,則叫做切點 Pointcut,比如我在某些類的某個方法中插入

專案中可以插入地方的型別,叫做連線點 Join Point,比如我可以在方法中插入,可以在變數取值時插入

插入的方式 Advice,可以讓我們指定在切點前插入,還是在切點執行後插入等

這些後面都會具體介紹

Android實現AOP,可以使用的方案主要有兩個

一個是大神的 github.com/JakeWharton…

一個是滬江的 github.com/HujiangTech…

都是基於 aspectJ 的,所以也可以直接配置aspectJ,不過太麻煩~

我們以Hugo為例,採坑之旅現在開始~


先配置

專案 build.gradle

buildscript {

  repositories {

    mavenCentral()

  }

  dependencies {

    classpath `com.jakewharton.hugo:hugo-plugin:1.2.1`

  }

}
複製程式碼

app / build.gradle

apply plugin: `com.jakewharton.hugo`
複製程式碼

程式碼中可以設定開啟

Hugo.setEnabled(true|false)
複製程式碼

注意

1.如果有引用module,需要在module中新增以上配置和編寫程式碼

2.不支援lambda

實踐~

比如要解決 快速點選開啟多頁面 的問題

配置好後,開始編寫程式碼~

@Aspect

public class FastClickBlockAspect {

    public static final String TAG = "FastClickBlockAspect";

    @Around("call(* android.content.Context.startActivity(..))")

    public void onStartBefore(ProceedingJoinPoint joinPoint) {

        try {

            if (!ViewUtils.isFastClick()) {

                joinPoint.proceed();

            }

        } catch (Throwable e) {

            e.printStackTrace();

        }

    }

}
複製程式碼

這個類檔案儲存在依賴module(沒有就在主app module中)中任意package下就行了。

不用任何配置或其他程式碼處理,不用修改原有程式碼~ 然後直接run專案,就可以了~

這樣,程式碼中所有context.startActivity的地方就都會先判斷是否是快速點選,然後再執行,達到防止重複開啟頁面的目標


解釋下程式碼

@Aspect 標註AOP類,表示該類裡面是處理切面程式碼的,固定寫法

Advice 切點插入方式。表示在匹配的切點處,用什麼方式去處理,一共有如下幾個型別

  • @Around 環繞插入。引數為ProceedingJoinPoint,可以手動包裹程式碼後,在需要的條件中呼叫引數的方法 proceed() 表示執行目標方法
  • @Before 前置插入。在切點前執行
  • @After 後置插入。在切點後執行
  • @After returning。在返回值之後執行
  • @After throwing。在丟擲異常後執行

注意: 只有Around引數是ProceedingJoinPoint,需要呼叫proceed執行方法,其他的都只是前後插入,不會影響原有程式碼的執行

所以埋點功能的話我們就可以使用after或before在原有方法前後執行埋點請求;

而防止連續跳轉頁面,就可以使用Around,然後在判斷條件裡手動 proceed 呼叫原方法

Join Point 連線點。表示我們可以插入程式碼的位置型別,和Pointcuts切點結合使用

  • Method call。方法被呼叫。結合切點寫法:call(方法切點正則)
  • ** Method execution**。方法被執行。結合切點寫法:execution(方法切點正則)
  • Constructor call。構造方法被呼叫。結合切點寫法:call(構造方法切點正則)
  • Constructor execution。構造方法被執行。結合切點寫法:execution(構造方法切點正則)
  • Field get。屬性讀取。結合切點寫法:get(變數切點正則)
  • Field set。屬性設定。結合切點寫法:set(變數切點正則)
  • Pre-init。初始化前。結合切點寫法:preinitialization(構造方法切點正則)
  • Init。初始化。結合切點寫法:initialization(構造方法切點正則)
  • Static init。靜態程式碼塊初始化。結合切點寫法:staticinitialization(對應程式碼切點正則)
  • Handler。異常處理。結合切點寫法:handle(對應程式碼切點正則)
  • Advice execution。所有Advice執行。結合切點寫法:adviceexecution()

最常用的是 method call 和 execution,一般系統類的方法直接用call,@Around(call(xxx))包裹處理;

如果是自定義方法,希望裡面插入,就@Before(execution(xxx))

Poincuts 切點。是一段匹配規則,表示需要切入程式碼的地方,規則如下
@註解 訪問許可權 返回值型別 包名.方法名(方法引數)

  • @註解 可選。可以用來匹配指定註解的切點,也可以自定義個註解在需要特殊處理的地方標註
  • 訪問許可權 可選。就是 public private static 等,不加的話就是全匹配

後面返回值、包名什麼的,支援萬用字元 * .. + 等

  • * 表示匹配任意內容。 比如
    包名中使用。java.*.Date 可以表示 java.sql.Date也可以表示java.utils.Date
    單獨使用。返回值如果是 * 表示任意型別返回
    拼接使用。*Dialog 表示匹配任意 XXDialog內容
  • .. 表示匹配任意型別任意數量內容。比如
    包名中使用。com..Utils 表示java任意包以及子包下的 Utils類
    引數中使用。(..)表示匹配任意型別任意數量的引數,也可以(String, ..) 指定第一個,其他的不定
  • + 表示子類。比如
    java..*Model+,表示在java任意包或子包下以Model結尾類的子類

所以翻譯下我們之前程式碼的核心方法部分

@Around("call(* android.content.Context.startActivity(..))")
複製程式碼

就是在系統context.startActivity方法呼叫(call)的時候,環繞插入程式碼(@Around),

方法內處理具體實現,判斷是否是快速點選,如果非快速點選才正常執行ProceedingJoinPoint.proceed()

但程式碼還有些問題,就是 Context.startActivity並不能包含所有的情況,

還有Activity.startActivity,以及 startActivityForResult等沒有覆蓋到~ 這裡就可以用我們新學習的姿勢解決,修改如下

@Aspect

public class FastClickBlockAspect {

    public static final String TAG = "FastClickBlockAspect";

    @Around("call(* android..*.startActivity*(..))")

    public void onStartBefore(ProceedingJoinPoint joinPoint) {

        try {

            if (!ViewUtils.isFastClick()) {

                joinPoint.proceed();

            }

        } catch (Throwable e) {

            e.printStackTrace();

        }

    }

}
複製程式碼

方法內不變,修改了匹配切點的規則

@Around("call(* android..*.startActivity*(..))")
複製程式碼

解釋下就是,在 android.任意包或子包.. 下,的任意類*(可以是Activity、Context或Fragment),

呼叫該類的方法 startActivity*(包括startActivity方法和startActivityForResult方法)時,進行自定義處理

繼續優化,如果希望在開啟FragmentDialog的時候,也要防止重複顯示,那怎麼辦,

這時萬用字元不能包含兩個區別較大的切點規則了,我們可以申明多個切點,然後用邏輯符號拼接起來

切點申明很簡單,直接用 @Pointcut 申明一個空方法,@Pointcut後也可以直接加上 連線點(切點規則)

多個方法對應多個切點,最後在需要處理的主方法內 @Around(切點規則方法1 || 切點規則方法2) 這樣邏輯拼接起來

程式碼如下

@Aspect

public class FastClickBlockAspect {

    public static final String TAG = "FastClickBlockAspect";

    @Pointcut("execution(* com.archex.core.base.BaseDialogFragment.show(..))")

    public void showBaseDialogFragment() {}

    @Pointcut("call(* android..*.startActivity*(..))")

    public void startActivity() {}

    @Around("showBaseDialogFragment() || startActivity()")

    public void onStartBefore(ProceedingJoinPoint joinPoint) {

        try {

            if (!ViewUtils.isFastClick()) {

                joinPoint.proceed();

            }

        } catch (Throwable e) {

            e.printStackTrace();

        }

    }

}
複製程式碼

到此簡單使用就結束啦~

相關文章