關(guān)于AOP在Android中應(yīng)用的一些思考

AOP的基本概念

什么是AOP

AOP(Aspect Oriented Programming)甜奄,面向切面編程捍掺,是OOP(面向?qū)ο缶幊蹋┑难永m(xù)启妹。

在OOP思想中训枢,我們會把問題劃分為各個模塊托修,如語音、表情等肮砾。在劃分這些模塊的過程中诀黍,也會出現(xiàn)一些共同特征(如埋點)。它的邏輯被分散到了各個模塊仗处,導(dǎo)致了代碼代碼復(fù)雜度提高眯勾,可復(fù)用性降低。

而AOP婆誓,就是將各個模塊中的通用邏輯抽離出來吃环。我們將這些邏輯視作Aspect(切面),然后動態(tài)地把代碼插入到類的指定方法洋幻、指定位置中郁轻。

Aspect(切面)是AOP 中的一個很重要的概念。切面一種新的模塊化機制,用來描述分散在對象好唯、類或函數(shù)中的橫切關(guān)點(crosscutting concern)竭沫。

下面兩張圖說明了使用分別OOP和AOP思想實現(xiàn)上的不同:



AOP中的幾個重要概念

Aspect 切面
一個關(guān)注點的模塊化,這個關(guān)注點實現(xiàn)可能橫切多個對象骑篙。如日志切面蜕提、權(quán)限切面等。

JPoints 執(zhí)行點
Joint Points靶端,簡稱JPoints谎势,表示的是程序運行時的一些執(zhí)行點(可切入點)。
在一個程序中杨名,一個類的構(gòu)造脏榆,一個方法的執(zhí)行,一個變量的設(shè)置台谍,一個異常的捕獲须喂,都可以看成是一個執(zhí)行點。

Pointcuts 切入點
一個程序可以有很多的Jpoints典唇,一個Jpoints還分為call(調(diào)用)和execution(執(zhí)行)镊折。但不是所有的Jpoints都需要關(guān)心。
Pointcuts定義了如何在眾多的Jpoints中選擇想要的切入點介衔。

Target Object 目標(biāo)對象
包含JPoints的對象恨胚,也被稱作被通知或被代理對象。這些對象中已經(jīng)只剩下干干凈凈的核心業(yè)務(wù)邏輯代碼了炎咖,所有的共有功能等代碼則是等待AOP容器的切入赃泡。

Advice 執(zhí)行時機
Advice簡單來說就是hook點,常見的有before乘盼、after升熊、around三種類型。

Aspect 切面
Pointcut和Advice的組合可以看做切面绸栅,它是一個關(guān)注點的模塊化级野,這個關(guān)注點可能會橫切多個對象。

Weaving 織入
把代碼織入到目標(biāo)對象的過程粹胯。

AOP的主要應(yīng)用場合

在開發(fā)中蓖柔,我們通常會把核心功能劃分為一個個模塊開發(fā),再把各個核心模塊中的通用邏輯抽離出來风纠,使用AOP的思想動態(tài)織入業(yè)務(wù)邏輯中况鸣。
所以,根據(jù)AOP的特性竹观,AOP更適合與核心業(yè)務(wù)相關(guān)的通用邏輯镐捧,如:

  • 權(quán)限檢查
  • 日志記錄
  • 性能監(jiān)控
  • 埋點操作
  • 異常處理
  • 參數(shù)校驗

Android下實現(xiàn)AOP的幾種工具

無論是OOP還是AOP潜索,它們都是方法論。
在Android中懂酱,AOP可以通過預(yù)編譯竹习,或者在運行期動態(tài)代理的實現(xiàn)。

動態(tài)代理

實現(xiàn)AOP最基礎(chǔ)的方案玩焰,可能就是Java語言的反射機制與動態(tài)代理機制了由驹。

業(yè)務(wù)邏輯組件在運行過程中,AOP容器會動態(tài)創(chuàng)建一個代理對象供使用者調(diào)用昔园,該代理對象已經(jīng)按程序員的意圖將切面成功切入到目標(biāo)方法的連接點上,從而使切面的功能與業(yè)務(wù)邏輯的功能都得以執(zhí)行并炮。
從原理上講默刚,調(diào)用者直接調(diào)用的其實是AOP容器動態(tài)生成的代理對象,再由代理對象調(diào)用目標(biāo)對象完成原始的業(yè)務(wù)邏輯處理逃魄,而代理對象則已經(jīng)將切面與業(yè)務(wù)邏輯方法進行了合成荤西。

動態(tài)代理又可細分為JDK動態(tài)代理和CGLib動態(tài)代理。
JDK動態(tài)代理利用接口實現(xiàn)伍俘。被代理的對象必須實現(xiàn)業(yè)務(wù)接口邪锌,代理對象必須實現(xiàn)InvocationHanlder接口。代理對象在調(diào)用具體方法前將其攔截癌瘾。
而CGLIB動態(tài)代理則利用繼承實現(xiàn)觅丰。通過ASM(一個開源的字節(jié)碼修改工具),將代理對象類的class文件加載進來妨退,通過修改其字節(jié)碼生成子類來處理妇萄。

