Welcome to Android AOP

What?

As we all know,在進行項目構(gòu)建時,追求各模塊高內(nèi)聚,模塊間低耦合。然而現(xiàn)實并不總是如此美好,某些通用功能是橫跨并嵌入到其他各模塊中的。比如日志打印、方法的執(zhí)行時間統(tǒng)計、參數(shù)校驗等。這些功能星羅棋布得分散在項目工程中,既不方便統(tǒng)一管理,也不利于后期維護。

如何對這些零散卻通用的功能做一些優(yōu)化處理呢? AOP(Aspect-Oriented Programming),為我們提供了一種新的思路。AOP翻譯成中文是面向切面編程,其與OOP一樣,是一種編程范式。如果說OOP是把問題劃分到單個模塊的話,那么AOP就是把涉及到眾多模塊的某一類問題進行統(tǒng)一管理。AOP可通過預(yù)編譯和運行時動態(tài)代理的方式實現(xiàn),從而更好地組織工程和代碼結(jié)構(gòu),AOP的實現(xiàn)原理如圖1所示。

圖1 AOP實現(xiàn)原理

AOP已廣泛應(yīng)用于服務(wù)端編程,并取得了巨大的成功,如大名鼎鼎的Spring框架。而在本文中,我們將AOP引入Android工程,用以解決開篇提到的編程痛點。閱讀完本文后便會發(fā)現(xiàn),AOP在Android項目中同樣可以做一些有趣的事。

Where?

AOP在Android工程中能用在哪?或者說能用AOP來做什么?下面列舉了一些使用場景:

  • 日志記錄;
  • 持久化;
  • 性能監(jiān)控;
  • 方法的參數(shù)校驗;
  • 緩存;
  • 權(quán)限檢查:業(yè)務(wù)權(quán)限(如登陸,或用戶等級)、系統(tǒng)權(quán)限(如拍照定位)。
  • 其他更多

當(dāng)然,用AOP還可以實現(xiàn)一些具體的業(yè)務(wù)需求。

How?

介紹了這么多AOP的概念,那么如何進行具體應(yīng)用呢?下面介紹幾種現(xiàn)有的工具和類庫,可以很方便地實現(xiàn)AOP編程。

  • AspectJ 一種基于JavaTM編程語言的面向切面編程無縫擴展,適用Android,編譯期織入目標字節(jié)碼文件中,實現(xiàn)無縫侵入。
  • Javassist for Android 知名Java類庫Javassist的Android平臺移植版,同樣操作字節(jié)碼文件。
  • DexMaker 在Dalvik VM上編譯期或運行時生成目標代碼的Java API。
  • ASMDEX 一個類似ASM的字節(jié)碼操作庫,運行在Android平臺,操作字節(jié)碼文件。

琳瑯滿目的兵器庫中,當(dāng)然要選擇一件最趁手的,就是AspectJ了。選擇AspectJ的具體原因主要有三點:1、功能強大,能滿足大部分需求;2、支持編譯期和加載時代碼注入,無侵入實現(xiàn);3、易于使用。

AspectJ是一種幾乎和Java完全一樣的語言而且完全兼容Java。使用AspectJ有兩種方法,除了使用AspectJ的語法之外,還支持原生Java。然而,無論使用哪種方法,最后都需要AspectJ的編譯工具ajc來實現(xiàn)編譯,由于AspectJ實際上脫胎于Java,所以ajc工具也能編譯Java源碼。

AspectJ支持編譯期和加載時代碼注入,可以理解為是一種Hook。在開始之前,我們先看幾個關(guān)鍵詞:Join Point、Pointcut、Advice、Aspect、Weaving。這幾個關(guān)鍵詞在AspectJ中的抽象關(guān)系如圖2所示。

圖2 AspectJ中各組件的抽象關(guān)系

接下來詳細闡述各個關(guān)鍵詞的含義和作用。

  • Join point: 程序中可能作為代碼注入目標的特定的入口。通俗來說就是可能被我們Hook的入口,這個入口可以是方法調(diào)用、執(zhí)行、某個變量、類初始化、異常處理等,Join Point的常用類型如表所示。
