1. 程式人生 > >Android AOP 三劍客:APT AspectJ Javassist

Android AOP 三劍客:APT AspectJ Javassist

概述

AOP三劍客各自作用的位置 這裡寫圖片描述

APT 註解處理器(Java5 中的Annotation Processing Tool),註解現在已經比較常見,使用廣泛,可以為我們提供準確的切入點。教程參見

代表框架:DataBinding、Dagger2、EventBus3、DBFlow、AndroidAnnotation等

AspectJ主要任務是在編譯期注入程式碼

代表框架:Hugo(Jake Wharton)

拓展介紹:通過AOP程式設計來實現對原始碼無侵入埋點的工具有

工具 方式 能力 缺點
XPosed 執行期hook 能hook自己應用程序的方法能hook其他應用的方法能hook系統的方法 手機需要root依賴三方包的支援,碎片化嚴重相容性差
DexPosed 執行期hook 能hook自己應用程序的方法 目前不支援4.4以上的系統依賴三方包支援,碎片化嚴重相容性差
AspectJ 編譯期位元組碼注入 可以在編譯成位元組碼的過程中插入程式碼 官方有Eclipse外掛;Android Studio沒有,需要替換編譯器,環境不好部署
ASM 編譯期或執行期位元組碼注入 可以在位元組碼中檔案或者ClassLoader載入位元組碼的時候插入程式碼 需要熟悉位元組碼語法

Javassist可以在編譯期間修改class二進位制檔案(ASM也有同樣的功能),一般利用gradle建task在打包成dex之前進行class的修改

AspectJ

環境搭建

專案的build.gradle配置

buildscript {

    repositories {
        google()
        jcenter()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.3'

        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 } }

module下的build.gradle

apply plugin: 'com.android.application'
apply plugin: 'android-aspectjx'

AspectJ是對java的擴充套件,而且是完全相容java的。但是編譯時得用Aspect專門的編譯器,這裡的配置就是使用Aspect的編譯器

到這裡環境搭建完成

幾個基礎概念

Advice(通知): 典型的 Advice 型別有 before、after 和 around,分別表示在目標方法執行之前、執行後和完全替代目標方法執行的程式碼。

Joint point(連線點): 程式中可能作為程式碼注入目標的特定的點和入口。

Pointcut(切入點): 告訴程式碼注入工具,在何處注入一段特定程式碼的表示式。

Aspect(切面): Pointcut 和 Advice 的組合看做切面。例如,在本例中通過定義一個 pointcut 和給定恰當的advice,新增一個了記憶體快取的切面。

Weaving(織入): 注入程式碼(advices)到目標位置(joint points)的過程。

AspectJ 使用

兩種常見的使用姿勢 第一種

@Aspect
public class TestAspect {

    @Before("execution(* android.view.View.OnClickListener.onClick(android.view.View))")
    public void onViewClickListener(JoinPoint joinPoint) throws Throwable {
        Log.d("hsb","onViewClickListener");
    }
}

第二種

@Aspect
public class TestAspect {

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

    @Before("point()")
    public void before(JoinPoint joinPoint){
        Log.d("hsb","OkHttpAspect before");
    }

    @After("point()")
    public void after(){
        Log.d("hsb","OkHttpAspect after");
    }
}

下面一一解析下,

首先是註解 - @Aspect:宣告切面,標記類 - @Pointcut(切點表示式):定義切點,標記方法 - @Before(切點表示式):前置通知,切點之前執行 - @Around(切點表示式):環繞通知,切點前後執行 - @After(切點表示式):後置通知,切點之後執行 - @AfterReturning(切點表示式):返回通知,切點方法返回結果之後執行 - @AfterThrowing(切點表示式):異常通知,切點丟擲異常時執行

再者是切點表示式

execution(<修飾符模式>? <返回型別模式> <方法名模式>(<引數模式>) <異常模式>?)