一個簡單的JDK動態(tài)代理的代碼示例如下:

/**
 * 業(yè)務(wù)接口
 */
public interface Subject {
    void call();
}
/**
 * 業(yè)務(wù)接口的實現(xiàn)(被代理的類)
 */
public class RealSubjcet implements Subject {
    @Override
    public void call() {
        Log.d("denny", "RealSubject#call");
    }
}
/**
 * 代理類
 */
public class ProxyHandler implements InvocationHandler {

    private final Object realSubject;

    public ProxyHandler(Object realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public Object invoke(Object o, Method method, Object[] args) throws Throwable {
        Log.d("denny", "before ProxyHandler#invoke");
        Object result = method.invoke(realSubject, args);
        Log.d("denny", "after ProxyHandler#invoke");
        return result;
    }
}
// 通過Proxy創(chuàng)建代理類對象
RealSubjcet realSubjcet = new RealSubjcet();
Subject proxySubject = (Subject) Proxy.newProxyInstance(
        Subject.class.getClassLoader(),
        new Class[]{Subject.class},
        new ProxyHandler(realSubjcet));
proxySubject.call();

以及,最終的結(jié)果:
D/denny: before ProxyHandler#invoke
D/denny: RealSubject#call
D/denny: after ProxyHandler#invoke

APT

在編譯的時候咬荷,利用APT生成.java文件冠句。例如Dagger2、ButterKnife幸乒、EventBus3等懦底。
APT(Annotation Process Tool)是一種注解處理工具,它對源代碼文件進行檢測并找出其中的Annotation罕扎,使用Annotation進行額外的處理聚唐。
使用APT主要有以下幾個缺點:

  1. 相關(guān)API晦澀難懂,需要一定的編譯基礎(chǔ)壳影。
  2. APT無法掃描其他module拱层。
  3. 很難把相關(guān)代碼插入一個帶有返回值的方法之后(也就是在return之后)。

我們也可以通過繼承AbstractProcessor等方式宴咧,實現(xiàn)自己的APT根灯。

AspectJ

在.java編譯為.class(java字節(jié)碼)的時候,進行代碼注入。
AspectJ功能強大烙肺。語法較多纳猪,但是難度不大,掌握幾個常用的就能應(yīng)付大部分場合桃笙。

Javassist氏堤、ASM等字節(jié)碼操作類庫

這兩個工具比較相似,都是對已經(jīng)編譯好的class文件進行操作搏明。相比于AspectJ鼠锈,這兩個工具顯得更加強大靈活,可以直接對Java的字節(jié)碼進行修改星著,但是難度也較高购笆。
Java的二進制被存儲在嚴(yán)格格式定義的.class文件中,這些字節(jié)碼文件擁有足夠的元數(shù)據(jù)信息來表示類中的所有元素虚循,包括類名稱同欠、方法、屬性以及Java字節(jié)碼指令横缔。
ASM可以動態(tài)生成類或者增強既有類的功能铺遂。使用ASM,可以直接生成二進制.class文件茎刚,也可以在類被加載入Java虛擬機前動態(tài)改變既有類的行為襟锐。

下面這張圖說明了各個工具的作用時機。


AspectJ

AspectJ簡介

AspectJ是目前應(yīng)用最為廣泛的AOP實現(xiàn)方案斗蒋。
使用AspectJ開發(fā)有兩種打開方式:

  • 使用AspectJ的語言編寫.aj文件進行開發(fā)捌斧。這種語言與Java十分相似,只是多了幾個關(guān)鍵字而已
  • 使用AspectJ提供的注解泉沾,直接在Java語言上進行開發(fā)

但在Android開發(fā)中捞蚂,由于Android Studio并不認(rèn)識.aj文件,因此建議使用注解進行開發(fā)跷究。后續(xù)的例子也都使用注解的方式姓迅。一般情況下,使用提供的注解和一些簡單的語法就可以實現(xiàn)絕大部分功能上的需求了俊马。

Android中集成AspectJ

AspectJ的引入很簡單丁存。
首先,我們在項目根目錄的gradle中引入hujiang的gradle插件柴我。
當(dāng)然解寝,如果你愿意,也可以自己寫個gradle插件來實現(xiàn)艘儒。

Hujiang的插件是一個基于AspectJ并在此基礎(chǔ)上擴展出來可應(yīng)用于Android開發(fā)平臺的AOP框架聋伦,可作用于java源碼夫偶,class文件及jar包,同時支持kotlin的應(yīng)用觉增。感興趣的小伙伴可以看看他們的github

classpath "com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4"

之后兵拢,在app的gradle下添加AspectJ的依賴

apply plugin: 'android-aspectjx'
dependencies {
    compile 'org.aspectj:aspectjrt:1.8.10'
}

先舉個栗子

讓我們先看一個簡單的例子。

@After("execution(protected void com.example.myapplication.MainActivity.onCreate(android.os.Bundle))")
public void logCreate(JoinPoint joinPoint) {
    Log.d("denny", "MainActivity#onCreate");
}

在這個例子中逾礁,

