天天看點

看AspectJ在Android中的強勢插入

AOP是Aspect Oriented Programming的縮寫,即『面向切面程式設計』。它和我們平時接觸到的OOP都是程式設計的不同思想,OOP,即『面向對象程式設計』,它提倡的是将功能子產品化,對象化,而AOP的思想,則不太一樣,它提倡的是針對同一類問題的統一處理,當然,我們在實際程式設計過程中,不可能單純的安裝AOP或者OOP的思想來程式設計,很多時候,可能會混合多種程式設計思想,大家也不必要糾結該使用哪種思想,取百家之長,才是正道。

那麼AOP這種程式設計思想有什麼用呢,一般來說,主要用于不想侵入原有代碼的場景中,例如SDK需要無侵入的在宿主中插入一些代碼,做日志埋點、性能監控、動态權限控制、甚至是代碼調試等等。

AspectJ實際上是對AOP程式設計思想的一個實踐,當然,除了AspectJ以外,還有很多其它的AOP實作,例如ASMDex,但目前最好、最友善的,依然是AspectJ。

AOP的用處非常廣,從Spring到Android,各個地方都有使用,特别是在後端,Spring中已經使用的非常友善了,而且功能非常強大,但是在Android中,AspectJ的實作是略閹割的版本,并不是所有功能都支援,但對于一般的用戶端開發來說,已經完全足夠用了。

在Android上內建AspectJ實際上是比較複雜的,不是一句話就能compile,但是,鄙司已經給大家把這個問題解決了,大家現在直接使用這個SDK就可以很友善的在Android Studio中使用AspectJ了。Github位址如下:

<a href="https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx">https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx</a>

另外一個比較成功的使用AOP的庫是Jake大神的Hugo:

<a href="https://github.com/JakeWharton/hugo">https://github.com/JakeWharton/hugo</a>

首先,需要在項目根目錄的build.gradle中增加依賴:

完整代碼如下:

然後再主項目或者庫的build.gradle中增加AspectJ的依賴:

同時在build.gradle中加入AspectJX子產品:

這樣就把整個Android Studio中的AspectJ的環境配置完畢了,如果在編譯的時候,遇到一些『can’t determine superclass of missing type xxxxx』這樣的錯誤,請參考項目README中關于excludeJarFilter的使用。

我們通過一段簡單的代碼來了解下基本的使用方法和功能,建立一個AspectTest類檔案,代碼如下:

在類的最開始,我們使用@Aspect注解來定義這樣一個AspectJ檔案,編譯器在編譯的時候,就會自動去解析,并不需要主動去調用AspectJ類裡面的代碼。

我的原始代碼很簡單:

通過這種方式編譯後,我們來看下生成的代碼是怎樣的。AspectJ的原理實際上是在編譯的時候,根據一定的規則解析,然後插入一些代碼,通過aspectjx生成的代碼,會在Build目錄下:

看AspectJ在Android中的強勢插入

通過反編譯工具檢視下生成内容:

看AspectJ在Android中的強勢插入

我們可以發現,在onCreate的最前面,插入了一行AspectJ的代碼。這個就是AspectJ的主要功能,抛開AOP的思想來說,我們想做的,實際上就是『在不侵入原有代碼的基礎上,增加新的代碼』。

Join Points,簡稱JPoints,是AspectJ的核心思想之一,它就像一把刀,把程式的整個執行過程切成了一段段不同的部分。例如,構造方法調用、調用方法、方法執行、異常等等,這些都是Join Points,實際上,也就是你想把新的代碼插在程式的哪個地方,是插在構造方法中,還是插在某個方法調用前,或者是插在某個方法中,這個地方就是Join Points,當然,不是所有地方都能給你插的,隻有能插的地方,才叫Join Points。

Join Points和Pointcuts的差別實際上很難說,我也不敢說我了解的一定對,但這些都是概念上的内容,并不影響我們去使用。

Pointcuts,在我了解,實際上就是在Join Points中通過一定條件選擇出我們所需要的Join Points,是以說,Pointcuts,也就是帶條件的Join Points,作為我們需要的代碼切入點。

又來一個Advice,Advice其實是最好了解的,也就是我們具體插入的代碼,以及如何插入這些代碼。我們最開始舉的那個例子,裡面就是使用的最簡單的Advice——Before。類似的還有After、Around,我們後面來講講他們的差別。

我們以前面的Demo來看下最簡單的AspectJ文法:

這裡會分成幾個部分,我們依次來看:

@Before:Advice,也就是具體的插入點

execution:處理Join Point的類型,例如call、execution

(* android.app.Activity.on**(..)):這個是最重要的表達式,第一個『*』表示傳回值,『*』表示傳回值為任意類型,後面這個就是典型的包名路徑,其中可以包含『*』來進行通配,幾個『*』沒差別。同時,這裡可以通過『&amp;&amp;、||、!』來進行條件組合。()代表這個方法的參數,你可以指定類型,例如android.os.Bundle,或者(..)這樣來代表任意類型、任意個數的參數。

public void onActivityMethodBefore:實際切入的代碼。

這裡還有一些比對規則,可以作為示例來進行講解:

表達式

含義

java.lang.String

比對String類型

java.*.String

比對java包下的任何“一級子包”下的String類型,如比對java.lang.String,但不比對java.lang.ss.String

