先介紹概念
比如我希望在所有頁面啟動的時候加一個埋點~
希望在所有按鈕點選的時候加個快速重複點選的判斷~等等
這樣在專案中同一種型別的所有程式碼處,統一加入邏輯處理的方法,叫做 面向切面程式設計 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();
}
}
}
複製程式碼
到此簡單使用就結束啦~