  • @After定義了Advice说铃,After的含義是在切入點之后執(zhí)行。
  • execution(XXX)定義了JPoints的類型嘹履,execution的含義是截獲方法的執(zhí)行腻扇,并在執(zhí)行前/后插入切點。
  • protected void com.example.myapplication.MainActivity.onCreate(android.os.Bundle))定義了過濾條件植捎。在這里衙解,我們選擇的切入點是call,因此它的過濾條件一定是某個(或某些)函數(shù)焰枢。
    • protect過濾了Jpoint的訪問權(quán)限
    • void表示Jpoint無返回值
    • com.example.myapplication.MainActivity是函數(shù)的包名,緊跟其后的onCreate是函數(shù)名
    • android.os.Bundle同理舌剂,指明了函數(shù)只有一個參數(shù)济锄,參數(shù)的類型是位于android.os包下的Bundle

以上這個注解連起來的意思就是,選擇一個截獲了onCreate方法調(diào)用的切入點霍转,在該切入點后執(zhí)行l(wèi)ogCreate方法(打印一條log)荐绝。
其中,onCreate方法的過濾條件為:訪問類型為protect的方法避消,該方法位于com.example.myapplication.MainActivity中低滩,方法名為onCreate,參數(shù)為android.os.Bundle

AspectJ語法解析

JPoints 執(zhí)行點

Jpoint是指程序運行中可切入的點岩喷。
在Aspect中支持的Jpoints如下:

Jpoints 說明
Method call 方法被調(diào)用
Method execution 方法執(zhí)行
Constructor call 構(gòu)造函數(shù)被調(diào)用
Constructor execution 構(gòu)造函數(shù)執(zhí)行
Field get 讀取屬性
Field set 寫入屬性
Pre-initialization 與構(gòu)造函數(shù)有關(guān)恕沫,很少用到
Initialization 與構(gòu)造函數(shù)有關(guān),很少用到
Static initialization static 塊初始化
Handler 異常處理
Advice execution 所有 Advice 執(zhí)行

execution和call的含義是不同的纱意。

execution截獲的是方法真正執(zhí)行的代碼區(qū)婶溯,使用@Around可以控制原方法執(zhí)行與否,可以選擇執(zhí)行或者替換偷霉;
而call截獲的是方法的調(diào)用區(qū)迄委,它無法控制原來方法的執(zhí)行與否,只是在方法調(diào)用前后插入切點类少,因此比較適合做一些輕量的監(jiān)控(方法調(diào)用耗時等)

Pointcuts 切入點
概覽

Pointcuts定義了如何在眾多的Jpoints中選擇想要的切入點叙身。

AspectJ中,對于Pointcuts有一套標(biāo)準(zhǔn)的語法硫狞。使用這套語法信轿,可以實現(xiàn)許多強大的功能晃痴。在實際使用中,我們只需要掌握一些簡單的應(yīng)用就可以了虏两。至于那些高級玩法愧旦,等到項目需要的時候再去查詢文檔也不遲。

PointCuts中最常選擇的點與JPoint密切相關(guān)定罢,下面的表格給出了二者之間的關(guān)系:

Join Point 說明 Pointcuts語法
Method call 方法被調(diào)用 call(MethodPattern)
Method execution 方法執(zhí)行 execution(MethodPattern)
Constructor call 構(gòu)造函數(shù)被調(diào)用 call(ConstructorPattern)
Constructor execution 構(gòu)造函數(shù)執(zhí)行 execution(ConstructorPattern)
Field get 讀取屬性 get(FieldPattern)
Field set 寫入屬性 set(FieldPattern)
Pre-initialization 與構(gòu)造函數(shù)有關(guān)笤虫,很少用到 preinitialization(ConstructorPattern)
Initialization 與構(gòu)造函數(shù)有關(guān),很少用到 initialization(ConstructorPattern)
Static initialization static 塊初始化 staticinitialization(TypePattern)
Handler 異常處理 handler(TypePattern)
Advice execution 所有 Advice 執(zhí)行 adviceexcution()

除了上表中的Jpoint祖凫,AspectJ還提供其他一些選擇方法琼蚯,下表列出了一些常用的選擇非Jpoint的方法:

Pointcuts synatx 說明
within(TypePattern) 符合 TypePattern 的代碼中的 Join Point
withincode(MethodPattern) 在某些方法中的 Join Point
withincode(ConstructorPattern) 在某些構(gòu)造函數(shù)中的 Join Point
cflow(Pointcut) Pointcut 選擇出的切入點 P 的控制流中的所有 Join Point,包括 P 本身
cflowbelow(Pointcut) Pointcut 選擇出的切入點 P 的控制流中的所有 Join Point惠况,不包括 P 本身
this(Type or Id) Join Point 所屬的 this 對象是否 instanceOf Type 或者 Id 的類型
target(Type or Id) Join Point 所在的對象(例如 call 或 execution 操作符應(yīng)用的對象)是否 instanceOf Type 或者 Id 的類型
args(Type or Id, ...) 方法或構(gòu)造函數(shù)參數(shù)的類型
if(BooleanExpression) 滿足表達式的 Join Point遭庶,表達式只能使用靜態(tài)屬性、Pointcuts 或 Advice 暴露的參數(shù)稠屠、thisJoinPoint 對象

上面Pointcuts的語法中涉及到一些Pattern峦睡,這些Pattern的具體規(guī)則如下表所示,[]里的內(nèi)容是可選的:

Pattern 規(guī)則
MethodPattern [!] [@Annotation] [public,protected,private] [static] [final] 返回值類型 [類名.]方法名(參數(shù)類型列表) [throws 異常類型]
ConstructorPattern [!] [@Annotation] [public,protected,private] [final] [類名.]new(參數(shù)類型列表) [throws 異常類型]
FieldPattern [!] [@Annotation] [public,protected,private] [static] [final] 屬性類型 [類名.]屬性名
TypePattern 其他 Pattern 涉及到的類型規(guī)則也是一樣权埠,可以使用 '!'榨了、''、'..'攘蔽、'+'龙屉,'!' 表示取反,'*' 匹配除 . 外的所有字符串满俗,'*' 單獨使用事表示匹配任意類型转捕,'..' 匹配任意字符串,'..' 單獨使用時表示匹配任意長度任意類型唆垃,'+' 匹配其自身及子類五芝,還有一個 '...'表示不定個數(shù)

Pointcut和Pattern都可以使用!降盹、&&与柑、|| 來組合選取,其中的含義和Java一樣蓄坏,這里就不再贅述了价捧。

舉例

以MethodPattern為例:
一個MethodPattern的完整表達式為:@注解 訪問權(quán)限 返回值的類型 包名.函數(shù)名(參數(shù))

  • @注解和訪問權(quán)限(public/private/protect,以及static/final)屬于可選項涡戳。如果不設(shè)置它們结蟋,則默認(rèn)都會選擇。以訪問權(quán)限為例渔彰,如果沒有設(shè)置訪問權(quán)限作為條件嵌屎,那么public推正,private,protect及static宝惰、final的函數(shù)都會進行搜索植榕。

  • 返回值類型就是普通的函數(shù)的返回值類型。如果不限定類型的話尼夺,就用*通配符表示

  • 包名.函數(shù)名用于查找匹配的函數(shù)尊残。可以使用通配符淤堵,包括*和..以及+號寝衫。其中*號用于匹配除.號之外的任意字符,而..則表示任意子package拐邪,+號表示子類慰毅。比如:

    • java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date
    • Test*:可以表示TestBase扎阶,也可以表示TestDervied
    • java..*:表示任意以java開頭的包名汹胃,如java.a.Test
    • *..Date: 表示任意以Date結(jié)尾的包名,如a.b.Date
    • java..Date: 表示任意以java開頭东臀,以Date結(jié)尾的包名
    • java..*Model+:表示Java任意package中统台,名字以Model結(jié)尾的子類,比如TabelModel啡邑,TreeModel 等
  • 最后來看函數(shù)的參數(shù)。參數(shù)匹配比較簡單井赌,主要是參數(shù)類型谤逼,比如:

    • (int, char):表示參數(shù)只有兩個,并且第一個參數(shù)類型是int仇穗,第二個參數(shù)類型是char
    • (String, ..):表示至少有一個參數(shù)流部。并且第一個參數(shù)類型是String,后面參數(shù)類型不限纹坐。在參數(shù)匹配中枝冀,..代表任意參數(shù)個數(shù)和類型
    • (Object ...):表示不定個數(shù)的參數(shù),且類型都是Object耘子,這里的...不是通配符果漾,而是Java中代表不定參數(shù)的意思
更多

更詳細的Pointcuts定義,可以查看官方Pointcus說明谷誓。

Advice 執(zhí)行時機

Advice簡單來說就是hook點绒障,常見的有before、after捍歪、around三種類型户辱。

Advice 說明
@Before 在執(zhí)行Join Point之前
@After 在執(zhí)行Join Point之后鸵钝,包括正常的return和throw異常
@AfterReturning Join Point為方法調(diào)用且正常return時
@AfterThrowing Join Point為方法調(diào)用且拋出異常時
@Around 替代 Join Point 的代碼,如果要執(zhí)行原來代碼的話庐镐,要使用 ProceedingJoinPoint.proceed()