除了返回型別模式、方法名模式、引數模式外其他均為可選

其中execution其實還有其他的方法

方法 描述
execution(MethodPattern) 方法執行
call(MethodPattern) 方法呼叫
execution(ConstructorPattern) 建構函式執行
call(ConstructorPattern) 建構函式被呼叫
staticinitialization(TypePattern) static 塊初始化
get(FieldPattern) 屬性讀操作
set(FieldPattern) 屬性寫操作
handler(TypePattern) 異常處理
adviceexecution() 所有 Advice 執行

匹配規則的型別

名稱 描述
MethodPattern [!] [@Annotation] [public,protected,private] [static] [final] 返回值型別 [類名.]方法名(引數型別列表) [throws 異常型別]
ConstructorPattern [!] [@Annotation] [public,protected,private] [final] [類名.]new(引數型別列表) [throws 異常型別]
FieldPattern [!] [@Annotation] [public,protected,private] [static] [final] 屬性型別 [類名.]屬性名
TypePattern 其他 Pattern 涉及到的型別規則也是一樣,可以使用 ‘!’、”、’..’、’+’,’!’ 表示取反,” 匹配除 . 外的所有字串,’*’ 單獨使用事表示匹配任意型別,’..’ 匹配任意字串,’..’ 單獨使用時表示匹配任意長度任意型別,’+’ 匹配其自身及子類,還有一個 ‘…’表示不定個數

匹配規則

符號 描述
* 表示任何數量的字元,除了(.)
.. 表示任何數量的字元包括任何數量的(.)
+ 描述指定型別的任何子類或者子介面
! 一 元操作符
|| 和 && 二 元操作符

一些例子

例子 描述
*Account 使用Account名稱結束的型別,如SavingsAccount和CheckingAccount
java.*.Date 型別Date在任何直接的java子包中,如java.util.Date和java.sql.Date
java..* 任何在java包或者所有子包中的型別,如java.awt和java.util或者java.awt.event 和java.util.logging
javax..*Model+ 所有javax包或者子包中以Model結尾的型別和其所有子類,如TableModel,TreeModel。
!vector 所有除了Vector的型別
Vector
java.util.RandomAccess+ RandomAccess的所有子類
void   Account.set*(*) Account中以set開頭,並且只有一個引數型別,無返回值的方法
void   Account.*() Account中所有的沒有引數的void 方法
public   *   Account.*() Account中所有沒有引數的public 方法
public   *   Account.*(..) Account中所有的public 方法
      Account.*(..)
Account中的所有方法,包括private方法
!public   *   Account.*(..) 所有的非public 方法
*   Account+.*(..) 所有的方法,包括子類的方法
*   java.io.Reader.read(..) 所有的read方法
*   java.io.Reader.read(char[],..) 所有以read(char[])開始的方法,包括read(char[])和read(char[],int,int)
*   javax..*.add*Listener(EventListener+) 命名以add開始,以Listener結尾的方法,引數中為EventListener或子類
*   *.*(..) throws RemoteException 丟擲RemoteException的所有方法

搭配註解使用

這方面主要利用註解來精確定位程式碼注入點,這個時候我們的AspectJ的表示式就有了輕微的變動了,可以寫成這樣 execution(<@註解型別模式>? <修飾符模式>? <返回型別模式> <方法名模式>(<引數模式>) <異常模式>?)

先寫個註解類

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnoTrace {
    String value();
    int type();
}

在業務中使用註解

@TestAnnoTrace(value = "lqr_test", type = 1)
public void test() {
    Log.d("hsb","Hello, I am a test");
}

最後在切面中進行處理

@Before("execution(@com.android.tdw.TestAnnoTrace * *(..))")
public void pointcut(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 通過Method物件得到切點上的註解
TestAnnoTrace annotation = method.getAnnotation(TestAnnoTrace.class);
String value = annotation.value();
int type = annotation.type();
}