Join Point 描述 示例
method call 方法調(diào)用 如調(diào)用a.method(),此處為Join Point
method execution 方法執(zhí)行 如a.method()執(zhí)行內(nèi)部,此處為Join Point
constructor call 構(gòu)造方法調(diào)用 同method call類似
constructor execution 構(gòu)造方法執(zhí)行 同method execution類似
field get get 變量 如讀取a.field成員變量,此處為Join Point
field set set 變量 如設(shè)置a.field成員變量,此處為Join Point
static initialization 類初始化 如class A中的static{},此處為Join Point
handler 異常處理 如try-catch(xxx)中對應(yīng)catch內(nèi)部的執(zhí)行
  • Pointcut: 告訴代碼注入工具,在何處注入一段特定代碼的表達式。可以將Ponitcut理解為Join Point的過濾規(guī)則,通過過濾規(guī)則篩選出一些特定的Join Point。
    舉個Pointcut的栗子可能更容易說明問題:

public pointcut test call(public * *.method(…)) && !within(A);

  • 第一個public:表示該pointcut的訪問權(quán)限是public的,這與AspectJ的繼承關(guān)系有關(guān),屬于AspectJ的高級語法,涉及到的應(yīng)用場景較少,此處暫不討論;
  • pointcut:關(guān)鍵字,表示該語句是一個pointcut;
  • test:pointcut可分為具名和匿名兩種形式,考慮方便調(diào)用起見,建議使用具名的方式。namedPointcut的冒號后面部分是真正pointcut的內(nèi)容,也就是篩選規(guī)則;
  • call:表示選擇的Join point是一種“方法調(diào)用”類型,對應(yīng)表一的method call,下表展示了常用的Pointcut類型;
Join Point類型 Pointcut類型
method call call(MethodSignature)
method execution execution(MethodSignature)
constructor call call(ConstructorSignature)
constructor execution execution(ConstructorSignature)
Field read access get(FieldSignature)
field write access set(FieldSignature)
static initialzation staticinitialzation(TypeSignature)
handler handler(TypeSignature)

Pointcut的過濾規(guī)則語法內(nèi)容較多,規(guī)則如下:

@注解(可選) 訪問權(quán)限(可選) 返回值的類型 包名.函數(shù)名(參數(shù))

  • 第二個public:表示的是目標Join Point的訪問權(quán)限,為可選項,如目標Join Point是一個方法,則該方法的訪問類型為public。如果不設(shè)置,則默認所有訪問權(quán)限(public|protected|defult|private)都匹配;
  • **.method(..):第一個*表示的是方法返回值類型,此處為任意類型。第二個*表示完整的類名,此處為任意包名和類名。method表示方法名稱,..表示參數(shù)類型和參數(shù)個數(shù),此處意為任意類型和任意參數(shù)個數(shù)。包名.方法名用于查找匹配的方法,可以使用通配符。包括*和..和+。其中*匹配除.之外的任意字符,而..則匹配某個包下的任意子包,+匹配某個類的任意子類。舉些栗子:

com.*.util:可以表示com.netease.util,也可以表示com.google.util;
com.netease.*AOP,可以表示com.netease.AndroidAOP,也可以表示com.neteast.JavaAOP;
com..*表示com包中任意子包下的任意子類;
com..*AOP+表示com包下任意子包下以AOP結(jié)尾的子類,如AndroidAop的子類;

方法參數(shù)也有相應(yīng)的匹配規(guī)則:

(int, String):表示第一個參數(shù)為int,第二個參數(shù)為String;
(int, ..):表示第一個參數(shù)為int,后面參數(shù)任意數(shù)量和類型;
(String …):表示不定參數(shù),且類型都為String;
(..):表示任意數(shù)量和類型的參數(shù);

  • !within(A):這是對Join Point的附加篩選。與Java語法相似,!代表非邏輯,within(A)表示某個包或類中的所有Join Point,此處為類A中所有Join Point。常用的附加篩選條件如表所示。
附加篩選 描述 示例
within(TypeSignature) 參數(shù)表示package或class,可使用通配符 如within(A),表示class A中的所有Join Point
withincode(Constructor&Method) 與within類似,匹配精確到方法 如within(A.method())表示class的method()涉及到的所有Join Point
this(Type) 判斷Join Point是否是Type類型 Join Point屬于某個類,this用以判斷該Join Point所在的類是否是Type類型
args(TypeSignature) 對參數(shù)進行條件搜索 args(String...),表示參數(shù)為String的不定參數(shù)

所以,總結(jié)起來,上文的Pointcut的栗子的語義就是:

選擇那些任意包名和類名的&&訪問權(quán)限為public的&&方法名為method的&&參數(shù)任意的方法作為目標Join Point。但需要排除class A中的所有Join Point。

  • Advice: 典型的 Advice 類型有 before、after 和 around,分別表示在目標方法執(zhí)行前、執(zhí)行后和完全替代目標方法執(zhí)行的代碼。Advice的作用就是,在Pointcut篩選出特定的執(zhí)行點和入口,做真正的Hook操作。常見的Advice類型如表所示。
Advice類型 描述
before() 表示在Join Points之前前Hook并執(zhí)行
after() 表示在Join Points執(zhí)行完之后Hook并執(zhí)行
after():returning(返回值類型) 方法執(zhí)行完正常返回處Hook并執(zhí)行
after():throwing(異常類型) 方法異常退出點Hook并執(zhí)行
around() around作用是取代原Join Points
  • Aspect: Pointcut 和 Advice的組合可看做切面Aspect,這是一個抽象的概念,理解即可。例如,我們在應(yīng)用中通過定義一個 Pointcut 和給定恰當(dāng)?shù)腁dvice,添加一個日志切面,將該切面切入原有的業(yè)務(wù)代碼中。
  • Weaving: 注入代碼(Advices)到目標位置(Join points)的過程,由編譯器ajc完成。

引入AOP的栗子

在項目中引入AOP的方式共有三種,分別是原生AspectJ語言,在Java代碼中使用AspectJ,結(jié)合自定義注解的方式在Java代碼中使用AspectJ??紤]到易用性和非侵入性,本栗子的方式是在Java代碼中使用AspectJ,在不修改業(yè)務(wù)代碼的前提下,實現(xiàn)onCreate方法性能的監(jiān)控。

首先考慮未引入AOP的情形,我們?nèi)绾瓮瓿蓪I(yè)務(wù)代碼的性能監(jiān)控。

public class MainActivity extends AppCompatActivity
        implements NavigationView.OnNavigationItemSelectedListener {
    private static final String TAG = MainActivity.class.getSimpleName();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        TimeWatcher watcher = new TimeWatcher();
        watcher.start();

        initView();

        watcher.stop();
        Log.i(TAG, "onCreate execute with " + watcher.getTotalTime() + "ms");
    }

很容易可以發(fā)現(xiàn),業(yè)務(wù)代碼和性能監(jiān)控的功能性代碼交織在一起,結(jié)構(gòu)不清晰,耦合度較高。而且更關(guān)鍵的是,每當(dāng)一處業(yè)務(wù)代碼需要做性能統(tǒng)計,就需要把統(tǒng)計代碼重新復(fù)制一份。后期如果需要修改性能統(tǒng)計代碼的邏輯,比如修改統(tǒng)計日志內(nèi)容、增加日志上傳功能,需要找到每一處引用到的代碼進行修改,這樣無疑增加了工作量和出錯的概率。

引入AOP可以很好地剝離項目中業(yè)務(wù)代碼和性能統(tǒng)計代碼。首先創(chuàng)建aspect Module,引入AspectJ的相關(guān)類庫,并構(gòu)建主類和輔助類,項目結(jié)構(gòu)如圖3所示。

圖3 項目結(jié)構(gòu)

在aspect這個module下新建一個Java類。

@Aspect
public class TimeWatchAspect {
    private static final String POINTCUT_TIME_WATCH =
            "execution(* com.netease.aopdemo.MainActivity.onCreate(..))";

    @Pointcut(POINTCUT_TIME_WATCH)
    public void timeWatch() {}

    @Around("timeWatch()")
    public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature ms = (MethodSignature) joinPoint.getSignature();
        String methodName = ms.getName();
        String className = ms.getDeclaringType().getSimpleName();

        TimeWatcher watcher = new TimeWatcher();
        watcher.start();
        Object result = joinPoint.proceed();
        watcher.stop();

        Log.i(className, methodName + " execute with " + watcher.getTotalTime() + "ms");

        return result;
    }
}

打上@Aspect注解,則該類可以被ajc編譯器識別為一個Asepct,在工程項目編譯時便能非常方便地實現(xiàn)代碼織入。圖5中可以看到AspectJ的三個要素,Join Point、Advice和Aspect。好像少了Join Point?Join Point早已定義在Pointcut的字符串常量中,即MainActivity的onCreate方法。Pointcut以注解的形式定義,注解了timeWatch方法,從而timeWatch就是這個Pointcut的名稱,注解參數(shù)則使用定義好的字符串常量,作為Join Point的過濾規(guī)則。同樣,Advice也是將類型關(guān)鍵字(此處為Around)注解在特定的方法weaveJoinPoint之上,注解的參數(shù)為具名的Pointcut,即timeWatch。上文提到Around類型即用該方法替換原Join Point的實現(xiàn),圖5中Object result = joinPoint.proceed()等價于原有的被Hook方法,即MainActivity的onCreate()。在該語句的前后,是性能統(tǒng)計的代碼片段。

