Android之AOP架構(gòu)<第一篇>:入門

(1)AOP的概念

AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過預(yù)編譯方式和運(yùn)行期動態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。AOP是OOP的延續(xù),是軟件開發(fā)中的一個(gè)熱點(diǎn),是函數(shù)式編程的一種衍生范型。利用AOP可以對業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開發(fā)的效率。

場景

首先來看以下代碼:

private void method_1(){

    long startTime = System.currentTimeMillis();

    //...業(yè)務(wù)邏輯

    //...業(yè)務(wù)邏輯

    long endTime = System.currentTimeMillis();
    LogUtil.i(TAG, "method_1耗時(shí)時(shí)長:"+(endTime-startTime));

}

private void method_2(){

    long startTime = System.currentTimeMillis();

    //...業(yè)務(wù)邏輯

    //...業(yè)務(wù)邏輯

    long endTime = System.currentTimeMillis();
    LogUtil.i(TAG, "method_1耗時(shí)時(shí)長:"+(endTime-startTime));

}

private void method_3(){

    long startTime = System.currentTimeMillis();

    //...業(yè)務(wù)邏輯

    //...業(yè)務(wù)邏輯

    long endTime = System.currentTimeMillis();
    LogUtil.i(TAG, "method_1耗時(shí)時(shí)長:"+(endTime-startTime));

}

有三個(gè)方法,這三個(gè)方法實(shí)現(xiàn)了不同的業(yè)務(wù)邏輯,此時(shí),需要統(tǒng)計(jì)它們執(zhí)行的耗時(shí)時(shí)間,那么如上代碼的實(shí)現(xiàn)方式是可以實(shí)現(xiàn)的。但是在某些大廠是不允許這樣實(shí)現(xiàn)的,因?yàn)?code>這使本身的業(yè)務(wù)邏輯與日志邏輯相互耦合,很顯然,這違反了單一功能原則。

以上代碼可以繪制一張圖來表示,如下:

圖片.png

現(xiàn)在大致介紹一下上圖,在一個(gè)大型項(xiàng)目,有多個(gè)方法,一般情況下,一個(gè)方法只做一件事,但是呢?因?yàn)樾枨蟮谋匾?,有些時(shí)候會發(fā)生一個(gè)方法里面存在和業(yè)務(wù)無關(guān)的代碼,比如日志打印,如果每個(gè)方法都加上日志,那么就破壞了方法的單一原則,為了使代碼簡潔, 單一原則是必須遵守的,所以原則上,業(yè)務(wù)邏輯日志邏輯必須解耦。

這里就需要采用面向切面編程(AOP)來實(shí)現(xiàn)解耦了。

再來看下圖:

圖片.png

圖中畫出了性能檢測切面日志切面,這兩個(gè)功能的代碼需要放在某方法中才能實(shí)現(xiàn),切面的代碼和業(yè)務(wù)邏輯完全解耦,原理是:定義一個(gè)切面,從源碼層看,切面和業(yè)務(wù)邏輯完全解耦,當(dāng)編譯生成字節(jié)碼(class)文件時(shí),將對應(yīng)的代碼動態(tài)注入到指定方法中,這個(gè)動態(tài)注入可以描述成動態(tài)代理。

所以,可以總結(jié)出AOP的優(yōu)勢:減少重復(fù)代碼、提高開發(fā)效率、維護(hù)方便,簡單說就是:解耦!簡單!好維護(hù)

(2)AOP在Android中的實(shí)現(xiàn)

[AspectJX框架]

在Android中實(shí)現(xiàn)AOP,一般采用AspectJX框架,工欲善其事,必先利其器,我們有必要引用已有三方庫,在github搜索下AspectJX,可以找到一些AspectJX框架的遠(yuǎn)程倉庫,本文引用以下依賴庫,如下:

https://github.com/JakeWharton/hugo

打開這個(gè)鏈接,可以發(fā)現(xiàn),AspectJX的依賴配置方式已經(jīng)為我們提供了。

