看AspectJ在Android中的強勢插入
什麼是AOP
AOP是Aspect Oriented Programming的縮寫,即『面向切面程式設計』。它和我們平時接觸到的OOP都是程式設計的不同思想,OOP,即『物件導向程式設計』,它提倡的是將功能模組化,物件化,而AOP的思想,則不太一樣,它提倡的是針對同一類問題的統一處理,當然,我們在實際程式設計過程中,不可能單純的安裝AOP或者OOP的思想來程式設計,很多時候,可能會混合多種程式設計思想,大家也不必要糾結該使用哪種思想,取百家之長,才是正道。
那麼AOP這種程式設計思想有什麼用呢,一般來說,主要用於不想侵入原有程式碼的場景中,例如SDK需要無侵入的在宿主中插入一些程式碼,做日誌埋點、效能監控、動態許可權控制、甚至是程式碼除錯等等。
AspectJ
AspectJ實際上是對AOP程式設計思想的一個實踐,當然,除了AspectJ以外,還有很多其它的AOP實現,例如ASMDex,但目前最好、最方便的,依然是AspectJ。
在Android專案中使用AspectJ
AOP的用處非常廣,從Spring到Android,各個地方都有使用,特別是在後端,Spring中已經使用的非常方便了,而且功能非常強大,但是在Android中,AspectJ的實現是略閹割的版本,並不是所有功能都支援,但對於一般的客戶端開發來說,已經完全足夠用了。
在Android上整合AspectJ實際上是比較複雜的,不是一句話就能compile,但是,鄙司已經給大家把這個問題解決了,大家現在直接使用這個SDK就可以很方便的在Android Studio中使用AspectJ了。Github地址如下:
另外一個比較成功的使用AOP的庫是Jake大神的Hugo:
接入說明
首先,需要在專案根目錄的build.gradle中增加依賴:
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'
}複製程式碼
AspectJ入門
我們通過一段簡單的程式碼來了解下基本的使用方法和功能,新建一個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的思想來說,我們想做的,實際上就是『在不侵入原有程式碼的基礎上,增加新的程式碼』。
AspectJ之Join Points
Join Points,簡稱JPoints,是AspectJ的核心思想之一,它就像一把刀,把程式的整個執行過程切成了一段段不同的部分。例如,構造方法呼叫、呼叫方法、方法執行、異常等等,這些都是Join Points,實際上,也就是你想把新的程式碼插在程式的哪個地方,是插在構造方法中,還是插在某個方法呼叫前,或者是插在某個方法中,這個地方就是Join Points,當然,不是所有地方都能給你插的,只有能插的地方,才叫Join Points。
AspectJ之Pointcuts
Join Points和Pointcuts的區別實際上很難說,我也不敢說我理解的一定對,但這些都是概念上的內容,並不影響我們去使用。
Pointcuts,在我理解,實際上就是在Join Points中通過一定條件選擇出我們所需要的Join Points,所以說,Pointcuts,也就是帶條件的Join Points,作為我們需要的程式碼切入點。
AspectJ之Advice
又來一個Advice,Advice其實是最好理解的,也就是我們具體插入的程式碼,以及如何插入這些程式碼。我們最開始舉的那個例子,裡面就是使用的最簡單的Advice——Before。類似的還有After、Around,我們後面來講講他們的區別。
AspectJ之切點語法
我們以前面的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型別的引數結束,且其前邊接受有一個任意型別引數的方法 |
AspectJ例項
Before、After
這兩個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也能被正確的插入並列印出來。
Around
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
自定義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,從而增加監控的粒度。
call和execution
在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)
}複製程式碼
切入點過濾與withincode
除了前面提到的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
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,也就不會再有異常發生了,也就無法切入了。
AspectJX使用案例
目前鄙司的很多專案都已經使用了這套AOP方案,例如基於AOP的動態許可權管理、基於AOP的業務資料埋點、基於AOP的效能監測系統等等。
現在已經開源了一部分基於AOP的動態許可權管理的原始碼,但由於需要剝離業務程式碼,所以後面會更加完善這功能程式碼,大家可以繼續關注,github地址如下所示:
其它的AOP專案陸續開源中,大家可以持續關注~
歡迎關注我的微信公眾號