談談Android AOP技術方案

理解AOP

以前幾篇文章咱們詳細介紹了AOP的幾種技術方案,因爲AOP技術複雜多樣,實際需求也不盡相同,那麼咱們應該如何作技術選型呢?html

本篇將會對現有的AOP技術作一個統一的介紹,尤爲側重在Android方向的落地,但願對你有所幫助,文中內容、示例大都來自工做總結,若有偏頗不妥,歡迎指正。java

這裏先統一一下基本名詞,以便表述。android

  • 切面: 對一類行爲的抽象,是切點的集合,好比在用戶訪問全部模塊前作的權限認證。
  • 切點: 描述切面的具體的一個業務場景。
  • 通知(Advice)類型: 一般分爲切點前、切點後和切點內,好比在方法前織入代碼是指切點前。

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

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

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雖然好用,但也存在一些嚴重的問題。

  • 重複織入、不織入
  • 不支持Java8

AspectJ切面表達式支持繼承語法,雖然方便了開發,但存在致命的問題,就是在繼承樹上的類可能都會織入代碼,這在多數業務場景下是不適用的,好比無埋點。

另外使用java8語法編寫的代碼,不會被進入切面範圍,也就沒法織入代碼。

更多詳情參見舊文 Android AspectJ詳解

ASM

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的不足。

  • 切面代碼須要硬編碼,一般是手動寫過濾條件,不夠靈活,試想一下如何用ASM實現統計全部Activity的生命週期方法。
  • 很難實如今方法調用先後織入新的代碼,而在AspectJ中一個call關鍵字就解決了。

更多詳情參見舊文 Android ASM框架詳解

javassit

javassit是一個開源的字節碼建立、編輯類庫,現屬於Jboss web容器的一個子模塊,特色是簡單、快速,與AspectJ同樣,使用它不須要了解字節碼和虛擬機指令,這裏是官方文檔

javassit核心的類庫包含ClassPool,CtClass ,CtMethod和CtField。

  • ClassPool:一個基於HashMap實現的CtClass對象容器。
  • CtClass:表示一個類,可從ClassPool中經過完整類名獲取。
  • CtMethods:表示類中的方法。
  • CtFields :表示類中的字段。

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的父類。

詳見 調試研究Shadow對字節碼編輯的正確姿式

動態代理

動態代理是代理模式的一種實現,用於在運行時動態加強原始類的行爲,實現方式是運行時直接生成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動態代理 運行時擴展代理接口功能。 運行時動態加強。 僅支持代理接口,擴展性差,使用反射性能差。
相關文章
相關標籤/搜索