public class MainActivity extends AppCompatActivity
        implements NavigationView.OnNavigationItemSelectedListener {
    private static final String TAG = MainActivity.class.getSimpleName();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initView();
    }

再看一眼我們重構(gòu)后的業(yè)務(wù)代碼,僅需要專注于業(yè)務(wù)邏輯處理,是不是變得非常簡潔。

編譯并運行項目代碼后,結(jié)果如圖4所示。

圖4 AOP運行結(jié)果

那么ajc編譯器在字節(jié)碼層面到底做了什么呢?

  protected void onCreate(Bundle paramBundle)
  {
    JoinPoint localJoinPoint = Factory.makeJP(ajc$tjp_0, this, this, paramBundle);
    TimeWatchAspect.aspectOf().weaveJoinPoint(new MainActivity.AjcClosure1(new Object[] { this, paramBundle, localJoinPoint }).linkClosureAndJoinPoint(69648));
  }


  public class MainActivity$AjcClosure1 extends AroundClosure
{
  public MainActivity$AjcClosure1(Object[] paramArrayOfObject)
  {
    super(paramArrayOfObject);
  }

  public Object run(Object[] paramArrayOfObject)
  {
    Object[] arrayOfObject = this.state;
    MainActivity.onCreate_aroundBody0((MainActivity)arrayOfObject[0], (Bundle)arrayOfObject[1], (JoinPoint)arrayOfObject[2]);
    return null;
  }
}

反編譯項目生成的apk后可以看到,ajc在Join Point處織入了代碼,用TimeWatchAspect.aspectOf().weaveJoinPoint()實現(xiàn)了替換。

踩過的坑

正所謂人不能兩次踏進同一條河流,我踩過的坑希望大家可以避免。
由于AspectJ在字節(jié)碼層面將功能性代碼織入業(yè)務(wù)代碼中,源碼層面無法看到變化,且無法在功能性代碼中進行斷點調(diào)試。所以一旦出錯,調(diào)試成本相對較高。如果項目運行結(jié)果與預(yù)期不符,首先檢查編譯問題,能否正常實現(xiàn)代碼織入(可以看apk中的class文件樹結(jié)構(gòu)),再檢查Join Point、Pointcut和Advice是否符合AspectJ語法,Hook是否正確。

如果Android Studio中的Instant Run開啟,則在編譯時可能會影響代碼的正??椚耄越ㄗh關(guān)閉Instant Run。

另外,一般初級階段會選擇日志打印的方式驗證AspectJ接入的可行性。如果測試機是魅族系列手機,則注意把項目中Log等級提升到D以上,或者在手機的開發(fā)者選項中選擇顯示所有等級的日志,否則默認情況下你看不到D及D以下等級日志的輸出(慘痛的教訓(xùn),浪費了兩天時間排查問題)。

總結(jié)

本文主要介紹了Android AOP的編程思想和應(yīng)用場景,AOP這種編程范式已經(jīng)在服務(wù)端取得了成功,我相信在Android客戶端編程中,也一樣可以有用武之地。通過AspectJ這種工具可以很方便地實現(xiàn)AOP編程,在文中重點介紹了AspectJ的語法和使用方式,并以實例的方式展示了如何通過AspectJ重構(gòu)代碼,降低了耦合度并利于后期維護。最后介紹了我自己踩過的坑,避免重復(fù)入坑。AOP是一種編程思想,理解并運用到工程中將會大有裨益,AspectJ同樣是非常強大的工具,文中只介紹了一些基本用法,還有很多高級特性待探索和實踐。只要腦洞夠大,一定能實現(xiàn)一些意想不到的效果。最后,希望我的文章能給大家?guī)硪恍﹩l(fā),在今后的開發(fā)過程中,可以用AOP來做一些exciting的事情。

參考文章

  1. http://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android/
  2. https://blog.egorand.me/going-aspect-oriented-with-aspectj-and-google-analytics/
  3. http://blog.csdn.net/innost/article/details/49387395

Demo源碼

https://github.com/Master-Neo/AopDemo

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

相關(guān)閱讀更多精彩內(nèi)容

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