AOP是Aspect Oriented Programming的縮寫,即『面向切面編程』。它和咱們平時接觸到的OOP都是編程的不一樣思想,OOP,即『面向對象編程』,它提倡的是將功能模塊化,對象化,而AOP的思想,則不太同樣,它提倡的是針對同一類問題的統一處理,固然,咱們在實際編程過程當中,不可能單純的安裝AOP或者OOP的思想來編程,不少時候,可能會混合多種編程思想,你們也沒必要要糾結該使用哪一種思想,取百家之長,纔是正道。java
那麼AOP這種編程思想有什麼用呢,通常來講,主要用於不想侵入原有代碼的場景中,例如SDK須要無侵入的在宿主中插入一些代碼,作日誌埋點、性能監控、動態權限控制、甚至是代碼調試等等。android
AspectJ其實是對AOP編程思想的一個實踐,固然,除了AspectJ之外,還有不少其它的AOP實現,例如ASMDex,但目前最好、最方便的,依然是AspectJ。git
AOP的用處很是廣,從Spring到Android,各個地方都有使用,特別是在後端,Spring中已經使用的很是方便了,並且功能很是強大,可是在Android中,AspectJ的實現是略閹割的版本,並非全部功能都支持,但對於通常的客戶端開發來講,已經徹底足夠用了。github
在Android上集成AspectJ其實是比較複雜的,不是一句話就能compile,可是,鄙司已經給你們把這個問題解決了,你們如今直接使用這個SDK就能夠很方便的在Android Studio中使用AspectJ了。Github地址以下:編程
https://github.com/HujiangTec...後端
另一個比較成功的使用AOP的庫是Jake大神的Hugo:微信
https://github.com/JakeWharto...app
首先,須要在項目根目錄的build.gradle中增長依賴:ide
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8'
完整代碼以下:模塊化
buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.3.0-beta2' classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } }
而後再主項目或者庫的build.gradle中增長AspectJ的依賴:
compile 'org.aspectj:aspectjrt:1.8.9'
同時在build.gradle中加入AspectJX模塊:
apply plugin: 'android-aspectjx'
這樣就把整個Android Studio中的AspectJ的環境配置完畢了,若是在編譯的時候,遇到一些『can't determine superclass of missing type xxxxx』這樣的錯誤,請參考項目README中關於excludeJarFilter的使用。
aspectjx { //includes the libs that you want to weave includeJarFilter 'universal-image-loader', 'AspectJX-Demo/library' //excludes the libs that you don't want to weave excludeJarFilter 'universal-image-loader' }
咱們經過一段簡單的代碼來了解下基本的使用方法和功能,新建一個AspectTest類文件,代碼以下:
@Aspect public class AspectTest { private static final String TAG = "xuyisheng"; @Before("execution(* android.app.Activity.on**(..))") public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable { String key = joinPoint.getSignature().toString(); Log.d(TAG, "onActivityMethodBefore: " + key); } }
在類的最開始,咱們使用@Aspect註解來定義這樣一個AspectJ文件,編譯器在編譯的時候,就會自動去解析,並不須要主動去調用AspectJ類裏面的代碼。
個人原始代碼很簡單:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }
經過這種方式編譯後,咱們來看下生成的代碼是怎樣的。AspectJ的原理其實是在編譯的時候,根據必定的規則解析,而後插入一些代碼,經過aspectjx生成的代碼,會在Build目錄下:
經過反編譯工具查看下生成內容:
咱們能夠發現,在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("execution(* android.app.Activity.on**(..))") public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable { }
這裏會分紅幾個部分,咱們依次來看:
@Before:Advice,也就是具體的插入點
execution:處理Join Point的類型,例如call、execution
( android.app.Activity.on*(..)):這個是最重要的表達式,第一個『*』表示返回值,『*』表示返回值爲任意類型,後面這個就是典型的包名路徑,其中能夠包含『*』來進行通配,幾個『*』沒區別。同時,這裏能夠經過『&&、||、!』來進行條件組合。()表明這個方法的參數,你能夠指定類型,例如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。
@Before("execution(* com.xys.aspectjxdemo.MainActivity.on*(android.os.Bundle))") public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable { String key = joinPoint.getSignature().toString(); Log.d(TAG, "onActivityMethodBefore: " + key); } @After("execution(* com.xys.aspectjxdemo.MainActivity.on*(android.os.Bundle))") public void onActivityMethodAfter(JoinPoint joinPoint) throws Throwable { String key = joinPoint.getSignature().toString(); Log.d(TAG, "onActivityMethodAfter: " + key); }
通過上面的語法解釋,如今看這個應該很好理解了,咱們來看下編譯後的類:
咱們能夠看見,在原始代碼的基礎上,增長了Before和After的代碼,Log也能被正確的插入並打印出來。
Before和After其實仍是很好理解的,也就是在Pointcuts以前和以後,插入代碼,那麼Around呢,從字面含義上來說,也就是在方法先後各插入代碼,是的,他包含了Before和After的所有功能,代碼以下:
@Around("execution(* com.xys.aspectjxdemo.MainActivity.testAOP())") public void onActivityMethodAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { String key = proceedingJoinPoint.getSignature().toString(); Log.d(TAG, "onActivityMethodAroundFirst: " + key); proceedingJoinPoint.proceed(); Log.d(TAG, "onActivityMethodAroundSecond: " + key); }
其中,proceedingJoinPoint.proceed()表明執行原始的方法,在這以前、以後,均可以進行各類邏輯處理。
原始代碼:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); testAOP(); } public void testAOP() { Log.d("xuyisheng", "testAOP"); } }
咱們先來看下編譯後的代碼:
咱們能夠發現,Around確實實現了Before和After的功能,可是要注意的是,Around和After是不能同時做用在同一個方法上的,會產生重複切入的問題。
自定義Pointcuts可讓咱們更加精確的切入一個或多個指定的切入點。
首先,咱們須要自定義一個註解類,例如——DebugTool.java:
/** * 自定義AOP註解 * <p> * Created by xuyisheng on 17/1/12. */ @Retention(RetentionPolicy.CLASS) @Target({ElementType.CONSTRUCTOR, ElementType.METHOD}) public @interface DebugTool { }
而後在須要插入代碼的地方使用這個註解:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); testAOP(); } @DebugTool public void testAOP() { Log.d("xuyisheng", "testAOP"); } }
最後,咱們來建立本身的切入文件。
@Pointcut("execution(@com.xys.aspectjxdemo.DebugTool * *(..))") public void DebugToolMethod() { } @Before("DebugToolMethod()") public void onDebugToolMethodBefore(JoinPoint joinPoint) throws Throwable { String key = joinPoint.getSignature().toString(); Log.d(TAG, "onDebugToolMethodBefore: " + key); }
先定義Pointcut,並申明要監控的方法名,最後,在Before或者其它Advice裏面添加切入代碼,便可完成切入。
編譯好的代碼以下:
經過這種方式,咱們能夠很是方便的監控指定的Pointcut,從而增長監控的粒度。
在AspectJ的切入點表達式中,咱們前面都是使用的execution,實際上,還有一種類型——call,那麼這兩種語法有什麼區別呢,咱們來試驗下就知道了。
被切代碼依然很簡單:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); testAOP(); } public void testAOP() { Log.d("xuyisheng", "testAOP"); } }
先來看execution,代碼以下:
@Before("execution(* com.xys.aspectjxdemo.MainActivity.testAOP(..))") public void methodAOPTest(JoinPoint joinPoint) throws Throwable { String key = joinPoint.getSignature().toString(); Log.d(TAG, "methodAOPTest: " + key); }
編譯以後的代碼以下所示:
再來看下call,代碼以下:
@Before("call(* com.xys.aspectjxdemo.MainActivity.testAOP(..))") public void methodAOPTest(JoinPoint joinPoint) throws Throwable { String key = joinPoint.getSignature().toString(); Log.d(TAG, "methodAOPTest: " + key); }
編譯以後的代碼以下所示:
其實對照起來看就一目瞭然了,execution是在被切入的方法中,call是在調用被切入的方法前或者後。
對於Call來講:
Call(Before) Pointcut{ Pointcut Method } Call(After)
對於Execution來講:
Pointcut{ execution(Before) Pointcut Method execution(After) }
除了前面提到的call和execution,比較經常使用的還有一個withincode。這個語法一般來進行一些切入點條件的過濾,做更加精確的切入控制。咱們能夠參考下面這個例子:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); testAOP1(); testAOP2(); } public void testAOP() { Log.d("xuyisheng", "testAOP"); } public void testAOP1() { testAOP(); } public void testAOP2() { testAOP(); } }
testAOP1()和testAOP2()都調用了testAOP()方法,可是,如今想在testAOP2()方法調用testAOP()方法的時候,才切入代碼,那麼這個時候,就須要使用到Pointcut和withincode組合的方式,來精肯定位切入點。
// 在testAOP2()方法內 @Pointcut("withincode(* com.xys.aspectjxdemo.MainActivity.testAOP2(..))") public void invokeAOP2() { } // 調用testAOP()方法的時候 @Pointcut("call(* com.xys.aspectjxdemo.MainActivity.testAOP(..))") public void invokeAOP() { } // 同時知足前面的條件,即在testAOP2()方法內調用testAOP()方法的時候才切入 @Pointcut("invokeAOP() && invokeAOP2()") public void invokeAOPOnlyInAOP2() { } @Before("invokeAOPOnlyInAOP2()") public void beforeInvokeAOPOnlyInAOP2(JoinPoint joinPoint) { String key = joinPoint.getSignature().toString(); Log.d(TAG, "onDebugToolMethodBefore: " + key); }
咱們再來看下編譯後的代碼:
咱們能夠看見,只有在testAOP2()方法中被插入了代碼,這就作到了精確條件的插入。
AfterThrowing是一個比較少見的Advice,他用於處理程序中未處理的異常,記住,這點很重要,是未處理的異常,具體緣由,咱們等會看反編譯出來的代碼就知道了。咱們隨手寫一個異常,代碼以下:
public void testAOP() { View view = null; view.animate(); }
而後使用AfterThrowing來進行AOP代碼的編寫:
@AfterThrowing(pointcut = "execution(* com.xys.aspectjxdemo.*.*(..))", throwing = "exception") public void catchExceptionMethod(Exception exception) { String message = exception.toString(); Log.d(TAG, "catchExceptionMethod: " + message); }
這段代碼很簡單,一樣是使用咱們前面相似的表達式,可是這裏是爲了處理異常,因此,使用了*.*來進行通配,在異常中,咱們執行一行日誌,編譯好的代碼以下:
咱們能夠看見com.xys.aspectjxdemo包下的全部方法都被加上了try catch,同時,在catch中,被插入了咱們切入的代碼,可是最後,他依然會throw e,也就是說,這個異常已經會被拋出去,崩潰依舊是會發生的。同時,若是你的原始代碼中已經try catch了,那麼一樣也沒法處理,具體緣由,咱們看一個反編譯的代碼:
能夠看見,實際上,原始代碼的catch中,又被套了一層try catch,因此,e.printStackTrace()被try catch,也就不會再有異常發生了,也就沒法切入了。
目前鄙司的不少項目都已經使用了這套AOP方案,例如基於AOP的動態權限管理、基於AOP的業務數據埋點、基於AOP的性能監測系統等等。
如今已經開源了一部分基於AOP的動態權限管理的源碼,但因爲須要剝離業務代碼,因此後面會更加完善這功能代碼,你們能夠繼續關注,github地址以下所示:
https://github.com/firefly112...
其它的AOP項目陸續開源中,你們能夠持續關注~
歡迎關注個人微信公衆號