前言
在上篇文章Android編譯期插樁,讓程式自己寫程式碼(一)的前言部分我放了一張圖,用來說明編譯期插樁的位置和相應的技術。這裡,我還打算這張圖來開篇。
AspectJ
在上圖中,我們可以清楚的看到AspectJ的插樁位置是.java與.class之間。這很容易使人聯想到編譯器。事實上,AspectJ就是一種編譯器,它在Java編譯器的基礎上增加了關鍵字識別和編譯方法。因此,AspectJ可以編譯Java程式碼。它還提供了Aspect程式。在編譯期間,將開發者編寫的Aspect程式織入到目標程式中,擴充套件目標程式的功能。
AspectJ可以應用於Android和後端開發中。在後端,AspectJ 應用更為廣泛一些,著名的Spring框架就對AspectJ提供了支援。不過,近些年,AspectJ技術在Android領域也開始嶄露頭角,比較知名的有JakeWharton的hugo。另外,一些企業也開始探索AspectJ在埋點、許可權管理等方面的應用。
關於AspectJ更為詳細的介紹,請大家移步鄧平凡大神的部落格深入理解Android之AOP。這篇文章對於初次接觸AspectJ的人來說十分友好,筆者最初就是通過它進入AspectJ殿堂的。珠玉在前,本文就不再介紹AspectJ的基礎知識了。那本文要說些什麼呢?
- 一個簡單的Hugo框架。
- 從位元組碼分析AspectJ。
Hugo
Hugo是JakeWharton基於AspectJ開源的一個除錯框架,其功能是通過註解的方式可以列印出方法的執行時間,方便開發者效能調優。今天,我們就來看一下它的廬山真面目。
配置AspectJ
Hugo是基於AspectJ的,那首先我們就要支援AspectJ。這裡向大家推薦滬江的AspectJX,它不僅使用簡單,而且還支援過濾一些aar或jar包。
首先我們需要在根build.gradle中依賴AspectJX
dependencies {
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
}
複製程式碼
在app專案的build.gradle裡應用外掛,並新增aspectj的依賴庫
apply plugin: 'android-aspectjx'
api 'org.aspectj:aspectjrt:1.8.9'
複製程式碼
這樣就配置完成了,是不是很簡單啊。
注意:筆者這裡採用的gradle版本是3.0.1,如果沒有編譯通過,檢查一下gradle版本。
定義DebugLog註解
十分簡單,直接上程式碼
@Target({METHOD, CONSTRUCTOR})
@Retention(CLASS)
public @interface DebugLog {
}
複製程式碼
編寫Aspect
@Aspect
public class Hugo {
@Pointcut("execution(@com.hugo.example.lib.DebugLog * *(..))")
public void method() {}
@Pointcut("execution(@com.hugo.example.lib.DebugLog *.new(..))")
public void constructor() {}
@Around("method() || constructor()")
public Object logAndExecute(ProceedingJoinPoint joinPoint) throws Throwable {
CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();
Class<?> cls = codeSignature.getDeclaringType();
String methodName = codeSignature.getName();
long startNanos = System.nanoTime();
Object result = joinPoint.proceed();
long stopNanos = System.nanoTime();
long lengthMillis = TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos);
StringBuilder builder = new StringBuilder();
builder.append("methodName:")
.append(methodName)
.append(" ---- executeTime:")
.append(lengthMillis);
Log.e(asTag(cls), builder.toString());
return result;
}
private static String asTag(Class<?> cls) {
if (cls.isAnonymousClass()) {
return asTag(cls.getEnclosingClass());
}
return cls.getSimpleName();
}
}
複製程式碼
如果你學習了深入理解Android之AOP,那麼這段程式碼應該很容易能看懂。這裡我簡單解釋一下。
- 選取註解了DebugLog的method和constructor作為pointcut。
- 在原方法執行前織入開始時間,在原方法執行後織入結束時間,並計算出執行時間。通過Log.v列印出來。
到此,這個簡化版的Hugo基本就介紹完了。你可以做個Demo試一下了。
- 筆者DebugLog的包名是com.hugo.example.lib。因此在method()和constractor()中的註解內容是com.hugo.example.lib.DebugLog。
- 其實,真正的Hugo框架核心也只有一個類。它只是在日誌列印時,輸出了更多的方法資訊。本文為了方便讀者理解作了簡化。
測試
我們定義一個Test類,然後在Activity啟動的時候呼叫myThread()方法。Test類如下:
public class Test {
@DebugLog
public void myMethod1() throws Exception{
Thread.sleep(1000);
}
}
複製程式碼
我們看一下日誌,方法的執行時間被完美的列印出來了。
從位元組碼分析AspectJ
我們仍然以Test為例,看一下Test反編之後的位元組碼。
反編譯的內容看起來不太方便,我在這裡把它轉換成了如下程式碼:
為了觀看方便,上圖將程式碼分為4部分。
我們先看第一部分,這是一個靜態程式碼塊,也就是說在類載入的時候,程式會AspectJ提供的Factory類,建立一個型別為JoinPoint.StaticPart靜態例項STATIC_PART。深入理解Android之AOP中對JoinPoint.StaticPart介紹如下:
thisJoinPointStaticPart物件:在advice程式碼中可直接使用,代表JPoint中那些不變的東西。比如這個JPoint的型別,JPoint所處的程式碼位置等。這裡thisJoinPointStaticPart就是程式碼中的JoinPoint.StaticPart。
第二部分是我們之前定義的myThread方法,它在編譯期間被替換了。在執行時,它首先通過Factory的靜態方法makeJP建立一個JoinPoint物件。makeJp是一個過載方法,我們看一下。
public static JoinPoint makeJP(JoinPoint.StaticPart staticPart, Object _this, Object target) {
return new JoinPointImpl(staticPart, _this, target, NO_ARGS);
}
public static JoinPoint makeJP(JoinPoint.StaticPart staticPart, Object _this, Object target, Object[] args) {
return new JoinPointImpl(staticPart, _this, target, args);
}
複製程式碼
通過我列出來了兩個,可以看到JoinPoint除了包含了我們第一步中提到了STATIC_PART物件,還包括了this,target物件,以及方法引數。這和深入理解Android之AOP中對thisJoinpoint描述也是一致的:
thisJoinpoint物件:在advice程式碼中可直接使用。代表JPoint每次被觸發時的一些動態資訊,比如引數啊之類的。
建立完JoinPoint物件後,隨後呼叫了第三部分中的advice()方法。advice()方法大部分都是我們在Hugo中編寫的織入程式碼,這裡只有一個不同,那就是joinPoint.proceed()不見了,替換成了原始碼中具體的處理邏輯。
總結
通過上述分析,我們可以清楚的感知到AspectJ提供了非常強大的功能。但同時,由於其為每個切入點生成一個JoinPoint.StaticPar靜態例項和在執行過程中生成的JoinPoint以及一些其它的封裝,這必然會導致程式在記憶體和處理速度等方面受影響。因此,在小範圍內使用AspectJ是可以的,但是如果涉及範圍較大就要慎重考慮了。