以前幾篇文章咱們詳細介紹了AOP的幾種技術方案,因爲AOP技術複雜多樣,實際需求也不盡相同,那麼咱們應該如何作技術選型呢?html
本篇將會對現有的AOP技術作一個統一的介紹,尤爲側重在Android方向的落地,但願對你有所幫助,文中內容、示例大都來自工做總結,若有偏頗不妥,歡迎指正。java
這裏先統一一下基本名詞,以便表述。android
AOP是一種面向切面編程的技術的統稱,AOP框架最終都會圍繞class字節碼的操做展開,不管是對字節碼的操做增刪改,爲方便描述,咱們統稱爲代碼的織入。git
雖然AOP翻譯過來叫面向切面編程,但在實際使用過程當中,切面可能退化成了一個點,好比咱們想統計app的冷啓動時間,這就很是具體了。若是咱們用AOP的技術實現統計全部函數的耗時時間,天然能統計到相似啓動這個階段的時間。github
從狹義來看實現AOP技術的框架必須是能將切面編程抽象成上層能夠直接使用的工具或API,但當咱們將切面降維後,最終面向的就是切點而已。換句話說,只要能將代碼織入到某個點那這種技術就必定能夠實現AOP,這樣AOP技術所涵蓋的領域就得以拓展,由於從狹義的角度看目前只有AspectJ符合這個標準。web
從廣義上來說,AOP技術能夠是任何能實現代碼織入的技術或框架,對代碼的改動最終都會體如今字節碼上,而這類技術也能夠叫作字節碼加強,通用名詞理解便可。編程
下面咱們將介紹一些經常使用的AOP技術。設計模式
首先,從織入的時機的角度看,能夠分爲源碼階段、class階段、dex階段、運行時織入。bash
對於前三項源碼階段、class階段、dex織入,因爲他們都發生在class加載到虛擬機前,咱們統稱爲靜態織入, 而在運行階段發生的改動,咱們統稱爲動態織入。微信
常見的技術框架以下表:
織入時機 | 技術框架 |
---|---|
靜態織入 | APT,AspectJ、ASM、Javassit |
動態織入 | java動態代理,cglib、Javassit |
靜態織入發生在編譯器,所以幾乎不會對運行時的效率產生影響;動態織入發生在運行期,可直接將字節碼寫入內存,並經過反射完成類的加載,因此效率相對較低,但更靈活。
動態織入的前提是類還未被加載,你不能將一個已經加載的類通過修改再次加載,這是ClassLoader的限制。可是能夠經過另外一個ClassLoader進行加載,虛擬機容許兩個相同類名的class被不一樣的ClassLoader加載,在運行時也會被認爲是兩個不一樣的類,所以須要注意不能相互賦值, 否則會拋出ClassCastException。
java動態代理、cglib只會建立新的代理類而不是對原有類的字節碼直接修改,Javassit可修改原有字節碼。
其實利用反射或者hook技術一樣能夠實現代碼行爲的改變,但因爲這類技術並無真正的改變原有的字節碼,因此暫不在談論範圍內,好比xposed,dexposed。
其次,咱們須要關注這些框架具有哪切面編程的能力,這有助於幫助我作技術選型,因爲AspectJ、ASM 、Javassit是相對比較完善的AOP框架,所以只對三者進行比較。
能力 | AspectJ | ASM | Javassit |
---|---|---|---|
切面抽象 | ✓ | ||
切點抽象 | ✓ | ||
通知類型抽象 | ✓ | ✓ | ✓ |
其中:
切面抽象:具有篩選過濾class的能力,好比咱們想爲Activity的全部生命週期織入代碼,那你是否是首先須要具有過濾Activity及其子類的能力。
切點抽象:具體到某個class,是否具有方法、字段、註解訪問的能力。
通知類型抽象:是否直接支持在方法前、後、中直接織入代碼。
固然不具有能力不表明不能作AOP編程,能夠經過其餘方法解決,只是易用性的問題。
下面咱們將開始對上述框架逐一介紹,Let' go~~~
APT(Annotation Processing Tool)即註解處理器,在Gradle 版本>=2.2後被annotationProcessor取代。
它用來在編譯時掃描和處理註解,掃描過程可以使用 auto-service 來簡化尋找註解的配置,在處理過程當中可生成java文件(建立java文件一般依賴 javapoet 這個庫)。經常使用於生成一些模板代碼或運行時依賴的類文件,好比常見的ButterKnife、Dagger、ARouter,它的優勢是簡單方便。
以ButterKnife爲例:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.toolbar)
Toolbar toolbar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
}
複製代碼
一句簡單的ButterKnife.bind(this)
是如何實現控件的賦值的?
事實上 @Bind 註解在編譯期會生成一個MainActivity_ViewBinding類,而ButterKnife.bind(this) 此次調用最終會經過反射建立出MainActivity_ViewBinding對象,並把activity的引用傳遞給它。
# ButterKnife
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
...
//建立xxx_binding對象並把activity傳入
return constructor.newInstance(target, source);
}
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
...
try {
//運行時經過反射加載在編譯階段生成的類
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
}
...
return bindingCtor;
}
複製代碼
這樣最終在MainActivity_ViewBinding的構造函數中完成控件的賦值。
public class MainActivity_ViewBinding<T extends MainActivity> implements Unbinder {
protected T target;
public MainActivity_ViewBinding(final T target, Finder finder, Object source) {
...
//爲控件賦值 其中優化了控件的查找
target.toolbar = finder.findRequiredViewAsType(source, R.id.toolbar, "field 'toolbar'", Toolbar.class);
...
}
}
複製代碼
爲了在此類中能訪問到MainActivity中聲明的屬性,爲此ButterKnife框架要求,使用@Bind註解聲明的屬性不能是private的。
能夠看到ButterKnife中仍然用到了反射,這是爲了統一API使用 ButterKnife.bind(this) 做出的犧牲,而Dagger則會經過Component,Module的名字經過動態生成不一樣的方法名,所以使用以前須要對工程進行build。
之因此會這樣,是由於APT技術的不足,一般只是用來建立新的類,而不能對原有類進行改動,在不能改動的狀況下,只能經過反射實現動態化。
AspectJ是一種嚴格意義上的AOP技術,由於它提供了完整的面向切面編程的註解,這樣讓使用者能夠在不關心字節碼原理的狀況下完成代碼的織入,由於編寫的切面代碼就是要織入的實際代碼。
AspectJ實現代碼織入有兩種方式,一是自行編寫.ajc文件,二是使用AspectJ提供的@Aspect、@Pointcut等註解,兩者最終都是經過ajc編譯器完成代碼的織入。
舉個簡單的例子,假設咱們想統計全部view的點擊事件,使用AspectJ只須要寫一個類便可。
@Aspect
public class MethodAspect {
private static final String TAG = "MethodAspect5";
//切面表達式,聲明須要過濾的類和方法
@Pointcut("execution(* android.view.View.OnClickListener+.onClick(..))")
public void callMethod() {
}
//before表示在方法調用前織入
@before("callMethod()")
public void beforeMethodCall(ProceedingJoinPoint joinPoint) {
//編寫業務代碼
}
}
複製代碼
註解簡明直觀,上手難度近乎爲0。
經常使用的函數耗時統計工具Hugo,就是AspectJ的一個實際應用,Android平臺Hujiang開源的AspectJX插件靈感也來自於Hugo,詳情見舊文Android 函數耗時統計工具之Hugo。
AspectJ雖然好用,但也存在一些嚴重的問題。
AspectJ切面表達式支持繼承語法,雖然方便了開發,但存在致命的問題,就是在繼承樹上的類可能都會織入代碼,這在多數業務場景下是不適用的,好比無埋點。
另外使用java8語法編寫的代碼,不會被進入切面範圍,也就沒法織入代碼。
更多詳情參見舊文 Android AspectJ詳解 。
ASM是很是底層的面向字節碼編程的AOP框架,理論上能夠實現任何關於字節碼的修改,很是硬核。許多字節碼生成API底層都是用ASM實現,常見好比Groovy、cglib,所以在Android平臺下使用ASM無需添加額外的依賴。完整的學習ASM必須瞭解字節碼和JVM相關知識。
好比要織入一句簡單的日誌輸出
Log.d("tag", " onCreate");
複製代碼
使用ASM編寫是下面這個樣子,沒錯由於JVM是基於棧的,函數的調用須要參數先入棧,而後執行函數入棧,最後出棧,總共四條JVM指令。
mv.visitLdcInsn("tag");
mv.visitLdcInsn("onCreate");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);
複製代碼
能夠看出ASM與AspectJ有很大的不一樣,AspectJ織入的代碼就是實際編寫的代碼,但ASM必須使用其提供的API編寫指令。一行java代碼可能對應多行ASM API代碼,由於一行java代碼背後可能隱藏這多個JVM指令。
你沒必要擔憂不會編寫ASM代碼,官方提供了ASM Bytecode Outline插件能夠直接將java代碼生成ASM代碼。
ASM的實際使用場景很是普遍,咱們以Matrix爲例。
Matrix是微信開源的一個APM框架,其中TraceCanary子模塊用於監測幀率低、卡頓、ANR等場景,具有函數耗時統計的功能。
爲了實現函數的耗時統計,一般的作法都是在函數執行開始和結束爲止進行插樁,最後以兩個插樁點的時間差爲函數的執行時間。
# -> MethodTracer.TraceMethodAdapter
@Override
protected void onMethodEnter() {
TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
if (traceMethod != null) {
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
//入口插樁
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
}
}
@Override
protected void onMethodExit(int opcode) {
TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
...
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
//出口插樁
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
}
複製代碼
整體上就是每一個方法的開頭和結尾處各添加一行代碼,而後交由TraceMethod進行統計和計算。
詳情見舊文Matrix系列文章(一) 卡頓分析工具之Trace Canary。
接下來,咱們分析一下ASM的不足。
更多詳情參見舊文 Android ASM框架詳解 。
javassit是一個開源的字節碼建立、編輯類庫,現屬於Jboss web容器的一個子模塊,特色是簡單、快速,與AspectJ同樣,使用它不須要了解字節碼和虛擬機指令,這裏是官方文檔。
javassit核心的類庫包含ClassPool,CtClass ,CtMethod和CtField。
javassit API簡潔直觀,好比咱們想動態建立一個類,並添加一個helloWorld方法。
ClassPool pool = ClassPool.getDefault();
//經過makeClass建立類
CtClass ct = pool.makeClass("test.helloworld.Test");//建立類
//爲ct添加一個方法
CtMethod helloMethod = CtNewMethod.make("public void helloWorld(String des){ System.out.println(des);}",ct);
ct.addMethod(helloMethod);
//寫入文件
ct.writeFile();
//加載進內存
// ct.toClass();
複製代碼
而後,咱們想在helloWorld方法先後織入代碼。
ClassPool pool = ClassPool.getDefault();
//獲取class
CtClass ct = pool.getCtClass("test.helloworld.Test");
//獲取helloWorld方法
CtMethod m = ct.getDeclaredMethod("helloWorld");
//在方法開頭織入
m.insertBefore("{ System.out.print(\"before insert\");");
//在方法末尾織入 可以使用this關鍵字
m.insertAfter("{System.out.println(this.x); }");
//寫入文件
ct.writeFile();
複製代碼
javassit的語法直觀簡潔的特色,使得在不少開源項目中都有它的身影。
好比QQ zone的熱修復方案,當時遇到的問題是補丁包加載作odex優化時,因爲差分的patch包並不依賴其餘dex,致使補丁包中的類被打上is_preverfied標籤(這有助於運行時提高性能),但在補丁運行時實際會去引用其餘dex中的類,就會拋出錯誤java.lang.IllegalAccessError:Class ref pre-verified class resovled to unexpected implement。
當時qq空間團隊的解決方案是在編譯階段爲對全部類的構造方法進行插樁,引用一個事先定義好的AnalyseLoad類,而後干預分包過程,讓這個類處於一個獨立的dex中,這樣就避免了上述問題。
這裏用的AOP方案就是javassit,詳情見 QQ空間補丁方案解析 。
還有最近開源的插件化框架 shadow,shadow框架中的一個需求是,插件包具有獨立運行的能力,當運行插件工程時,插件中Activity的父類ShadowActivity繼承Activity,當插件做爲子模塊加載到插件中時ShadowActivity沒必要繼承系統Activity,只是做爲一個代理類就夠了。此時shadow團隊封裝了JavassistTransform,在編譯期動態修改Activity的父類。
動態代理是代理模式的一種實現,用於在運行時動態加強原始類的行爲,實現方式是運行時直接生成class字節碼並將其加載進虛擬機。
JDK自己就提供一個Proxy類用於實現動態代理。 咱們一般使用下面的API建立代理類。
# java.lang.reflect.Proxy
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
複製代碼
其中在InvocationHandler實現類中定義核心切點代碼。
public class InvocationHandlerImpl implements InvocationHandler {
/** 被代理的實例 */
private Object mObj = null;
public InvocationHandlerImpl(Object obj){
this.mObj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//前切入點
Object result = method.invoke(this.mObj, args);
//後切入點
return result;
}
}
複製代碼
這樣在先後切入點的位置能夠編寫要織入的代碼。
在咱們經常使用的Retrofit框架中就用到了動態代理。Retrofit提供了一套易於開發網絡請求的註解,而在註解中聲明的參數正是經過代理包裝以後發出的網絡請求。
# Retrofit.create
public <T> T create(final Class<T> service) {
...
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
private final Object[] emptyArgs = new Object[0];
@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
//代理
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}
});
}
複製代碼
java動態代理最大的問題是隻能代理接口,而不能代理普通類或者抽象類,這是由於默認建立的代理類繼承Porxy,而java又不支持多繼承,這一點極大的限制了動態代理的使用場景,cglib可代理普通類。
更多詳情參見 設計模式之代理模式 。
最後咱們總結一下 上述AOP框架的特色及優劣勢,你能夠根據自身需求進行技術選型。
技術框架 | 特色 | 開發難度 | 優點 | 不足 |
---|---|---|---|---|
APT | 經常使用於經過註解減小模板代碼,對類的建立於加強須要依賴其餘框架。 | ★★ | 開發註解簡化上層編碼。 | 使用註解對原工程具備侵入性。 |
AspectJ | 提供完整的面向切面編程的註解。 | ★★ | 真正意義的AOP,支持通配、繼承結構的AOP,無需硬編碼切面。 | 重複織入、不織入問題,不支持java8 |
ASM | 面向字節碼指令編程,功能強大。 | ★★★ | 高效,ASM5開始支持java8。 | 切面能力不足,部分場景需硬編碼。 |
Javassit | API簡潔易懂,快速開發。 | ★ | 上手快,新人友好,具有運行時加載class能力。 | 切點代碼編寫需注意class path加載問題。 |
java動態代理 | 運行時擴展代理接口功能。 | ★ | 運行時動態加強。 | 僅支持代理接口,擴展性差,使用反射性能差。 |