java..*

比對java包及任何子包下的任何類型,如比對java.lang.String、java.lang.annotation.Annotation

java.lang.*ing

比對任何java.lang包下的以ing結尾的類型

java.lang.Number+

比對java.lang包下的任何Number的自類型,如比對java.lang.Integer,也比對java.math.BigInteger

參數

()

表示方法沒有任何參數

(..)

表示比對接受任意個參數的方法

(..,java.lang.String)

表示比對接受java.lang.String類型的參數結束,且其前邊可以接受有任意個參數的方法

(java.lang.String,..)

表示比對接受java.lang.String類型的參數開始,且其後邊可以接受任意個參數的方法

(*,java.lang.String)

表示比對接受java.lang.String類型的參數結束,且其前邊接受有一個任意類型參數的方法

這兩個Advice應該是使用的最多的,是以,我們先來看下這兩個Advice的執行個體,首先看下Before和After。

經過上面的文法解釋,現在看這個應該很好了解了,我們來看下編譯後的類:

看AspectJ在Android中的強勢插入

我們可以看見,在原始代碼的基礎上,增加了Before和After的代碼,Log也能被正确的插入并列印出來。

Before和After其實還是很好了解的,也就是在Pointcuts之前和之後,插入代碼,那麼Around呢,從字面含義上來講,也就是在方法前後各插入代碼,是的,他包含了Before和After的全部功能,代碼如下:

其中,proceedingJoinPoint.proceed()代表執行原始的方法,在這之前、之後,都可以進行各種邏輯處理。

原始代碼:

我們先來看下編譯後的代碼:

看AspectJ在Android中的強勢插入

我們可以發現,Around确實實作了Before和After的功能,但是要注意的是,Around和After是不能同時作用在同一個方法上的,會産生重複切入的問題。

自定義Pointcuts可以讓我們更加精确的切入一個或多個指定的切入點。

首先,我們需要自定義一個注解類,例如——DebugTool.java:

然後在需要插入代碼的地方使用這個注解:

最後,我們來建立自己的切入檔案。

先定義Pointcut,并申明要監控的方法名,最後,在Before或者其它Advice裡面添加切入代碼,即可完成切入。

編譯好的代碼如下:

看AspectJ在Android中的強勢插入

通過這種方式,我們可以非常友善的監控指定的Pointcut,進而增加監控的粒度。

在AspectJ的切入點表達式中,我們前面都是使用的execution,實際上,還有一種類型——call,那麼這兩種文法有什麼差別呢,我們來試驗下就知道了。

被切代碼依然很簡單:

先來看execution,代碼如下:

編譯之後的代碼如下所示:

看AspectJ在Android中的強勢插入

再來看下call,代碼如下:

看AspectJ在Android中的強勢插入

其實對照起來看就一目了然了,execution是在被切入的方法中,call是在調用被切入的方法前或者後。

對于Call來說:

對于Execution來說:

除了前面提到的call和execution,比較常用的還有一個withincode。這個文法通常來進行一些切入點條件的過濾,作更加精确的切入控制。我們可以參考下面這個例子:

testAOP1()和testAOP2()都調用了testAOP()方法,但是,現在想在testAOP2()方法調用testAOP()方法的時候,才切入代碼,那麼這個時候,就需要使用到Pointcut和withincode組合的方式,來精确定位切入點。

我們再來看下編譯後的代碼:

看AspectJ在Android中的強勢插入

我們可以看見,隻有在testAOP2()方法中被插入了代碼,這就做到了精确條件的插入。

AfterThrowing是一個比較少見的Advice,他用于處理程式中未處理的異常,記住,這點很重要,是未處理的異常,具體原因,我們等會看反編譯出來的代碼就知道了。我們随手寫一個異常,代碼如下:

然後使用AfterThrowing來進行AOP代碼的編寫:

這段代碼很簡單,同樣是使用我們前面類似的表達式,但是這裡是為了處理異常,是以,使用了*.*來進行通配,在異常中,我們執行一行日志,編譯好的代碼如下:

看AspectJ在Android中的強勢插入

我們可以看見com.xys.aspectjxdemo包下的所有方法都被加上了try catch,同時,在catch中,被插入了我們切入的代碼,但是最後,他依然會throw e,也就是說,這個異常已經會被抛出去,崩潰依舊是會發生的。同時,如果你的原始代碼中已經try catch了,那麼同樣也無法處理,具體原因,我們看一個反編譯的代碼:

看AspectJ在Android中的強勢插入

可以看見,實際上,原始代碼的catch中,又被套了一層try catch,是以,e.printStackTrace()被try catch,也就不會再有異常發生了,也就無法切入了。

目前鄙司的很多項目都已經使用了這套AOP方案,例如基于AOP的動态權限管理、基于AOP的業務資料埋點、基于AOP的性能監測系統等等。

現在已經開源了一部分基于AOP的動态權限管理的源碼,但由于需要剝離業務代碼,是以後面會更加完善這功能代碼,大家可以繼續關注,github位址如下所示:

<a href="https://github.com/firefly1126/android_permission_aspectjx">https://github.com/firefly1126/android_permission_aspectjx</a>

其它的AOP項目陸續開源中,大家可以持續關注~

歡迎關注我的微信公衆号

看AspectJ在Android中的強勢插入

繼續閱讀