@After默認(rèn)包含了@AfterReturning和@AfterThrowing兩種類型恩商。
理論上說,@Before和@After能夠?qū)崿F(xiàn)的必逆,@Around也完全能夠?qū)崿F(xiàn)怠堪。@Around的目標(biāo)是替換原Jpoint。
對于同一個但是在對一個但是在對一個Pointcut聲明末患,@Before和@After可以同時使用研叫,但是在聲明@Around后再聲明@Before或者@After,則會報錯璧针。
它們的區(qū)別如下:

  • @Before和@After沒有返回值嚷炉,而@Around的返回值與原Jpoint匹配。
  • @Around可以決定目標(biāo)方法是否執(zhí)行探橱,甚至可以替換目標(biāo)為另一方法申屹。
AspectJ 切面

使用注解的方式開發(fā)AspectJ,需要在類的頭部聲明@AspectJ隧膏,如:

@Aspect
public class ActivityLifeCycle {
    // your methods
}

這個類就相當(dāng)于一個關(guān)注面哗讥。我們可以再定義一個PermisssionCheckAspect進行權(quán)限檢查,定義一個PerformancMonitorAspect進行性能檢測……所有的關(guān)注點的相關(guān)代碼都挪到一個類型進行控制胞枕。

@Pointcut聲明

@Pointcuts由org.aspectj.lang.annotation.Pointcut注解修飾的方法聲明杆煞,方法返回值只能是void。@Pointcutz修飾的方法只能由空的方法實現(xiàn)而且不能有throws語句腐泻,方法的參數(shù)和pointcut中的參數(shù)相對應(yīng)决乎。
比如,下面這種方式

@Before("execution(* *..MainActivity.onCreate(..))")
public void logCreate(JoinPoint joinPoint) {
    Log.d("denny", "MainActivity#onCreate");
}

也可以用@Pointcut寫成:

@Pointcut("execution(* *..MainActivity.onCreate(..))")
public void onCreatePointcut() {

}

@Before("onCreatePointcut()")
public void logCreate(JoinPoint joinPoint) {
    Log.d("denny", "MainActivity#onCreate");
}
call與execution

舉個例子派桩,進一步說明call和execution的區(qū)別构诚。
首先,定義Animal接口:

public interface Animal {
    void execute();
}

接著铆惑,定義Rabbit與Tiger實現(xiàn)Animal接口:

public class Rabbit implements Animal {
    @Override
    public void execute() {
        Log.d("denny", "Rabbit is running");
    }
}
public class Tiger implements Animal {
    @Override
    public void execute() {
        Log.d("denny", "Tiger is running");
    }
}

然后范嘱,在MainActivity的onCreate中:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        execute();
    }

    private void execute() {
        new Rabbit().execute();
        new Tiger().execute();
    }
}

最后,我們來看使用call和execute的區(qū)別:

@Before("call(* *.execute(..))")
public void logExecute(JoinPoint joinPoint) {
    Log.d("denny", "aspectj: " + joinPoint.getSourceLocation());
}

D/denny: aspectj: MainActivity.java:15
D/denny: aspectj: MainActivity.java:19
D/denny: Rabbit is running
D/denny: aspectj: MainActivity.java:20
D/denny: Tiger is running

@Before("execution(* *.execute(..))")
public void logExecute(JoinPoint joinPoint) {
    Log.d("denny", "aspectj: " + joinPoint.getSourceLocation());
}

打印的結(jié)果為:
D/denny: aspectj: MainActivity.java:19
D/denny: aspectj: Rabbit.java:8
D/denny: Rabbit is running
D/denny: aspectj: Tiger.java:8
D/denny: Tiger is running

結(jié)論:call攔截的是方法的調(diào)用员魏,所有execute的調(diào)用都在MainActivity中丑蛤。而execution攔截的是方法的執(zhí)行,分別在MainActivity逆趋、Rabbit和Tiger中盏阶。

within

within用來選取符合條件的Pointcut。還是使用上面的例子來說明:

@Before("call(* *.execute(..)) && within(*..MainActivity)")
public void executeAOP(JoinPoint joinPoint) {
    Log.d("denny", "aspectj before: " + joinPoint.getSourceLocation());
}

D/denny: aspectj before: MainActivity.java:15
D/denny: aspectj before: MainActivity.java:19
D/denny: Rabbit is running
D/denny: aspectj before: MainActivity.java:20
D/denny: Tiger is running
攔截的是MainActivity中的execute方法的調(diào)用闻书。

@Before("execution(* *.execute(..)) && within(*..MainActivity)")
public void executeAOP(JoinPoint joinPoint) {
    Log.d("denny", "aspectj before: " + joinPoint.getSourceLocation());
}