[AOP基本術(shù)語]

  • Joinpoint(連接點(diǎn)):類里面可以被增強(qiáng)的方法,這些方法成為連接點(diǎn)

  • Pointcut(切入點(diǎn):):所謂切入點(diǎn)就是我們實(shí)際增強(qiáng)的那些方法

  • Advice(通知/增強(qiáng)):增強(qiáng)的邏輯,稱為增強(qiáng),比如擴(kuò)展日志功能,這個(gè)日志功能稱為增強(qiáng)

  • 前置通知:在方法之前執(zhí)行

  • 后置通知:在方法之后執(zhí)行

  • 異常通知:方法出現(xiàn)異常

  • 最終通知:在后置之后執(zhí)行

  • 環(huán)繞通知:在方法之前和之后執(zhí)行

  • 切面:把增強(qiáng)應(yīng)用到具體方法上面,過程稱為切面把增強(qiáng)用到切入點(diǎn)過程

[常用注解]

@Aspect:聲明切面,標(biāo)記類
@Pointcut(切點(diǎn)表達(dá)式):定義切點(diǎn),標(biāo)記方法
@Before(切點(diǎn)表達(dá)式):前置通知,切點(diǎn)之前執(zhí)行
@Around(切點(diǎn)表達(dá)式):環(huán)繞通知,切點(diǎn)前后執(zhí)行
@After(切點(diǎn)表達(dá)式):后置通知,切點(diǎn)之后執(zhí)行
@AfterReturning(切點(diǎn)表達(dá)式):返回通知,切點(diǎn)方法返回結(jié)果之后執(zhí)行
@AfterThrowing(切點(diǎn)表達(dá)式):異常通知,切點(diǎn)拋出異常時(shí)執(zhí)行
@Pointcut、@Before、@Around、@After、@AfterReturning、@AfterThrowing需要在切面類中使用,即在使用@Aspect的類中。

[切面(Aspect)]

定義一個(gè)切面比較簡單,只需要在類上加上一個(gè)@Aspect注解即可。

@Aspect
public class TestAnnoAspect {

}

TestAnnoAspect就是所謂的切面了,切面中主要定義一些切點(diǎn)。

[切點(diǎn)(Pointcut)]

@Aspect
public class TestAnnoAspect {

    @Pointcut("execution(* com.example.aopdemo.MainActivity.test(..))")
    public void pointcut() {
        Log.i("yunchong", "pointcut");
    }
}

這個(gè)切點(diǎn)對應(yīng)MainActivity的test方法,如下:

private void test(){
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法");
}

@Pointcut用于定義一個(gè)切點(diǎn),所以pointcut方法就是一個(gè)切點(diǎn),execution后面括號中的內(nèi)容是切點(diǎn)的語法,常用的切點(diǎn)種類有兩種:

方法執(zhí)行:execution(MethodSignature)
方法調(diào)用:call(MethodSignature)

本文就以execution為例,那么* com.example.aopdemo.MainActivity.test(..)是什么意思呢?

號代表任意返回值類型,由于test方法的返回值類型是void,所以號也可以改成void,test后面的兩個(gè)點(diǎn)表示任意參數(shù)。

[切點(diǎn)的處理]

切點(diǎn)定義好之后就可以使用切點(diǎn)表達(dá)式來處理這個(gè)切點(diǎn)。切點(diǎn)表達(dá)式有@Before、@Around、@After、@AfterReturning、@AfterThrowing

看一下如下代碼:

@Aspect
public class TestAnnoAspect {

    @Pointcut("execution(void com.example.aopdemo.MainActivity.test(..))")
    public void pointcut() {
        Log.i("yunchong", "pointcut");
    }

    @Before("pointcut()")
    public void before(JoinPoint point) {
        Log.i("yunchong", "before");
    }


    @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        Log.i("yunchong", "around");
        joinPoint.proceed();// 目標(biāo)方法執(zhí)行完畢
    }

    @After("pointcut()")
    public void after(JoinPoint point) {
        Log.i("yunchong", "after");
    }

    @AfterReturning("pointcut()")
    public void afterReturning(JoinPoint point, Object returnValue) {
        Log.i("yunchong", "afterReturning");
    }

    @AfterThrowing(value = "pointcut()", throwing = "ex")
    public void afterThrowing(Throwable ex) {
        Log.i("yunchong", "afterThrowing:"+ex.getMessage());
    }
}

@Before("pointcut()")表示先執(zhí)行被@Before修飾的方法,編譯后,MainActivity.class文件中的test方法如下:

