1 前言
1.1 什么是AOP,與OOP的區別
- OOP:即ObjectOriented Programming,面向對象編程。功能都被劃分到一個一個的模塊里邊,每個模塊專心干自己的事情,模塊之間通過設計好的接口交互。
- AOP:即Aspect Oriented Programming,面向切面編程。通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。
OOP | AOP | |
---|---|---|
面向目標 | 面向名詞領域 | 面向動詞領域 |
思想結構 | 縱向結構 | 橫向結構 |
注重方面 | 注重業務邏輯單元的劃分 | 偏重業務處理過程的某個步驟或階段 |
下圖:有三個模塊:登陸、轉賬、大文件上傳,現在需要加入性能檢測功能,統計這三個模塊每個方法耗時多少,OOP思想做法是設計一個性能檢測模塊,提供接口供這三個模塊調用。這樣每個模塊都要調用性能檢測模塊的接口,如果接口有改動,需要在這三個模塊中每次調用的地方修改,這樣做的弊端有:代碼冗余,邏輯不清晰,重構不方便,違背單一原則。運用AOP的思想做法是:在這些獨立的模塊間,在特定的切入點進行hook,將共同的邏輯添加到模塊中而不影響原有模塊的獨立性。如下圖OOP實現轉AOP實現,在不同的模塊中加入性能檢測功能,并不影響原有的架構。
1.2 Android AOP主流框架
名稱 | 描述 |
---|---|
Xposed | ROOT社區著名開源項目,需要root權限(運行時) |
Dexposed | 阿里AOP框架,改造Xposed,只支持Android2.3 - 4.4(運行時) |
APT | 注解處理器,通過注解生成源代碼,代表框架:DataBinding,Dagger2, ButterKnife, EventBus3 、DBFlow、AndroidAnnotation |
AspectJ | AspectJ定義了AOP語法,所以它有一個專門的編譯器用來生成遵守Java字節編碼規范的Class文件,在編譯期注入代碼。代表框架:Hugo(Jake Wharton) |
Javassist、ASM | 執行字節碼操作的庫。它可以在一個已經編譯好的類中添加新的方法,或者是修改已有的方法,可以繞過編譯,直接操作字節碼,從而實現代碼注入。代表框架:熱修復框架HotFix 、InstantRun |
2 AspectJ
2.1 環境配置
aspectJ常規配置
在app/build.gradle下配置:
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}
variants.all { variant ->
if (!variant.buildType.isDebuggable()) {
log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
return;
}
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.8",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)
MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler);
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break;
case IMessage.WARNING:
log.warn message.message, message.thrown
break;
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
}
}
dependencies {
compile files('libs/aspectjrt.jar') //將aspectjrt.jar包拷貝至app/libs目錄下
}
aspectjx插件配置
在根build.gradle下配置:
dependencies {
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.10'
}
在app/build.gradle下配置:
apply plugin: 'android-aspectjx'
dependencies {
compile 'org.aspectj:aspectjrt:1.8.9'
}
aspectjx默認會遍歷項目編譯后所有的.class文件和依賴的第三方庫去查找符合織入條件的切點,為了提升編譯效率,可以加入過濾條件指定遍歷某些庫或者不遍歷某些庫。
aspectjx {
//織入遍歷符合條件的庫
includeJarFilter 'universal-image-loader', 'AspectJX-Demo/library'
//排除包含‘universal-image-loader’的庫
excludeJarFilter 'universal-image-loader'
}
配置注意:
- 黑名單必須放在app/build.gralde下才生效
- app下的代碼會自動加入白名單
- gradle插件版本目前不支持3.0以上
- 只支持AspectJ annotation的方式
- 建議配置白名單,否則編譯時會遇到沖突
兩種配置區別
AspectJ常規配置不支持AAR或者JAR切入的,只會對編譯的代碼進行織入,AspectJX插件配置支持AAR, JAR及Kotlin的應用。這里需要注意的,在AspectJ常規配置中有這樣的代碼:"-inpath", javaCompile.destinationDir.toString()
,代表只對源文件進行織入。在查看Aspectjx源碼時,發現在“-inputs”配置加入了.jar文件,使得class類可以被織入代碼。這么理解來看,AspectJ也是支持對class文件的織入的,只是需要對它進行相關的配置,而配置比較繁瑣,所以誕生了AspectJx等插件。
2.2 語法
通常AspectJ需要編寫aj文件,然后把AOP代碼放到aj后綴名文件中,如下:
public pointcut testAll(): call(public * *.println(..)) && !within(TestAspect) ;
在Android開發中,建議不要使用aj文件。因為aj文件只有AspectJ編譯器才認識,而Android編譯器不認識這種文件。所以當更新了aj文件后,編譯器認為源碼沒有發生變化,不會編譯它。所以AspectJ提供了一種基于注解的方法,如下:
@Pointcut(“call(public * *.println(..)) && !within(TestAspect)")//方法切入點
public void testAll() { }
Join Points
Join Points 就是程序運行時的一些執行點,例如:我要打印所有Activity的onCreate方法,onCreate方法被調用就是一個Join Points。除了方法被調用,還有很多,例如方法內部“讀、寫”變量,異常處理等,如下表:
Join Point | 說明 | Pointcuts語法 |
---|---|---|
Method call | 方法被調用 | call(MethodPattern) |
Method execution | 方法執行 | execution(MethodPattern) |
Constructor call | 構造函數被調用 | call(ConstructorPattern) |
Constructor execution | 構造函數執行 | execution(ConstructorPattern) |
Field get | 讀取屬性 | get(FieldPattern) |
Field set | 寫入屬性 | set(FieldPattern) |
Pre-initialization | 與構造函數有關,很少用到 | preinitialization(ConstructorPattern) |
Initialization | 與構造函數有關,很少用到 | initialization(ConstructorPattern) |
Static initialization | static 塊初始化 | staticinitialization(TypePattern) |
Handler | 異常處理 | handler(TypePattern) |
Advice execution | 所有 Advice 執行 | adviceexcution() |
Pointcuts
上表中,同一個函數,還分為call類型和execution類型的JPoint,如何選擇自己想要的JPoint呢,這就是Pointcuts的功能:提供一種方法使得開發者能夠選擇自己感興趣的JoinPoints。例如:我要打印所有Activity的onCreate方法,Pointcuts需要篩選的就是所有Activity的onCreate方法,而不是任意類的onCreate方法。除了上表與Join Point 對應的選擇外,Pointcuts 還有其他選擇方法:
Pointcuts 語法 | 說明 | 示例 |
---|---|---|
within(TypePattern) | TypePattern標示package或者類 TypePatter可以使用通配符 | 表示某個package或者類中的所有JPoint。比如within(Test):Test類中所有JPoint |
withincode(Constructor Signature/Method Signature) | 表示某個構造函數或其他函數執行過程中涉及到的JPoint | 比如 withinCode(* TestDerived.testMethod(..)) 表示testMethod涉及的JPoint。withinCode( *.Test.new(..))表示Test構造函數涉及的JPoint |
cflow(pointcuts) | cflow是call flow的意思,cflow的條件是一個pointcut | 比如cflow(call TestDerived.testMethod):表示調用TestDerived.testMethod函數時所包含的JPoint,包括testMethod的call這個JPoint本身 |
cflowbelow(Pointcut) | cflow是call flow的意思 | 比如cflowblow(call TestDerived.testMethod):表示調用TestDerived.testMethod函數時所包含的JPoint,不包括testMethod的call這個JPoint本身 |
this(Type) | Join Point 所屬的 this 對象是否 instanceOf Type 或者 Id 的類型 | JPoint是代碼段(不論是函數,異常處理,static block),從語法上說,它都屬于一個類。如果這個類的類型是Type標示的類型,則和它相關的JPoint將全部被選中。圖2示例的testMethod是TestDerived類。所以this(TestDerived)將會選中這個testMethod JPoint |
target(Type) | JPoint的target對象是Type類型 | 和this相對的是target。不過target一般用在call的情況。call一個函數,這個函數可能定義在其他類。比如testMethod是TestDerived類定義的。那么target(TestDerived)就會搜索到調用testMethod的地方。但是不包括testMethod的execution JPoint |
args(TypeSignature) | 用來對JPoint的參數進行條件搜索的 | 比如args(int,..),表示第一個參數是int,后面參數個數和類型不限的JPoint。 |
Pointcut 表達式還可以 !、&&、|| 來組合,語義和java一樣。上面 Pointcuts 的語法中涉及到一些 Pattern,下面是這些 Pattern 的規則,[]里的內容是可選的:
Pattern | 規則 |
---|---|
MethodPattern | [!] [@Annotation] [public,protected,private] [static] [final] 返回值類型 [類名.]方法名(參數類型列表) [throws 異常類型] |
ConstructorPattern | [!] [@Annotation] [public,protected,private] [final] [類名.]new(參數類型列表) [throws 異常類型] |
FieldPattern | [!] [@Annotation] [public,protected,private] [static] [final] 屬性類型 [類名.]屬性名 |
TypePattern | 其他 Pattern 涉及到的類型規則也是一樣,可以使用 ‘!’、’‘、’..’、’+’,’!’ 表示取反,’‘ 匹配除 . 外的所有字符串,’*’ 單獨使用事表示匹配任意類型,’..’ 匹配任意字符串,’..’ 單獨使用時表示匹配任意長度任意類型,’+’ 匹配其自身及子類,還有一個 ‘…’表示不定個數。也可以使用 &&、|| 操作符 |
下面主要介紹下上表中的MethodPattern。
MethodPattern對應的一個完整的表達式為:@注解 訪問權限 返回值的類型 包名.函數名(參數)
- @注解和訪問權限(public/private/protect,以及static/final)屬于可選項。如果不設置它們,則默認都會選擇。以訪問權限為例,如果沒有設置訪問權限作為條件,那么public,private,protect及static、final的函數都會進行搜索。
- 返回值類型就是普通的函數的返回值類型。如果不限定類型的話,就用*通配符表示
- 包名.函數名用于查找匹配的函數。可以使用通配符,包括和..以及+號。其中號用于匹配除.號之外的任意字符,而..則表示任意子package,+號表示子類。 比如:
1) java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date
2) Test*:可以表示TestBase,也可以表示TestDervied
3) java..*:表示java任意子類
4) java..*Model+:表示Java任意package中名字以Model結尾的子類,比如TabelModel,TreeModel 等
- 最后來看函數的參數。參數匹配比較簡單,主要是參數類型,比如:
1) (int, char):表示參數只有兩個,并且第一個參數類型是int,第二個參數類型是char
2) (String, ..):表示至少有一個參數。并且第一個參數類型是String,后面參數類型不限.
3) ..代表任意參數個數和類型
4) (Object ...):表示不定個數的參數,且類型都是Object,這里的...不是通配符,而是Java中代表不定參數的意思
Pointcuts 示例
以下示例表示在aspectjx插件下,相同包是指同一個aar/jar包,AspectJ常規配置下不同包不能執行“execution”織入
execution
-
execution(* com.howtodoinjava.EmployeeManager.*( .. ))
匹配EmployeeManger接口中所有的方法 -
execution(* EmployeeManager.*( .. ))
當切面方法和EmployeeManager接口在相同的包下時,匹配EmployeeManger接口中所有的方法 -
execution(public * EmployeeManager.*(..))
當切面方法和EmployeeManager接口在相同的包下時,匹配EmployeeManager接口的所有public方法 -
execution(public EmployeeDTO EmployeeManager.*(..))
匹配EmployeeManager接口中權限為public并返回類型為EmployeeDTO的所有方法。 -
execution(public EmployeeDTO EmployeeManager.*(EmployeeDTO, ..))
匹配EmployeeManager接口中權限為public并返回類型為EmployeeDTO,第一個參數為EmployeeDTO類型的所有方法。 -
execution(public EmployeeDTO EmployeeManager.*(EmployeeDTO, Integer))
匹配EmployeeManager接口中權限為public、返回類型為EmployeeDTO,參數明確定義為EmployeeDTO,Integer的所有方法。 -
"execution(@com.xyz.service.BehaviorTrace * *(..))"
匹配注解為"@com.xyz.service.BehaviorTrace",返回值為任意類型,任意包名下的任意方法。
within
任意連接點:包括類/對象初始化塊,field,方法,構造器
-
within(com.xyz.service.*)
com.xyz.service包下任意連接點 -
within(com.xyz.service..*)
com.xyz.service包或子包下任意連接點 -
within(TestAspect)
TestAspect類下的任意連接點 -
within(@com.xyz.service.BehavioClass *)
持有com.xyz.service.BehavioClass注解的任意連接點
withincode
假設方法functionA, functionB都調用了dummy,但只想在functionB調用dummy時織入代碼。
public void functionA() { dummy() }
public void functionB() { dummy() }
public void dummy() {} // 只在functionB調用的時候織入代碼
@Aspect // 加上@Aspect注解表示此類會被aspectj編譯器編譯,相關的Pointcut才會被織入
public class MethodTracer {
// withincode: 在functionB方法內
@Pointcut("withincode(void org.sdet.aspectj.MainActivity.functionB(..))")
public void invokeFunctionB() {}
// call: 調用dummy方法
@Pointcut("call(void org.sdet.aspectj.MainActivity.dummy(..))")
public void invokeDummy() {}
// 在functionB內調用dummy方法
@Pointcut("invokeDummy() && invokeFunctionB()")
public void invokeDummyInsideFunctionB() {}
// 在functionB方法內,調用dummy方法之前invoke下面代碼(目前僅打印xxx)
@Before("invokeDummyInsideFunctionB()")
public void beforeInvokeDummyInsideFunctionB(JoinPoint joinPoint) {
System.out.printf("Before.InvokeDummyInsideFunctionB.advice() called on '%s'", joinPoint);
}
}
Advice
之前介紹的是如何找到切點,現在介紹的Advice就是告訴我們如何切,換個說法就是告訴我們要插入的代碼以何種方式插入,比如說有以下幾種:
名稱 | 描述 |
---|---|
Before | 在方法執行之前執行要插入的代碼 |
After | 在方法執行之后執行要插入的代碼 |
AfterReturning | 在方法執行后,返回一個結果再執行,如果沒結果,用此修辭符修辭是不會執行的 |
AfterThrowing | 在方法執行過程中拋出異常后執行,也就是方法執行過程中,如果拋出異常后,才會執行此切面方法。 |
Around | 在方法執行前后和拋出異常時執行(前面幾種通知的綜合) |
Before、After示例
Before和After原理和用法一樣,只是一個在方法前插入代碼,一個在方法后面插入代碼,在此只介紹Before。例如:在"com.luyao.aop.aspectj.AspectJActivity"執行onCreate里的代碼之前打印"hello world"
package com.luyao.aop.aspectj;
public class AspectJActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
@Before("execution(* com.luyao.aop.aspectj.AspectJActivity.on*(android.os.Bundle))")
public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
Log.e("luy", "hello world");
}}
查看反編譯后的代碼:
AfterReturning示例
在"com.luyao.aop.aspectj.AspectJActivity"執行getHeight()方法返回高度后打印這個高度值。
package com.luyao.aop.aspectj;
public class AspectJActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getHeight();
}
public int getHeight() {
return 1280;
}
}
@AfterReturning(pointcut = "call(* com.luyao.aop.aspectj.AspectJActivity.getHeight())", returning = "height")
public void getHeight(int height) { // height必須和上面"height"一樣
Log.e("luy", "height:" + height);
}
反編譯后的代碼:
AfterThrowing示例
如果我們經常需要收集拋出異常的方法信息,可以使用@AfterThrowing。比如我們要在任意類的任意方法拋出異常時,打印這個異常信息:
public class AspectJActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
divideZero();
}
public void divideZero() {
int i = 2 / 0;
}
}
@AfterThrowing(pointcut = "call(* *..*(..))", throwing = "throwable") // "throwable"必須和下面參數名稱一樣
public void anyFuncThrows(Throwable throwable) {
Log.e("luy", "throwable--->" + throwable); // throwable--->java.lang.ArithmeticException: divide by zero
}
反編譯后的代碼:
注意點:
- @AfterThrowing 不支持 Field -> get & set,一般用在 Method 和 Constructor
- 捕獲的是拋出異常的方法,即使這個方法的調用方已經處理了此異常。上面例子中即使divideZero()調用了try catch, 也能被anyFuncThrows織入。
Around 示例
例如我想在"com.luyao.aop.aspectj.AspectJActivity "執行setContentView方法前后打印當前系統時間:
package com.luyao.aop.aspectj;
public class AspectJActivity extends Activity {
private static final String TAG = "luyao";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_aspect_j);
}
}
@Around("call(* com.luyao.aop.aspectj.AspectJActivity.setContentView(..))")
public void invokeSetContentView(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
Log.e("luy", "執行setContentView方法前:" + System.currentTimeMillis());
proceedingJoinPoint.proceed();
Log.e("luy", "執行setContentView方法后:" + System.currentTimeMillis());
}
反編譯后的代碼:
2.3 簡單使用案例
需求:如圖,假設有2個功能分別是"朋友圈"和"搖一搖",功能很簡單,點擊按鈕觸發睡眠和打印日志。統計這2個功能的耗時。
思路:一般思路是在調用這2個方法之前后分別獲取當前系統的時間戳,然后相減得到耗時,關鍵代碼如下:
// 搖一搖點擊事件處理
public void shake(View view) {
long begin = SystemClock.currentThreadTimeMillis();
Log.i(TAG, "進入搖一搖方法體");
SystemClock.sleep(3000);
long end = SystemClock.currentThreadTimeMillis();
Log.i(TAG, "耗時:" + (end - begin));
}
// 朋友圈點擊事件處理
public void friend(View view) {
long begin = System.currentThreadTimeMillis();
Log.i(TAG, "進入朋友圈方法體");
SystemClock.sleep(2000);
long end = System.currentThreadTimeMillis();
Log.i(TAG, "耗時:" + (end - begin));
}
上面這種處理方法對于功能點少還好處理,如果很多方法都需要統計,每個方法都這樣寫無疑加了很大的工作量,導致代碼閱讀邏輯不清晰,重構不方便,違背單一原則。如果使用 AspectJ,可以通過一行注解,解決所有需要統計耗時的方法。具體代碼如下:
編寫布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.luyao.aop.aspectj.AspectJActivity"
android:orientation="vertical">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="shake"
android:text="搖一搖"
android:textSize="20sp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="friend"
android:textSize="20sp"
android:text="朋友圈"/>
</LinearLayout>
定義注解
@Target(ElementType.METHOD) // 修飾的是方法
@Retention(RetentionPolicy.CLASS) // 編譯時注解
public @interface BehaviorTrace {
String value(); // 功能點名稱
int type(); // 唯一確定功能點的值
}
通過定義@BehaviorTrace 來給"搖一搖"和"朋友圈"方法添加注解
編寫 Aspect
@Aspect // 此處一定要定義,否則不會該類不會參與編譯
public class BehaviorAspect {
@Pointcut("execution(@com.luyao.aop.aspectj.BehaviorTrace * *(..))") // 定義切點
public void annoBehavior() {
}
@Around("annoBehavior()") // 定義怎么切,也可以這么寫 @Around("execution(@com.luyao.aop.aspectj.BehaviorTrace * *(..))")
public void dealPoint(ProceedingJoinPoint point) throws Throwable {
//方法執行前
MethodSignature methodSignature = (MethodSignature) point.getSignature();
BehaviorTrace behaviorTrace = methodSignature.getMethod().getAnnotation(BehaviorTrace.class); // 拿到注解
long begin = System.currentTimeMillis();
Log.i("luy", "拿到需要切的方法啦,執行前");
point.proceed(); // 執行被切的方法
//方法執行完成
long end = System.currentTimeMillis();
Log.i("luy", behaviorTrace.value() + "(" + behaviorTrace.type() + ")" + " 耗時:" + (end - begin) + "ms");
}
}
使用@BehaviorAspect
public class AspectJActivity extends Activity {
private static final String TAG = "luy";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_aspect_j);
}
@BehaviorTrace(value = "搖一搖", type = 1)
public void shake(View view) {
Log.i(TAG, "進入搖一搖方法體");
SystemClock.sleep(3000);
}
@BehaviorTrace(value = "朋友圈", type = 2)
public void friend(View view) {
Log.i(TAG, "進入朋友圈方法體");
SystemClock.sleep(2000);
}
}
編寫完畢,接下來測試,點擊搖一搖打印日志:
拿到需要切的方法啦,執行前
進入搖一搖方法體
搖一搖(1) 耗時:3000ms
點擊朋友圈打印日志:
拿到需要切的方法啦,執行前
進入朋友圈方法體
朋友圈(2) 耗時:2000ms
寫在最后
本文介紹了AOP的思想、AOP的幾種工具和AspectJ的基本用法。在實際開發項目中,當有需求時,了解AOP可以多一種思維方式去解決問題。同時,AspectJ織入代碼會增加編譯時間,使用時也需要考慮。