D/denny: aspectj before: MainActivity.java:19
2019-09-04 20:39:03.373 17272-D/denny: Rabbit is running
2019-09-04 20:39:03.373 17272-D/denny: Tiger is running
攔截的是MainActivity中的execute方法的執(zhí)行名斟,MainActivity中只有一個execute方法脑慧,因此只攔截到了一處。

target與this

前面說過砰盐,AspetcJ是屬于靜態(tài)織入的闷袒,但其實AspectJ也有動態(tài)織入的部分,而target()與this()就是屬于它動態(tài)織入的方式岩梳。所以target()與this()需要在在運行時才能確定那些被攔截囊骤。

先給結(jié)論:

  • target是指:我們pointcut所選取的Join point的所有者,直白點說就是: 指明攔截的方法屬于那個類冀值。
  • this是指: 我們pointcut所選取的Join point的調(diào)用/執(zhí)行的所有者也物,就是說:方法是在那個類中被調(diào)用/執(zhí)行的。

下面的例子可以幫助大家理解兩者的微妙區(qū)別:

@Before("call(* *.execute(..))")
public void executeAOP(JoinPoint joinPoint) {
    Log.d("denny", "aspectj before: " + joinPoint.getSourceLocation()
        + " this: " + joinPoint.getThis().getClass().getSimpleName()
        + " target: " + joinPoint.getTarget().getClass().getSimpleName());
}

打印的結(jié)果為:
D/denny: aspectj before: MainActivity.java:15 this: MainActivity target: MainActivity
D/denny: aspectj before: MainActivity.java:19 this: MainActivity target: Rabbit
D/denny: Rabbit is running
D/denny: aspectj before: MainActivity.java:20 this: MainActivity target: Tiger
D/denny: Tiger is running
可以看到列疗,log中所有的this都是MainActivity滑蚯,事實上,我們所選擇的切入點(execute方法)確實都是在MainActivity中被調(diào)用的抵栈。而target是指所攔截的方法屬于哪個類告材,因此打印出來的分別是MainActivity、Rabbit和Tiger古劲。

@Before("execution(* *.execute(..))")
public void executeAOP(JoinPoint joinPoint) {
    Log.d("denny", "aspectj before: " + joinPoint.getSourceLocation()
        + " this: " + joinPoint.getThis().getClass().getSimpleName()
        + " target: " + joinPoint.getTarget().getClass().getSimpleName());
    }

打印的結(jié)果為:
D/denny: aspectj before: MainActivity.java:19 this: MainActivity target: MainActivity
D/denny: aspectj before: Rabbit.java:8 this: Rabbit target: Rabbit
D/denny: Rabbit is running
D/denny: aspectj before: Tiger.java:8 this: Tiger target: Tiger
D/denny: Tiger is running
和上一個例子相比斥赋,這里使用execution攔截方法的執(zhí)行。target沒有發(fā)生變化产艾,但是this發(fā)生了變化疤剑。對于Rabbit和Tiger來說,它們的execute()方法是在MainActivity中被調(diào)用闷堡,但是執(zhí)行當(dāng)然是在自己的類中啦骚露。

target()和this()還存在繼承關(guān)系作用,也就是說:如果你的signature是一個基類缚窿,那么這個pointcut同時也會對他的子類也起作用。這里就不舉例了焰扳,有興趣的小伙伴可以自行實驗一下倦零。

另外,target和this可以獲取他們對應(yīng)的實例吨悍,但within不行扫茅。看下面這個例子:

@Before("call(* *.execute(..)) && this(mainActivity) && target(tiger)")
public void executeAOP(JoinPoint joinPoint, MainActivity mainActivity, Tiger tiger) {
    Log.d("denny", "aspectj before: " + joinPoint.getSourceLocation()
        + " this: " + mainActivity.getClass().getSimpleName()
        + " target: " + tiger.getClass().getSimpleName());
}
if表達式

在基于AspectJ注解的開發(fā)方式中育瓜,if(...)表達式的用法與其他的選擇操作符不同葫隙,在@Pointcut的語句中if表達式只能是if()、if(true)或if(false)躏仇,而且@Pointcut方法必須為public static boolean恋脚,方法體內(nèi)就是if表達式的內(nèi)容腺办,可以使用暴露的參數(shù)、靜態(tài)屬性糟描、JoinPoint怀喉、JoinPoint.StaticPart、JoinPoint.EnclosingStaticPart。

JoinPoint.StaticParts僅包含join point的靜態(tài)信息。

下面這個例子中鱼的,“ImeService#onStartInputView_before”只會在打印五次闪盔。

private static int COUNT = 0;

@Pointcut("execution(* *..ImeService.onStartInputViewInternal(..)) && if()")
public static boolean onStartInputView() {
    return COUNT++ < 5;
}

@Before("onStartInputView()")
public void onBeforeStartInputView(JoinPoint.StaticPart joinPoint) {
    Log.d("denny", "ImeService#onStartInputView_before");
}