private void test() {
    JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
    TestAnnoAspect.aspectOf().before(var1);
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法1");
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法2");
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法3");
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法4");
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法4");
}

打印結(jié)果如下:

圖片.png

JoinPoint代碼一個(gè)點(diǎn),用專業(yè)的說法叫埋點(diǎn),也就是說,@Before表達(dá)式在test方法的開頭埋下了一個(gè)點(diǎn)。

@After("pointcut()")表示先執(zhí)行MainActivity中的test方法,后執(zhí)行切面中被@After修飾的方法,編譯后,MainActivity.class文件中的test方法如下:

private void test() {
    JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);

    try {
        Log.i("yunchong", "執(zhí)行了MainActivity中的test方法1");
        Log.i("yunchong", "執(zhí)行了MainActivity中的test方法2");
        Log.i("yunchong", "執(zhí)行了MainActivity中的test方法3");
        Log.i("yunchong", "執(zhí)行了MainActivity中的test方法4");
        Log.i("yunchong", "執(zhí)行了MainActivity中的test方法4");
    } catch (Throwable var3) {
        TestAnnoAspect.aspectOf().after(var1);
        throw var3;
    }

    TestAnnoAspect.aspectOf().after(var1);
}

打印結(jié)果如下:

圖片.png

@Around("pointcut()") 需要也別注意

假設(shè)切點(diǎn)是這樣的

@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
    Log.i("yunchong", "around");
}

打印結(jié)果如下:

圖片.png

實(shí)際上,test方法中的原有代碼根本就沒執(zhí)行到,因?yàn)榍悬c(diǎn)中還需要這樣一句代碼:

joinPoint.proceed();// 目標(biāo)方法執(zhí)行完畢

假設(shè),切點(diǎn)修改為如下:

@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
    Log.i("yunchong", "around1");
    joinPoint.proceed();// 目標(biāo)方法執(zhí)行完畢
    Log.i("yunchong", "around2");
}

打印結(jié)果如下:

圖片.png

所以,被@Around修飾的切點(diǎn),是否執(zhí)行目標(biāo)方法,joinPoint.proceed()可以控制。

@AfterReturning("pointcut()")和目標(biāo)方法的返回值有關(guān)。

假如test方法加上返回值,如下:

private int test(){
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法1");
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法2");
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法3");
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法4");
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法4");
    return 1;
}

那么,編譯之后,MainActivity.class文件中的test方法如下:

private int test() {
    JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法1");
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法2");
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法3");
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法4");
    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法4");
    byte var2 = 1;
    TestAnnoAspect.aspectOf().afterReturning(var1, (Object)null);
    return var2;
}

打印結(jié)果如下:

圖片.png

@AfterThrowing(value = "pointcut()", throwing = "ex")和異常有關(guān),只有出現(xiàn)異常之后才會生效,修改test方法中的代碼如下:

private void test(){

    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法1");

    int a = 1/0 ;

    Log.i("yunchong", "執(zhí)行了MainActivity中的test方法2");

}

編譯之后,MainActivity.class文件中的test方法如下:

private void test() {
    try {
        Log.i("yunchong", "執(zhí)行了MainActivity中的test方法1");
        int a = 1 / 0;
        Log.i("yunchong", "執(zhí)行了MainActivity中的test方法2");
    } catch (Throwable var3) {
        TestAnnoAspect.aspectOf().afterThrowing(var3);
        throw var3;
    }
}

也就是說,代碼塊被try...catch包裹,自帶捕獲異常的功能。

打印結(jié)果如下:

圖片.png
總結(jié):

Android AOP其實(shí)就是在某方法的開頭或者結(jié)尾處埋下一個(gè)點(diǎn),在指定點(diǎn)插入想要動態(tài)注入的代碼。在原方法里,方法里面的功能是單一的,沒有任何其它邏輯,符合單一性原則,編譯之后,在字節(jié)碼文件中會生成與原方法不一樣的代碼,字節(jié)碼中的代碼比原代碼多了被動態(tài)注入的代碼。

在Android中,很多場景都可以使用AOP編程的思想,如:

登錄判斷
網(wǎng)絡(luò)判斷
權(quán)限獲取
數(shù)據(jù)校驗(yàn)
日志輸出
性能監(jiān)控 
按鈕防抖

[本章完...]

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容