理解AOP
之前幾篇文章我們詳細介紹了AOP的幾種技術(shù)方案偿凭,由于AOP技術(shù)復(fù)雜多樣,實際需求也不盡相同弯囊,那么我們應(yīng)該如何做技術(shù)選型呢?
本篇將會對現(xiàn)有的AOP技術(shù)做一個統(tǒng)一的介紹胶果,尤其側(cè)重在Android方向的落地,希望對你有所幫助霎烙,文中內(nèi)容蕊连、示例大都來自工作總結(jié),如有偏頗不妥尝蠕,歡迎指正。
這里先統(tǒng)一一下基本名詞载庭,以便表述看彼。
- 切面: 對一類行為的抽象廊佩,是切點的集合,比如在用戶訪問所有模塊前做的權(quán)限認證靖榕。
- 切點: 描述切面的具體的一個業(yè)務(wù)場景标锄。
- 通知(Advice)類型: 通常分為切點前、切點后和切點內(nèi)茁计,比如在方法前織入代碼是指切點前鸯绿。
AOP是一種面向切面編程的技術(shù)的統(tǒng)稱,AOP框架最終都會圍繞class字節(jié)碼的操作展開簸淀,無論是對字節(jié)碼的操作增刪改瓶蝴,為方便描述,我們統(tǒng)稱為代碼的織入租幕。
雖然AOP翻譯過來叫面向切面編程舷手,但在實際使用過程中,切面可能退化成了一個點劲绪,比如我們想統(tǒng)計app的冷啟動時間男窟,這就非常具體了。如果我們用AOP的技術(shù)實現(xiàn)統(tǒng)計所有函數(shù)的耗時時間贾富,自然能統(tǒng)計到類似啟動這個階段的時間歉眷。
從狹義來看實現(xiàn)AOP技術(shù)的框架必須是能將切面編程抽象成上層可以直接使用的工具或API,但當(dāng)我們將切面降維后颤枪,最終面向的就是切點而已汗捡。換句話說,只要能將代碼織入到某個點那這種技術(shù)就一定可以實現(xiàn)AOP畏纲,這樣AOP技術(shù)所涵蓋的領(lǐng)域就得以拓展扇住,因為從狹義的角度看目前只有AspectJ符合這個標(biāo)準(zhǔn)。
從廣義上來講盗胀,AOP技術(shù)可以是任何能實現(xiàn)代碼織入的技術(shù)或框架艘蹋,對代碼的改動最終都會體現(xiàn)在字節(jié)碼上,而這類技術(shù)也可以叫做字節(jié)碼增強票灰,通用名詞理解即可女阀。
下面我們將介紹一些常用的AOP技術(shù)。
首先屑迂,從織入的時機的角度看浸策,可以分為源碼階段、class階段屈糊、dex階段的榛、運行時織入。
對于前三項源碼階段逻锐、class階段夫晌、dex織入雕薪,由于他們都發(fā)生在class加載到虛擬機前,我們統(tǒng)稱為靜態(tài)織入晓淀,
而在運行階段發(fā)生的改動所袁,我們統(tǒng)稱為動態(tài)織入。
常見的技術(shù)框架如下表:
織入時機 | 技術(shù)框架 |
---|---|
靜態(tài)織入 | APT凶掰,AspectJ燥爷、ASM、Javassit |
動態(tài)織入 | java動態(tài)代理懦窘,cglib前翎、Javassit |
靜態(tài)織入發(fā)生在編譯器,因此幾乎不會對運行時的效率產(chǎn)生影響畅涂;動態(tài)織入發(fā)生在運行期港华,可直接將字節(jié)碼寫入內(nèi)存,并通過反射完成類的加載午衰,所以效率相對較低立宜,但更靈活。
動態(tài)織入的前提是類還未被加載臊岸,你不能將一個已經(jīng)加載的類經(jīng)過修改再次加載橙数,這是ClassLoader的限制。但是可以通過另一個ClassLoader進行加載帅戒,虛擬機允許兩個相同類名的class被不同的ClassLoader加載灯帮,在運行時也會被認為是兩個不同的類,因此需要注意不能相互賦值蜘澜, 不然會拋出ClassCastException施流。
java動態(tài)代理响疚、cglib只會創(chuàng)建新的代理類而不是對原有類的字節(jié)碼直接修改鄙信,Javassit可修改原有字節(jié)碼。
其實利用反射或者hook技術(shù)同樣可以實現(xiàn)代碼行為的改變忿晕,但由于這類技術(shù)并沒有真正的改變原有的字節(jié)碼装诡,所以暫不在談?wù)摲秶鷥?nèi),比如xposed践盼,dexposed鸦采。
其次,我們需要關(guān)注這些框架具備哪切面編程的能力咕幻,這有助于幫助我做技術(shù)選型渔伯,由于AspectJ、ASM 肄程、Javassit是相對比較完善的AOP框架锣吼,因此只對三者進行比較选浑。
能力 | AspectJ | ASM | Javassit |
---|---|---|---|
切面抽象 | ? | ||
切點抽象 | ? | ||
通知類型抽象 | ? | ? | ? |
其中:
切面抽象:具備篩選過濾class的能力,比如我們想為Activity的所有生命周期織入代碼玄叠,那你是不是首先需要具備過濾Activity及其子類的能力古徒。
切點抽象:具體到某個class,是否具備方法读恃、字段隧膘、注解訪問的能力。
通知類型抽象:是否直接支持在方法前寺惫、后疹吃、中直接織入代碼。
當(dāng)然不具備能力不代表不能做AOP編程西雀,可以通過其他方法解決互墓,只是易用性的問題。
下面我們將開始對上述框架逐一介紹蒋搜,Let' go~~~
APT
APT(Annotation Processing Tool)即注解處理器篡撵,在Gradle 版本>=2.2后被annotationProcessor取代。
它用來在編譯時掃描和處理注解豆挽,掃描過程可使用 auto-service 來簡化尋找注解的配置育谬,在處理過程中可生成java文件(創(chuàng)建java文件通常依賴 javapoet 這個庫)。常用于生成一些模板代碼或運行時依賴的類文件帮哈,比如常見的ButterKnife膛檀、Dagger、ARouter娘侍,它的優(yōu)點是簡單方便咖刃。
以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)
是如何實現(xiàn)控件的賦值的?
事實上@Bind注解在編譯期會生成一個MainActivity_ViewBinding類憾筏,而ButterKnife.bind(this)這次調(diào)用最終會通過反射創(chuàng)建出MainActivity_ViewBinding對象嚎杨,并把activity的引用傳遞給它。
# ButterKnife
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
...
//創(chuàng)建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的構(gòu)造函數(shù)中完成控件的賦值氧腰。
public class MainActivity_ViewBinding<T extends MainActivity> implements Unbinder {
protected T target;
public MainActivity_ViewBinding(final T target, Finder finder, Object source) {
...
//為控件賦值 其中優(yōu)化了控件的查找
target.toolbar = finder.findRequiredViewAsType(source, R.id.toolbar, "field 'toolbar'", Toolbar.class);
...
}
}
為了在此類中能訪問到MainActivity中聲明的屬性枫浙,為此ButterKnife框架要求,使用@Bind注解聲明的屬性不能是private的古拴。
可以看到ButterKnife中仍然用到了反射箩帚,這是為了統(tǒng)一API使用ButterKnife.bind(...)作出的犧牲,而Dagger則會通過Component黄痪,Module的名字通過動態(tài)生成不同的方法名紧帕,因此使用之前需要對工程進行build。
之所以會這樣桅打,是因為APT技術(shù)的不足是嗜,通常只是用來創(chuàng)建新的類轻纪,而不能對原有類進行改動,在不能改動的情況下叠纷,只能通過反射實現(xiàn)動態(tài)化刻帚。
AspectJ
AspectJ是一種嚴格意義上的AOP技術(shù),因為它提供了完整的面向切面編程的注解涩嚣,這樣讓使用者可以在不關(guān)心字節(jié)碼原理的情況下完成代碼的織入崇众,因為編寫的切面代碼就是要織入的實際代碼。
AspectJ實現(xiàn)代碼織入有兩種方式航厚,一是自行編寫.ajc文件顷歌,二是使用AspectJ提供的@Aspect、@Pointcut等注解幔睬,二者最終都是通過ajc編譯器完成代碼的織入眯漩。
舉個簡單的例子,假設(shè)我們想統(tǒng)計所有view的點擊事件麻顶,使用AspectJ只需要寫一個類即可赦抖。
@Aspect
public class MethodAspect {
private static final String TAG = "MethodAspect5";
//切面表達式,聲明需要過濾的類和方法
@Pointcut("execution(* android.view.View.OnClickListener+.onClick(..))")
public void callMethod() {
}
//before表示在方法調(diào)用前織入
@before("callMethod()")
public void beforeMethodCall(ProceedingJoinPoint joinPoint) {
//編寫業(yè)務(wù)代碼
}
}
注解簡明直觀辅肾,上手難度近乎為0队萤。
常用的函數(shù)耗時統(tǒng)計工具Hugo,就是AspectJ的一個實際應(yīng)用矫钓,Android平臺Hujiang開源的AspectJX插件靈感也來自于Hugo要尔,詳情見舊文Android 函數(shù)耗時統(tǒng)計工具之Hugo。
AspectJ雖然好用新娜,但也存在一些嚴重的問題赵辕。
- 重復(fù)織入、不織入
- 不支持Java8
AspectJ切面表達式支持繼承語法概龄,雖然方便了開發(fā)还惠,但存在致命的問題,就是在繼承樹上的類可能都會織入代碼旁钧,這在多數(shù)業(yè)務(wù)場景下是不適用的吸重,比如無埋點。
另外使用java8語法編寫的代碼歪今,不會被進入切面范圍,也就無法織入代碼颜矿。
更多詳情參見舊文 Android AspectJ詳解 寄猩。
ASM
ASM是非常底層的面向字節(jié)碼編程的AOP框架,理論上可以實現(xiàn)任何關(guān)于字節(jié)碼的修改骑疆,非常硬核田篇。許多字節(jié)碼生成API底層都是用ASM實現(xiàn)替废,常見比如Groovy、cglib泊柬,因此在Android平臺下使用ASM無需添加額外的依賴椎镣。完整的學(xué)習(xí)ASM必須了解字節(jié)碼和JVM相關(guān)知識。
比如要織入一句簡單的日志輸出
Log.d("tag", " onCreate");
使用ASM編寫是下面這個樣子兽赁,沒錯因為JVM是基于棧的状答,函數(shù)的調(diào)用需要參數(shù)先入棧,然后執(zhí)行函數(shù)入棧刀崖,最后出棧惊科,總共四條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代碼可能對應(yīng)多行ASM API代碼蜂莉,因為一行java代碼背后可能隱藏這多個JVM指令蜡娶。
你不必擔(dān)心不會編寫ASM代碼,官方提供了ASM Bytecode Outline插件可以直接將java代碼生成ASM代碼映穗。
ASM的實際使用場景非常廣泛翎蹈,我們以Matrix為例。
Matrix是微信開源的一個APM框架男公,其中TraceCanary子模塊用于監(jiān)測幀率低荤堪、卡頓、ANR等場景枢赔,具備函數(shù)耗時統(tǒng)計的功能澄阳。
為了實現(xiàn)函數(shù)的耗時統(tǒng)計,通常的做法都是在函數(shù)執(zhí)行開始和結(jié)束為止進行插樁踏拜,最后以兩個插樁點的時間差為函數(shù)的執(zhí)行時間碎赢。
# -> 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);
}
總體上就是每個方法的開頭和結(jié)尾處各添加一行代碼,然后交由TraceMethod進行統(tǒng)計和計算速梗。
詳情見舊文Matrix系列文章(一) 卡頓分析工具之Trace Canary肮塞。
接下來,我們分析一下ASM的不足姻锁。
- 切面代碼需要硬編碼枕赵,通常是手動寫過濾條件,不夠靈活位隶,試想一下如何用ASM實現(xiàn)統(tǒng)計所有Activity的生命周期方法拷窜。
- 很難實現(xiàn)在方法調(diào)用前后織入新的代碼,而在AspectJ中一個call關(guān)鍵字就解決了。
更多詳情參見舊文 Android ASM框架詳解 篮昧。
javassit
javassit是一個開源的字節(jié)碼創(chuàng)建赋荆、編輯類庫,現(xiàn)屬于Jboss web容器的一個子模塊懊昨,特點是簡單窄潭、快速,與AspectJ一樣酵颁,使用它不需要了解字節(jié)碼和虛擬機指令嫉你,這里是官方文檔。
javassit核心的類庫包含ClassPool材义,CtClass 均抽,CtMethod和CtField。
- ClassPool:一個基于HashMap實現(xiàn)的CtClass對象容器其掂。
- CtClass:表示一個類油挥,可從ClassPool中通過完整類名獲取。
- CtMethods:表示類中的方法款熬。
- CtFields :表示類中的字段深寥。
javassit API簡潔直觀,比如我們想動態(tài)創(chuàng)建一個類贤牛,并添加一個helloWorld方法惋鹅。
ClassPool pool = ClassPool.getDefault();
//通過makeClass創(chuàng)建類
CtClass ct = pool.makeClass("test.helloworld.Test");//創(chuàng)建類
//為ct添加一個方法
CtMethod helloMethod = CtNewMethod.make("public void helloWorld(String des){ System.out.println(des);}",ct);
ct.addMethod(helloMethod);
//寫入文件
ct.writeFile();
//加載進內(nèi)存
// 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關(guān)鍵字
m.insertAfter("{System.out.println(this.x); }");
//寫入文件
ct.writeFile();
javassit的語法直觀簡潔的特點闰集,使得在很多開源項目中都有它的身影。
比如QQ zone的熱修復(fù)方案般卑,當(dāng)時遇到的問題是補丁包加載做odex優(yōu)化時武鲁,由于差分的patch包并不依賴其他dex,導(dǎo)致補丁包中的類被打上is_preverfied標(biāo)簽(這有助于運行時提升性能)蝠检,但在補丁運行時實際會去引用其他dex中的類沐鼠,就會拋出錯誤java.lang.IllegalAccessError:Class ref pre-verified class resovled to unexpected implement。
當(dāng)時qq空間團隊的解決方案是在編譯階段為對所有類的構(gòu)造方法進行插樁叹谁,引用一個事先定義好的AnalyseLoad類饲梭,然后干預(yù)分包過程,讓這個類處于一個獨立的dex中焰檩,這樣就避免了上述問題憔涉。
這里用的AOP方案就是javassit,詳情見 QQ空間補丁方案解析 。
還有最近開源的插件化框架shadow,shadow框架中的一個需求是,插件包具備獨立運行的能力,當(dāng)運行插件工程時浪腐,插件中Activity的父類ShadowActivity繼承Activity,當(dāng)插件作為子模塊加載到插件中時ShadowActivity不必繼承系統(tǒng)Activity顿乒,只是作為一個代理類就夠了议街。此時shadow團隊封裝了JavassistTransform,在編譯期動態(tài)修改Activity的父類璧榄。
詳見 調(diào)試研究Shadow對字節(jié)碼編輯的正確姿勢 特漩。
動態(tài)代理
動態(tài)代理是代理模式的一種實現(xiàn),用于在運行時動態(tài)增強原始類的行為骨杂,實現(xiàn)方式是運行時直接生成class字節(jié)碼并將其加載進虛擬機涂身。
JDK本身就提供一個Proxy類用于實現(xiàn)動態(tài)代理。
我們通常使用下面的API創(chuàng)建代理類搓蚪。
# java.lang.reflect.Proxy
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
其中在InvocationHandler實現(xiàn)類中定義核心切點代碼蛤售。
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框架中就用到了動態(tài)代理妒潭。Retrofit提供了一套易于開發(fā)網(wǎng)絡(luò)請求的注解悴能,而在注解中聲明的參數(shù)正是通過代理包裝之后發(fā)出的網(wǎng)絡(luò)請求。
# 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動態(tài)代理最大的問題是只能代理接口雳灾,而不能代理普通類或者抽象類漠酿,這是因為默認創(chuàng)建的代理類繼承Porxy,而java又不支持多繼承谎亩,這一點極大的限制了動態(tài)代理的使用場景炒嘲,cglib可代理普通類。
更多詳情參見 設(shè)計模式之代理模式 匈庭。
總結(jié)
最后我們總結(jié)一下 上述AOP框架的特點及優(yōu)劣勢夫凸,你可以根據(jù)自身需求進行技術(shù)選型。
技術(shù)框架 | 特點 | 開發(fā)難度 | 優(yōu)勢 | 不足 |
---|---|---|---|---|
APT | 常用于通過注解減少模板代碼嚎花,對類的創(chuàng)建于增強需要依賴其他框架寸痢。 | ★★ | 開發(fā)注解簡化上層編碼。 | 使用注解對原工程具有侵入性紊选。 |
AspectJ | 提供完整的面向切面編程的注解啼止。 | ★★ | 真正意義的AOP,支持通配兵罢、繼承結(jié)構(gòu)的AOP献烦,無需硬編碼切面。 | 重復(fù)織入卖词、不織入問題巩那,不支持java8 |
ASM | 面向字節(jié)碼指令編程吏夯,功能強大。 | ★★★ | 高效即横,ASM5開始支持java8噪生。 | 切面能力不足,部分場景需硬編碼东囚。 |
Javassit | API簡潔易懂跺嗽,快速開發(fā)。 | ★ | 上手快页藻,新人友好桨嫁,具備運行時加載class能力。 | 切點代碼編寫需注意class path加載問題份帐。 |
java動態(tài)代理 | 運行時擴展代理接口功能璃吧。 | ★ | 運行時動態(tài)增強。 | 僅支持代理接口废境,擴展性差畜挨,使用反射性能差。 |