代碼混淆

通過上述方式切入的代碼是可以混淆的。
代碼是在編譯階段織入卷胯,所以混淆是不會有影響,只有在運行時你需要通過類名,方法名去做一些事情的時候才不能混淆菱蔬,比如用到了反射技術(shù)等。任何在編譯階段植入代碼的AOP方案混淆都不會受影響荒辕,和混淆無關(guān)汗销。而在運行時的AOP方案就會受混淆影響。

反編譯查看實現(xiàn)

我們可以通過反編譯抵窒,看看AspectJ是如何在不修改原有代碼的情況下弛针,實現(xiàn)無縫插入的。
源碼:

public class MainActivity extends AppCompatActivity {
    private void testA() {
        Log.d("denny", "initial method");
    }
}

切面代碼:

@Aspect
public class AspectJ {
    @Around("execution(* *..testA(..))")
    public void log(ProceedingJoinPoint point) throws Throwable {
        point.proceed();
        Log.d("denny", "aspectj test");
        Log.d("denny", Log.getStackTraceString(new Throwable()));
    }
}

反編譯后李皇,MainActivity的代碼如下:

public class MainActivity extends AppCompatActivity {
    private static final /* synthetic */ StaticPart ajc$tjp_0 = null;

    static {
        ajc$preClinit();
    }

    private static /* synthetic */ void ajc$preClinit() {
        Factory factory = new Factory("MainActivity.java", MainActivity.class);
        ajc$tjp_0 = factory.makeSJP(JoinPoint.METHOD_EXECUTION, (Signature) factory.makeMethodSig("2", "testA", "com.example.dany.aspectjapplication.MainActivity", "", "", "", "void"), 61);
    }

    private void testA() {
        JoinPoint makeJP = Factory.makeJP(ajc$tjp_0, this, this);
        testA_aroundBody1$advice(this, makeJP, AspectJ.aspectOf(), (ProceedingJoinPoint) makeJP);
    }

    private static final /* synthetic */ void testA_aroundBody1$advice(MainActivity ajc$this, JoinPoint thisJoinPoint, AspectJ ajc$aspectInstance, ProceedingJoinPoint point) {
        Log.d("denny", "initial method");
        Log.d("denny", "aspectj test");
        Log.d("denny", Log.getStackTraceString(new Throwable()));
    }
}

我們主要關(guān)心testA方法即可削茁。不難發(fā)現(xiàn),我們的代碼被AspectJ重構(gòu)了掉房。在不修改源代的前提下茧跋,對java字節(jié)碼進行操作,實現(xiàn)無縫插入卓囚。

Best Practice

幫助定位問題

如果我們需要定位一個問題瘾杭,但是又不便于調(diào)試,那么AspectJ或許可以幫助我們哪亿。
舉一個在實際項目中遇到的例子粥烁。
用戶反饋了一個問題,通過查看代碼推測蝇棉,可能是網(wǎng)絡(luò)狀態(tài)的判斷出現(xiàn)了異常讨阻。但是,在調(diào)試的狀態(tài)下(更準(zhǔn)確地說篡殷,是在充電的情況下)钝吮,網(wǎng)絡(luò)狀態(tài)的判斷是正常的。
因此,接下來的思路就是奇瘦,通過log的形式記錄當(dāng)前的網(wǎng)絡(luò)狀態(tài)棘催,寫入文件幫助確定問題。
對網(wǎng)絡(luò)狀態(tài)的判斷的相關(guān)代碼在另一個組件中链患,無法直接在其中添加代碼巧鸭。此時,我想到了AspectJ:

@Aspect
public class AspectJTest {
    @After("execution(* *..A.B(..))")
    public void logXg(ProceedingJoinPoint point) throws Throwable {
        Log.d("denny", "current state: " + state);
    }
}

類似的例子還有很多麻捻,對于一些無法或者不方便修改源碼的第三方庫纲仍,我們都可以借助AspectJ切入。例如贸毕,在Bitmap的createBitmap方法之后記錄相關(guān)信息郑叠、在OnClickListener#onClick中記錄View的狀態(tài)等等。

利用AspectJ進行數(shù)據(jù)統(tǒng)計/埋點

AOP最直觀的一個實踐明棍,就是在數(shù)據(jù)統(tǒng)計中了乡革。
下面給出一個統(tǒng)計方法執(zhí)行時間的例子。
首先摊腋,我們定義注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PerformanceTrace {
}

然后沸版,定義AspectJ:

@Aspect
public class AspectJTest {
    @Around("execution(@*..PerformanceTrace * *(..))")
    public Object logPerformance(ProceedingJoinPoint point) throws Throwable {
        final long start = System.currentTimeMillis();
        final Object result = point.proceed();
        final long end = System.currentTimeMillis();
        Log.d("denny", "time cost: " + (end - start) + "ms"
                + " | method signature: " + point.getSignature());
        return result;
    }
}

