Android AOP實現原理全解析
前天早晨在公交車上,知乎搜尋了下Android的最新技術,回答還是很多的,我們搞技術的,永遠不能落後,要隨時與市場保持同步,這樣才能跟上市場的步伐。有朋友提到了一個AOP的面向切面的程式設計技術,從這個名字上,大概就可以知道是幹什麼的,也有很多朋友舉例就是在日誌列印、許可權檢查等比較散的地方,使用AOP可以實現統一管理,還是非常方便的。百度一下,也有好多大神寫的關於AOP的詳細介紹:
在AndroidStudio工具中開發的話,還需要編譯build.gradle角本,詳細情況可參考如下:
我們本部落格的重點是瞭解清楚AOP的整個實現流程,是直接使用的別人的程式碼,程式碼也是在別人的部落格中直接下載的,地址如下:
Activity就一個,執行時就直接呼叫TestMain.TestAll()日誌列印。我們先來看一下作者分module的用意。整個project分為aoplib、app、buildsrc、libinlib、testlib五個module,各module的意思應該也比較清楚,aoplib就是實現AOP功能的模組,app是本專案的啟動模組,buildsrc是用來構建專案的模組,libinlib中作者只提供了一個TestLog類,而且只有一個方法,目的是用來測試AOP功能的,最後一個testlib是作者實現自己意圖的模組,所有的測試類都是在這裡的。我們要分析的重點就是aoplib和testlib這兩個module了。
我們從程式的執行過程來一步步分析,首先看一下MainActivity類的onCreate方法,請注意,作者為這個方法上加了一個@DebugLog註解,@DebugLog註解是自己實現的,實現程式碼如下,非常簡單:
從這個註釋介面的定義上,我們可以看到,它的目標是使用在TYPE(介面、類、列舉、註解),METHOD(方法),CONSTRUCTOR(建構函式)三種類型上的,當前就是使用在MainActivity的onCreate方法上的。我們可以來看一下使用Aspect編譯後的MainActivity的class檔案:package com.example.aoplib; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.CLASS; @Target({TYPE, METHOD, CONSTRUCTOR}) @Retention(CLASS) public @interface DebugLog { }
從這裡大家可以非常清楚的看到,MainActivity的onCreate方法已經被替換了,它是按照Aspect編譯規則生成的,我們可以再來看看其他帶有@DebugLog註解的類或者方法對應生成的class檔案,比如TestMethodClass類的class檔案,整個檔案程式碼如下:
public class TestMethodClass {
public TestMethodClass() {
(new Thread() {
@DebugLog
public void run() {
JoinPoint var2 = Factory.makeJP(ajc$tjp_0, this, this);
Hugo var10000 = Hugo.aspectOf();
Object[] var3 = new Object[]{this, var2};
var10000.logAndExecute((new TestMethodClass$1$AjcClosure1(var3)).linkClosureAndJoinPoint(69648));
}
static {
ajc$preClinit();
}
}).start();
}
@DebugLog
public void spendTime1ms() {
JoinPoint var2 = Factory.makeJP(ajc$tjp_0, this, this);
Hugo var10000 = Hugo.aspectOf();
Object[] var3 = new Object[]{this, var2};
var10000.logAndExecute((new TestMethodClass$AjcClosure1(var3)).linkClosureAndJoinPoint(69648));
}
@DebugLog
public static void spendTime2ms() {
JoinPoint var1 = Factory.makeJP(ajc$tjp_1, (Object)null, (Object)null);
Hugo var10000 = Hugo.aspectOf();
Object[] var2 = new Object[]{var1};
var10000.logAndExecute((new TestMethodClass$AjcClosure3(var2)).linkClosureAndJoinPoint(65536));
}
@DebugLog
public final void spendTime3ms() {
JoinPoint var2 = Factory.makeJP(ajc$tjp_2, this, this);
Hugo var10000 = Hugo.aspectOf();
Object[] var3 = new Object[]{this, var2};
var10000.logAndExecute((new TestMethodClass$AjcClosure5(var3)).linkClosureAndJoinPoint(69648));
}
static {
ajc$preClinit();
}
}
在這個class檔案中,我們可以看到,run()、spendTime1ms()、spendTime2ms()、spendTime3ms()幾個方法全部都是這樣樣式,每個方法一共四句,第一句,構建一個切點JoinPoint,第二句呼叫Hugo.aspectOf()獲取我們自己定義的Aspect處理類,第三句構造引數陣列Object[],第四句呼叫當前類的相應方法。看到這裡我們基本就明白AOP的原理了,它就是利用我們自己實現的一個註解,將所有的切點集中在一個地方處理的,這樣,就可以把多個切點放在一起統一處理了,非常的方便!
下面我們就來分析一個方法的執行過程,此專案中其他方法的實現是完全一樣的,我們就以TestMethodClass類的spendTime1ms()為例來展開我們的分析,在編譯完的class檔案中,首先構造一個JoinPoint切點,Factory.makeJP()方法的實現就是使用傳入的引數直接構造一個JoinPointImpl物件,第二句就是獲取當前的Aspect處理類物件Hugo,此類必須要帶有@Aspect註解,第三句就是構造方法執行的陣列物件,第四句執行Aspect處理類的入口方法logAndExecute,Hugo類的完整程式碼如下:
@Aspect
public class Hugo {
@Pointcut("within(@com.example.aoplib.DebugLog *)")
public void withinAnnotatedClass() {}
@Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()")
public void methodInsideAnnotatedType() {}
@Pointcut("execution(!synthetic *.new(..)) && withinAnnotatedClass()")
public void constructorInsideAnnotatedType() {}
@Pointcut("execution(@com.example.aoplib.DebugLog * *(..)) || methodInsideAnnotatedType()")
public void method() {}
@Pointcut("execution(@com.example.aoplib.DebugLog *.new(..)) || constructorInsideAnnotatedType()")
public void constructor() {}
@Around("method() || constructor()")
public Object logAndExecute(ProceedingJoinPoint joinPoint) throws Throwable {
enterMethod(joinPoint);
long startNanos = System.nanoTime();
Object result = joinPoint.proceed();
long stopNanos = System.nanoTime();
long lengthMillis = TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos);
exitMethod(joinPoint, result, lengthMillis);
return result;
}
private static void enterMethod(JoinPoint joinPoint) {
CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();
Class<?> cls = codeSignature.getDeclaringType();
String methodName = codeSignature.getName();
String[] parameterNames = codeSignature.getParameterNames();
Object[] parameterValues = joinPoint.getArgs();
StringBuilder builder = new StringBuilder("\u21E2 ");
builder.append(methodName).append('(');
for (int i = 0; i < parameterValues.length; i++) {
if (i > 0) {
builder.append(", ");
}
builder.append(parameterNames[i]).append('=');
builder.append(Strings.toString(parameterValues[i]));
}
builder.append(')');
if (Looper.myLooper() != Looper.getMainLooper()) {
builder.append(" [Thread:\"").append(Thread.currentThread().getName()).append("\"]");
}
Log.v(asTag(cls), builder.toString());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
final String section = builder.toString().substring(2);
Trace.beginSection(section);
}
}
private static void exitMethod(JoinPoint joinPoint, Object result, long lengthMillis) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
Trace.endSection();
}
Signature signature = joinPoint.getSignature();
Class<?> cls = signature.getDeclaringType();
String methodName = signature.getName();
boolean hasReturnType = signature instanceof MethodSignature
&& ((MethodSignature) signature).getReturnType() != void.class;
StringBuilder builder = new StringBuilder("\u21E0 ")
.append(methodName)
.append(" [")
.append(lengthMillis)
.append("ms]");
if (hasReturnType) {
builder.append(" = ");
builder.append(Strings.toString(result));
}
Log.v(asTag(cls), builder.toString());
}
private static String asTag(Class<?> cls) {
if (cls.isAnonymousClass()) {
return asTag(cls.getEnclosingClass());
}
return cls.getSimpleName();
}
}
可以看到,在logAndExecute方法的處理中就是列印了日誌而已,當然,我們也可以根據我們自己需求實現不同的功能,比如許可權檢查,如果許可權OK,就正常執行,如果不OK,則直接在這裡丟擲異常。
好了,AOP的完整過程我們已經瞭解了,可以看出,整個執行過程還是比較簡單的,就是使用了一個註解把我們的目標切點集中到一起進行處理,最後我們來總結一下,如果我們自己的專案要實現AOP的切面程式設計,應該要有幾步:
1:我們要使用Aspect,肯定就需要有相應的jar包了,jar包的資源非常多,大家可以在網上隨便下載。
2:要定義我們的切點,相當於本例中的DebugLog註解,它的@Target可以根據自己的需求來實現。
3:定義自己的Aspect處理類,此類必須帶有@Aspect註解,需要在它裡面定義切點函式,定義切點的處理函式,也就是本例中Hugo類的logAndExecute方法了,Hugo類中的其他兩個方法enterMethod、exitMethod只是為了實現日誌列印的目的而寫的,大家可以根據自己的需求具體實現。
4:完成了上面兩步,我們的專案中的Aspect框架就算搭建好了,下面就是新增切點了,也就是我們要在哪裡切入的問題,可以通過新增我們自定義的註釋來實現。
5:我們的所有功能都完成了,最後還一定要注意編譯的方式要使用Aspect編譯器,如果大家使用javac編譯器的話,那生成的class檔案中根本沒有Aspect的程式碼,肯定也就無法實現我們的意圖了。