最后,只需要在需要進行性能測試的方法前兴蒸,加上@PerformanceTrace注解就可以了视粮。

jake Wharton大神已經(jīng)為我們提供了開源工具實現(xiàn)類似的功能:Hugo。其內(nèi)部實現(xiàn)橙凳,正是借用了AspectJ蕾殴。

檢查權(quán)限

與上面的例子相似,我們首先定義注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionAnnotation {
    String[] declaredPermission() default "";
}

之后岛啸,定義切點:

@Aspect
public class AspectJTest {
    @Around("execution(@*..PermissionAnnotation * *(..)) && @annotation(annotation)")
    public void permissionCheck(ProceedingJoinPoint point, PermissionAnnotation annotation) throws Throwable {
        final String[] declaredPermission = annotation.declaredPermission();
        final Context context = (Context) point.getThis();  // could lead to a null pointer
        AndPermission.with(context)
                .permission(declaredPermission)
                .onGranted(action -> {
                    try {
                        point.proceed();
                    } catch (Throwable throwable) {
                        throwable.printStackTrace();
                    }
                })
                .onDenied(action -> Toast.makeText(context, "Permission Deny", Toast.LENGTH_SHORT).show())
                .start();
    }
}

最后钓觉,在需要進行權(quán)限檢查的地方加上注解即可:

@PermissionAnnotation(declaredPermission = {Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS})

更多例子

更多的例子,可以參考Hujiang的這個Demo

AspectJ存在的問題

無法織入的問題

舉個例子坚踩,如果我們想要統(tǒng)計所有的Activity的onPause方法的耗時荡灾,我們可以這樣定義PointCut:

@Around("execution(* android.app.Activity+.onPause(..))")
public void logPerformance(ProceedingJoinPoint point) throws Throwable {
    // do something you want
}

但是,這樣做會導(dǎo)致兩個問題:

  1. 如果我們的Activity沒有復(fù)寫onPause方法瞬铸,那么將不會織入卧晓。這是因為android.app.Activity位于android設(shè)備內(nèi),不參與打包的過程赴捞。如果是support包中的Activity或者Fragment,就不會受到影響郁稍。
  2. 如果我們的Activity繼承了BaseActivity赦政,BaseActivity又繼承了android.app.Activity,那么這兩個Activity都會被織入,這就造成了重復(fù)統(tǒng)計的問題恢着。

出現(xiàn)問題較難排查

AspectJ是通過修改字節(jié)碼的方式實現(xiàn)AOP桐愉,對上層無感知,因此出現(xiàn)問題的時候較難定位問題原因掰派。在項目中如果使用了AOP从诲,應(yīng)當(dāng)盡可能留下文檔。

編譯時間變長

在Gradle的Transform過程靡羡,會遍歷所有class文件系洛,查找符合需求的切入點,然后插入字節(jié)碼略步。如果項目較大且織入代碼較多描扯,會增加編譯時間。

解決辦法包括:
使用exclude過濾掉不需要執(zhí)行織入的包名趟薄。
如果織入代碼在debug環(huán)境不需要織入绽诚,比如埋點,可以主動關(guān)閉AspectJ功能杭煎。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末恩够,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子羡铲,更是在濱河造成了極大的恐慌蜂桶,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件犀勒,死亡現(xiàn)場離奇詭異屎飘,居然都是意外死亡,警方通過查閱死者的電腦和手機贾费,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門钦购,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人褂萧,你說我怎么就攤上這事押桃。” “怎么了导犹?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵唱凯,是天一觀的道長。 經(jīng)常有香客問我谎痢,道長磕昼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任节猿,我火速辦了婚禮票从,結(jié)果婚禮上漫雕,老公的妹妹穿的比我還像新娘。我一直安慰自己峰鄙,他們只是感情好浸间,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吟榴,像睡著了一般魁蒜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吩翻,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天兜看,我揣著相機與錄音,去河邊找鬼仿野。 笑死铣减,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的脚作。 我是一名探鬼主播葫哗,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼球涛!你這毒婦竟也來了劣针?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤亿扁,失蹤者是張志新(化名)和其女友劉穎捺典,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體从祝,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡襟己,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了牍陌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片擎浴。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖毒涧,靈堂內(nèi)的尸體忽然破棺而出贮预,到底是詐尸還是另有隱情,我是刑警寧澤契讲,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布仿吞,位于F島的核電站,受9級特大地震影響捡偏,放射性物質(zhì)發(fā)生泄漏唤冈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一银伟、第九天 我趴在偏房一處隱蔽的房頂上張望你虹。 院中可真熱鬧凉当,春花似錦、人聲如沸售葡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挟伙。三九已至,卻和暖如春模孩,著一層夾襖步出監(jiān)牢的瞬間尖阔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工榨咐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留介却,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓块茁,卻偏偏與公主長得像齿坷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子数焊,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

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