前言
繁多的 AOP 方法該如何選擇阱扬?應(yīng)用的步驟過于繁瑣泣懊,語法概念看得頭暈?zāi)X脹?
本文將詳細(xì)展示選型種種考量維度麻惶,更是砍掉 2 個(gè)經(jīng)典開源庫的枝節(jié)馍刮,取其主干細(xì)細(xì)體會 AOP 的應(yīng)用思想和關(guān)鍵流程。一邊實(shí)踐 AOP 一邊還能掌握開源庫窃蹋,豈不快哉卡啰!
一、6 個(gè)要點(diǎn)幫你選擇合適的 AOP 方法
在上文 最全面 AOP 方法探討 中警没,我們分析對比了最熱門的幾種 AOP 方法匈辱。那么,在實(shí)際情況和業(yè)務(wù)需求中杀迹,我們該怎么考量選擇呢亡脸?
1. 明確你應(yīng)用 AOP 在什么項(xiàng)目
如果你正在維護(hù)一個(gè)現(xiàn)有的項(xiàng)目,你要么小范圍試用树酪,要么就需要選擇一個(gè)侵入性小的 AOP 方法(如:APT 代理類生效時(shí)機(jī)需要手動(dòng)調(diào)用梗掰,靈活,但在插入點(diǎn)繁多情況下侵入性過高)嗅回。
2. 明確切入點(diǎn)的相似性
第一步及穗,考慮一下切入點(diǎn)的數(shù)量和相似性,你是否愿意一個(gè)個(gè)在切點(diǎn)上面加注解绵载,還是用相似性統(tǒng)一切埂陆。
第二步,考慮下這些應(yīng)用切面的類有沒有被 final 修飾娃豹,同時(shí)相似的方法有沒有被 static 或 final 修飾時(shí)焚虱。 final 修飾的類就不能通過 cglib 生成代理,cglib 會繼承被代理類懂版,需要重寫被代理方法鹃栽,所以被代理類和方法不能是 final。
3. 明確織入的粒度和織入時(shí)機(jī)
我怎么選擇織入(Weave)的時(shí)機(jī)躯畴?編譯期間織入民鼓,還是編譯后薇芝?載入時(shí)?或是運(yùn)行時(shí)丰嘉?通過比較各大 AOP 方法在織入時(shí)機(jī)方面的不同和優(yōu)缺點(diǎn)夯到,來獲得對于如何選擇 Weave 時(shí)機(jī)進(jìn)行判定的準(zhǔn)則。
對于普通的情況而言饮亏,在編譯時(shí)進(jìn)行 Weave 是最為直觀的做法耍贾。因?yàn)樵闯绦蛑邪藨?yīng)用的所有信息,這種方式通常支持最多種類的聯(lián)結(jié)點(diǎn)路幸。利用編譯時(shí) Weave荐开,我們能夠使用 AOP 系統(tǒng)進(jìn)行細(xì)粒度的 Weave 操作,例如讀取或?qū)懭胱侄渭螂取T创a編譯之后形成的模塊將喪失大量的信息晃听,因此通常采用粗粒度的 AOP 方法。
同時(shí)着帽,對于傳統(tǒng)的編譯為本地代碼的語言如 C++ 來說,編譯完成后的模塊往往跟操作系統(tǒng)平臺相關(guān)移层,這就給建立統(tǒng)一的載入時(shí)仍翰、運(yùn)行時(shí) Weave 機(jī)制造成了困難。對于編譯為本地代碼的語言而言观话,只有在編譯時(shí)進(jìn)行 Weave 最為可行予借。盡管編譯時(shí) Weave 具有功能強(qiáng)大、適應(yīng)面廣泛等優(yōu)點(diǎn)频蛔,但他的缺點(diǎn)也很明顯灵迫。首先,它需要程序員提供所有的源代碼晦溪,因此對于模塊化的項(xiàng)目就力不從心了瀑粥。
為了解決這個(gè)問題,我們可以選擇支持編譯后 Weave 的 AOP 方法三圆。
新的問題又來了狞换,如果程序的主邏輯部分和 Aspect 作為不同的組件開發(fā),那么最為合理的 Weave 時(shí)機(jī)就是在框架載入 Aspect 代碼之時(shí)舟肉。
運(yùn)行時(shí) Weave 可能是所有 AOP 方法中最為靈活的修噪,程序在運(yùn)行過程中可以為單個(gè)的對象指定是否需要 Weave 特定的方面。
選擇合適的 Weave 時(shí)機(jī)對于 AOP 應(yīng)用來說是非常關(guān)鍵的路媚。針對具體的應(yīng)用場合黄琼,我們需要作出不同的抉擇。我們也可以結(jié)合多種 AOP 方法整慎,從而獲得更為靈活的 Weave 策略脏款。
4. 明確對性能的要求围苫,明確對方法數(shù)的要求
除了動(dòng)態(tài) Hook 方法,其他的 AOP 方法對性能影響幾乎可以忽略不計(jì)弛矛。動(dòng)態(tài) AOP 本質(zhì)使用了動(dòng)態(tài)代理够吩,不可避免要用到反射。而 APT 不可避免地要生成大量的代理類和方法丈氓。如何權(quán)衡周循,就看你對項(xiàng)目的要求。
5. 明確是否需要修改原有類
如果只是想特定地增強(qiáng)能力万俗,可以使用 APT湾笛,在編譯期間讀取 Java 代碼,解析注解闰歪,然后動(dòng)態(tài)生成 Java 代碼嚎研。
下圖是Java編譯代碼的流程:
可以看到,APT 工作在 Annotation Processing 階段库倘,最終通過注解處理器生成的代碼會和源代碼一起被編譯成 Java 字節(jié)碼临扮。不過比較遺憾的是你不能修改已經(jīng)存在的 Java 文件,比如在已經(jīng)存在的類中添加新的方法或刪除舊方法教翩,所以通過 APT 只能通過輔助類的方式來實(shí)現(xiàn)注入杆勇,這樣會略微增加項(xiàng)目的方法數(shù)和類數(shù),不過只要控制好饱亿,不會對項(xiàng)目有太大的影響蚜退。
6. 明確調(diào)用的時(shí)機(jī)
APT 的時(shí)機(jī)需要主動(dòng)調(diào)用,而其他 AOP 方法注入代碼的調(diào)用時(shí)機(jī)和切入點(diǎn)的調(diào)用時(shí)機(jī)一致彪笼。
二钻注、從開源庫剖析 AOP
AOP 的實(shí)踐都寫爛了,市面上有太多講怎么實(shí)踐 AOP 的博文了配猫。那這篇和其他的博文有什么不同呢幅恋?有什么可以讓大家受益的呢?
其實(shí) AOP 實(shí)踐很簡單泵肄,關(guān)鍵是理解并應(yīng)用佳遣,我們先參考開源庫的實(shí)踐,在這基礎(chǔ)上去抽象關(guān)鍵步驟凡伊,一邊實(shí)戰(zhàn)一邊達(dá)成閱讀開源庫任務(wù)零渐,美滋滋!
APT
1. 經(jīng)典 APT 框架 ButterKnife 工作流程
直接上圖說明系忙。
在上面的過程中诵盼,你可以看到,為什么用 @Bind 、 @OnClick 等注解標(biāo)注的屬性风宁、方法必須是 public 或 protected洁墙?
因?yàn)锽utterKnife 是通過 被代理類引用.this.editText 來注入View的。為什么要這樣呢戒财?
答案就是:性能 热监。如果你把 View 和方法設(shè)置成 private,那么框架必須通過反射來注入饮寞。
想深入到源碼細(xì)節(jié)了解 ButterKnife 更多孝扛?
2. 仿造 ButterKnife,上手 APT
我們?nèi)サ艏?xì)節(jié)幽崩,抽出關(guān)鍵流程苦始,看看 ButterKnife 是怎么應(yīng)用 APT 的。
可以看到關(guān)鍵步驟就幾項(xiàng):
- 定義注解
- 編寫注解處理器
- 掃描注解
- 編寫代理類內(nèi)容
- 生成代理類
- 調(diào)用代理類
我們標(biāo)出重點(diǎn)慌申,也就是我們需要實(shí)現(xiàn)的步驟陌选。如下:
咦,你可能發(fā)現(xiàn)了蹄溉,最后一個(gè)步驟是在合適的時(shí)機(jī)去調(diào)用代理類或門面對象咨油。這就是 APT 的缺點(diǎn)之一,在任意包位置自動(dòng)生成代碼但是運(yùn)行時(shí)卻需要主動(dòng)調(diào)用柒爵。
APT 手把手實(shí)現(xiàn)可參考 JavaPoet - 優(yōu)雅地生成代碼——3.2 一個(gè)簡單示例
3. 工具詳解
APT 中我們用到了以下 3 個(gè)工具:
(1)Java Annotation Tool
Java Annotation Tool 給了我們一系列 API 支持役电。
- 通過 Java Annotation Tool 的 Filer 可以幫助我們以文件的形式輸出JAVA源碼。
- 通過 Java Annotation Tool 的 Elements 可以幫助我們處理掃描過程中掃描到的所有的元素節(jié)點(diǎn)餐弱,比如包(PackageElement)宴霸、類(TypeElement)囱晴、方法(ExecuteableElement)等膏蚓。
- 通過 Java Annotation Tool 的 TypeMirror 可以幫助我們判斷某個(gè)元素是否是我們想要的類型。
(2)JavaPoet
你當(dāng)然可以直接通過字符串拼接的方式去生成 java 源碼畸写,怎么簡單怎么來驮瞧,一張圖 show JavaPoet 的厲害之處。
(3)APT 插件
注解處理器已經(jīng)有了狂魔,那么怎么執(zhí)行它?這個(gè)時(shí)候就需要用到 android-apt 這個(gè)插件了淫痰,使用它有兩個(gè)目的:
- 允許配置只在編譯時(shí)作為注解處理器的依賴最楷,而不添加到最后的APK或library
- 設(shè)置源路徑,使注解處理器生成的代碼能被Android Studio正確的引用
項(xiàng)目引入了 butterknife 之后就無需引入 apt 了,如果繼續(xù)引入會報(bào)
Using incompatible plugins for the annotation processing
(4)AutoService
想要運(yùn)行注解處理器籽孙,需要繁瑣的步驟:
- 在 processors 庫的 main 目錄下新建 resources 資源文件夾烈评;
- 在 resources文件夾下建立 META-INF/services 目錄文件夾;
- 在 META-INF/services 目錄文件夾下創(chuàng)建 javax.annotation.processing.Processor 文件犯建;
- 在 javax.annotation.processing.Processor 文件寫入注解處理器的全稱讲冠,包括包路徑;
Google 開發(fā)的 AutoService 可以減少我們的工作量适瓦,只需要在你定義的注解處理器上添加 @AutoService(Processor.class) 竿开,就能自動(dòng)完成上面的步驟,簡直不能再方便了犹菇。
4. 代理執(zhí)行
雖然前面有說過 APT 并不能像 Aspectj 一樣實(shí)現(xiàn)代碼插入德迹,但是可以使用變種方式實(shí)現(xiàn)。用注解修飾一系列方法揭芍,由 APT 來代理執(zhí)行胳搞。此部分可參考CakeRun
APT 生成的代理類按照一定次序依次執(zhí)行修飾了注解的初始化方法,并且在其中增加了一些邏輯判斷称杨,來決定是否要執(zhí)行這個(gè)方法肌毅。從而繞過發(fā)生 Crash 的類。
AspectJ
1. 經(jīng)典 Aspectj 框架 hugo 工作流程
J 神的框架一如既往小而美姑原,想啃開源庫源碼悬而,可以先從 J 神的開源庫先讀起。
回到正題锭汛,hugo是 J 神開發(fā)的 Debug 日志庫笨奠,包含了優(yōu)秀的思想以及流行的技術(shù),例如注解唤殴、AOP般婆、AspectJ、Gradle 插件朵逝、android-maven-gradle-plugin 等蔚袍。在進(jìn)行 hugo 源碼解讀之前,你需要首先對這些知識點(diǎn)有一定的了解配名。
先上工作流程圖啤咽,我們再講細(xì)節(jié):
2. 解惑之一個(gè)打印日志邏輯怎么織入的?
只需要一個(gè) @DebugLog
注解渠脉,hugo就能幫我們打印入?yún)⒊鰠⒂钫⒔y(tǒng)計(jì)方法耗時(shí)。自定義注解很好理解芋膘,我們重點(diǎn)看看切面 Hugo 是怎么處理的鳞青。
有沒有發(fā)現(xiàn)什么涩哟?
沒錯(cuò),切點(diǎn)表達(dá)式幫助我們描述具體要切入哪里盼玄。
AspectJ 的切點(diǎn)表達(dá)式由關(guān)鍵字和操作參數(shù)組成贴彼,以切點(diǎn)表達(dá)式 execution(* helloWorld(..))
為例,其中 execution
是關(guān)鍵字埃儿,為了便于理解器仗,通常也稱為函數(shù),而* helloWorld(..)
是操作參數(shù)童番,通常也稱為函數(shù)的入?yún)⒕ァG悬c(diǎn)表達(dá)式函數(shù)的類型很多,如方法切點(diǎn)函數(shù)剃斧,方法入?yún)⑶悬c(diǎn)函數(shù)轨香,目標(biāo)類切點(diǎn)函數(shù)等,hugo 用到的有兩種類型:
函數(shù)名 | 類型 | 入?yún)?/th> | 說明 |
---|---|---|---|
execution() | 方法切點(diǎn)函數(shù) | 方法匹配模式字符串 | 表示所有目標(biāo)類中滿足某個(gè)匹配模式的方法連接點(diǎn)幼东,例如 execution(* helloWorld(..)) 表示所有目標(biāo)類中的 helloWorld 方法臂容,返回值和參數(shù)任意 |
within() | 目標(biāo)類切點(diǎn)函數(shù) | 類名匹配模式字符串 | 表示滿足某個(gè)匹配模式的特定域中的類的所有連接點(diǎn),例如 within(com.feelschaotic.demo.*) 表示 com.feelschaotic.demo 中的所有類的所有方法 |
想詳細(xì)入門 AspectJ 語法根蟹?
- 看AspectJ在Android中的強(qiáng)勢插入
- 深入理解Android之AOP
語法講得非常詳細(xì)
3. 解惑之 AspectJ in Android 為何如此絲滑脓杉?
我們引入 hugo 只需要 3 步。
不是說 AspectJ 在 Android 中很不友好简逮?球散!說好的需要使用 andorid-library gradle 插件在編譯時(shí)做一些 hook,使用 AspectJ 的編譯器(ajc散庶,一個(gè)java編譯器的擴(kuò)展)對所有受 aspect 影響的類進(jìn)行織入蕉堰,在 gradle 的編譯 task 中增加一些額外配置,使之能正確編譯運(yùn)行等等等呢悲龟?
這些 hugo 已經(jīng)幫我們做好了(所以步驟 2 中屋讶,我們引入 hugo 的同時(shí)要使用 hugo 的 Gradle 插件,就是為了 hook 編譯)躲舌。
4. 抽絲剝繭 Aspect 的重點(diǎn)流程
抽象一下 hugo 的工作流程丑婿,我們得到了 2 種Aspect工作流程:
前面選擇合適的 AOP 方法第 2 點(diǎn)我們提到性雄,以 Pointcut 切入點(diǎn)作為區(qū)分没卸,AspectJ 有兩種用法:
- 用自定義注解修飾切入點(diǎn),精確控制切入點(diǎn)秒旋,屬于侵入式
//方法一:一個(gè)個(gè)在切入點(diǎn)上面加注解
protected void onCreate(Bundle savedInstanceState) {
//...
followTextView.setOnClickListener(view -> {
onClickFollow();
});
unFollowTextView.setOnClickListener(view -> {
onClickUnFollow();
});
}
@SingleClick(clickIntervalTime = 1000)
private void onClickUnFollow() {
}
@SingleClick(clickIntervalTime = 1000)
private void onClickFollow() {
}
@Aspect
public class AspectTest {
@Around("execution(@com.feelschaotic.aspectj.annotations.SingleClick * *(..))")
public void onClickLitener(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//...
}
}
- 不需要在切入點(diǎn)代碼中做任何修改约计,統(tǒng)一按相似性來切(比如類名,包名)迁筛,屬于非侵入式
//方法二:根據(jù)相似性統(tǒng)一切煤蚌,不需要再使用注解標(biāo)記了
protected void onCreate(Bundle savedInstanceState) {
//...
followTextView.setOnClickListener(view -> {
//...
});
unFollowTextView.setOnClickListener(view -> {
//...
});
}
@Aspect
public class AspectTest {
@Around("execution(* android.view.View.OnClickListener.onClick(..))")
public void onClickLitener(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//...
}
}
5. AspectJ 和 APT 最大的不同
APT 決定是否使用切面的權(quán)利仍然在業(yè)務(wù)代碼中,而 AspectJ 將決定是否使用切面的權(quán)利還給了切面。在寫切面的時(shí)候就可以決定哪些類的哪些方法會被代理尉桩,從邏輯上不需要侵入業(yè)務(wù)代碼筒占。
但是AspectJ 的使用需要匹配一些明確的 Join Points,如果 Join Points 的函數(shù)命名蜘犁、所在包位置等因素改變了翰苫,對應(yīng)的匹配規(guī)則沒有跟著改變,就有可能導(dǎo)致匹配不到指定的內(nèi)容而無法在該地方插入自己想要的功能这橙。
那 AspectJ 的執(zhí)行原理是什么奏窑?注入的代碼和目標(biāo)代碼是怎么連接的?請戳:會用就行了屈扎?你知道 AOP 框架的原理嗎埃唯?
三、應(yīng)用篇
Javassist
為什么用 Javassist 來實(shí)踐鹰晨?
因?yàn)閷?shí)踐過程中我們可以順帶掌握字節(jié)碼插樁的技術(shù)基礎(chǔ)墨叛,就算是后續(xù)學(xué)習(xí)熱修復(fù)、應(yīng)用 ASM模蜡,這些基礎(chǔ)都是通用的巍实。雖然 Javassist 性能比 ASM 低,但對新手很友好哩牍,操縱字節(jié)碼卻不需要直接接觸字節(jié)碼技術(shù)和了解虛擬機(jī)指令棚潦,因?yàn)?Javassist 實(shí)現(xiàn)了一個(gè)用于處理源代碼的小型編譯器,可以接收用 Java 編寫的源代碼膝昆,然后將其編譯成 Java 字節(jié)碼再內(nèi)聯(lián)到方法體中丸边。
話不多說,我們馬上上手荚孵,在上手之前妹窖,先了解幾個(gè)概念:
1. 入門概念
(1)Gradle
Javassist 修改對象是編譯后的 class 字節(jié)碼。那首先我們得知道什么時(shí)候編譯完成收叶,才能在 .class 文件被轉(zhuǎn)為 .dex 文件之前去做修改骄呼。
大多 Android 項(xiàng)目使用 Gradle 構(gòu)建,我們需要先理解 Gradle 的工作流程判没。Gradle 是通過一個(gè)一個(gè) Task 執(zhí)行完成整個(gè)流程的蜓萄,依次執(zhí)行完 Task 后,項(xiàng)目就打包完成了澄峰。 其實(shí) Gradle 就是一個(gè)裝載 Task 的腳本容器嫉沽。
(2) Plugin
那 Gralde 里面那么多 Task 是怎么來的呢,誰定義的呢俏竞?是Plugin绸硕!
我們回憶下堂竟,在 app module 的 build.gradle 文件中的第一行,往往會有 apply plugin : 'com.android.application'
玻佩,lib 的 build.gradle 則會有 apply plugin : 'com.android.library'
出嘹,就是 Plugin 為項(xiàng)目構(gòu)建提供了 Task,不同的 plugin 里注冊的 Task 不一樣咬崔,使用不同 plugin疚漆,module 的功能也就不一樣。
可以簡單地理解為刁赦, Gradle 只是一個(gè)框架娶聘,真正起作用的是 plugin,是plugin 往 Gradle 腳本中添加 Task甚脉。
(3)Task
思考一下丸升,如果一個(gè) Task 的職責(zé)是將 .java 編譯成 .class,這個(gè) Task 是不是要先拿到 java 文件的目錄牺氨?處理完成后還要告訴下一個(gè) Task class 的目錄狡耻?
沒錯(cuò),從 Task 執(zhí)行流程圖可以看出猴凹,Task 有一個(gè)重要的概念:inputs 和 outputs夷狰。
Task 通過 inputs 拿到一些需要的參數(shù),處理完畢之后就輸出 outputs郊霎,而下一個(gè) Task 的 inputs 則是上一個(gè) Task 的outputs沼头。
這些 Task 其中肯定也有將所有 class 打包成 dex 的 Task,那我們要怎么找到這個(gè) Task 书劝?在之前插入我們自己的 Task 做代碼注入呢进倍?用 Transfrom!
(4)Transform
Transfrom 是 Gradle 1.5以上新出的一個(gè) API购对,其實(shí)它也是 Task猾昆。
-
gradle plugin 1.5 以下,preDex 這個(gè) Task 會將依賴的 module 編譯后的 class 打包成 jar骡苞,然后 dex 這個(gè) Task 則會將所有 class 打包成dex垂蜗;
想要監(jiān)聽項(xiàng)目被打包成 .dex 的時(shí)機(jī),就必須自定義一個(gè) Gradle Task解幽,插入到 predex 或者 dex 之前贴见,在這個(gè)自定義的 Task 中使用 Javassist ca class 。
-
gradle plugin 1.5 以上亚铁,preDex 和 Dex 這兩個(gè) Task 已經(jīng)被 TransfromClassesWithDexForDebug 取代
Transform 更為方便蝇刀,我們不再需要插入到某個(gè) Task 前面螟加。Tranfrom 有自己的執(zhí)行時(shí)機(jī)徘溢,一經(jīng)注冊便會自動(dòng)添加到 Task 執(zhí)行序列中吞琐,且正好是 class 被打包成dex之前,所以我們自定義一個(gè) Transform 即可然爆。
(5)Groovy
- Gradle 使用 Groovy 語言實(shí)現(xiàn)站粟,想要自定義 Gradle 插件就需要使用 Groovy 語言。
- Groovy 語言 = Java語言的擴(kuò)展 + 眾多腳本語言的語法曾雕,運(yùn)行在 JVM 虛擬機(jī)上奴烙,可以與 Java 無縫對接。Java 開發(fā)者學(xué)習(xí) Groovy 的成本并不高剖张。
2. 小結(jié)
所以我們需要怎么做切诀?流程總結(jié)如下:
3. 實(shí)戰(zhàn) —— 自動(dòng)TryCatch
既然說了這么多,是時(shí)候?qū)崙?zhàn)了搔弄,每次看到項(xiàng)目代碼里充斥著防范性 try-catch幅虑,我就
我們照著流程圖,一步步來實(shí)現(xiàn)這個(gè)自動(dòng) try-Catch 功能:
(1)自定義 Plugin
- 新建一個(gè) module顾犹,選擇 library module倒庵,module 名字必須為 buildSrc
- 刪除 module 下所有文件,build.gradle 配置替換如下:
apply plugin: 'groovy'
repositories {
jcenter()
}
dependencies {
compile 'com.android.tools.build:gradle:2.3.3'
compile 'org.javassist:javassist:3.20.0-GA'
}
-
新建 groovy 目錄
新建 Plugin 類
需要注意: groovy 目錄下新建類炫刷,需要選擇 file且以.groovy
作為文件格式擎宝。
import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.gradle.AppExtension
class PathPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.logger.debug "================自定義插件成功!=========="
}
}
為了馬上看到效果浑玛,我們提前走流程圖中的步驟 4绍申,在 app module下的 buiil.gradle 中添加 apply 插件。
跑一下:
(2)自定義 Transfrom
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
class PathTransform extends Transform {
Project project
TransformOutputProvider outputProvider
// 構(gòu)造函數(shù)中我們將Project對象保存一下備用
public PathTransform(Project project) {
this.project = project
}
// 設(shè)置我們自定義的Transform對應(yīng)的Task名稱顾彰,TransfromClassesWithPreDexForXXXX
@Override
String getName() {
return "PathTransform"
}
//通過指定輸入的類型指定我們要處理的文件類型
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
//指定處理所有class和jar的字節(jié)碼
return TransformManager.CONTENT_CLASS
}
// 指定Transform的作用范圍
@Override
Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
}
@Override
void transform(Context context, Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider, boolean isIncremental)
throws IOException, TransformException, InterruptedException {
this.outputProvider = outputProvider
traversalInputs(inputs)
}
/**
* Transform的inputs有兩種類型:
* 一種是目錄失晴, DirectoryInput
* 一種是jar包,JarInput
* 要分開遍歷
*/
private ArrayList<TransformInput> traversalInputs(Collection<TransformInput> inputs) {
inputs.each {
TransformInput input ->
traversalDirInputs(input)
traversalJarInputs(input)
}
}
/**
* 對類型為文件夾的input進(jìn)行遍歷
*/
private ArrayList<DirectoryInput> traversalDirInputs(TransformInput input) {
input.directoryInputs.each {
/**
* 文件夾里面包含的是
* 我們手寫的類
* R.class拘央、
* BuildConfig.class
* R$XXX.class
* 等
* 根據(jù)自己的需要對應(yīng)處理
*/
println("it == ${it}")
//TODO:這里可以注入代碼M科ā!
// 獲取output目錄
def dest = outputProvider.getContentLocation(it.name
, it.contentTypes, it.scopes, Format.DIRECTORY)
// 將input的目錄復(fù)制到output指定目錄
FileUtils.copyDirectory(it.file, dest)
}
}
/**
* 對類型為jar文件的input進(jìn)行遍歷
*/
private ArrayList<JarInput> traversalJarInputs(TransformInput input) {
//沒有對jar注入的需求灰伟,暫不擴(kuò)展
}
}
(3)向自定義的 Plugin 注冊 Transfrom
回到我們剛剛定義的 PathPlugin拆又,在 apply 方法中注冊 PathTransfrom:
def android = project.extensions.findByType(AppExtension)
android.registerTransform(new PathTransform(project))
clean 項(xiàng)目,再跑一次栏账,確保沒有報(bào)錯(cuò)帖族。
(4)代碼注入
接著就是重頭戲了,我們新建一個(gè) TryCatchInject 類挡爵,先把掃描到的方法和類名打印出來:
這個(gè)類不同于前面定義的類竖般,無需繼承指定父類,無需實(shí)現(xiàn)指定方法茶鹃,所以我以短方法+有表達(dá)力的命名代替了注釋涣雕,如果有疑問請一定要反饋給我艰亮,我好反思是否寫得不夠清晰。
import javassist.ClassPool
import javassist.CtClass
import javassist.CtConstructor
import javassist.CtMethod
import javassist.bytecode.AnnotationsAttribute
import javassist.bytecode.MethodInfo
import java.lang.annotation.Annotation
class TryCatchInject {
private static String path
private static ClassPool pool = ClassPool.getDefault()
private static final String CLASS_SUFFIX = ".class"
//注入的入口
static void injectDir(String path, String packageName) {
this.path = path
pool.appendClassPath(path)
traverseFile(packageName)
}
private static traverseFile(String packageName) {
File dir = new File(path)
if (!dir.isDirectory()) {
return
}
beginTraverseFile(dir, packageName)
}
private static beginTraverseFile(File dir, packageName) {
dir.eachFileRecurse { File file ->
String filePath = file.absolutePath
if (isClassFile(filePath)) {
int index = filePath.indexOf(packageName.replace(".", File.separator))
boolean isClassFilePath = index != -1
if (isClassFilePath) {
transformPathAndInjectCode(filePath, index)
}
}
}
}
private static boolean isClassFile(String filePath) {
return filePath.endsWith(".class") && !filePath.contains('R') && !filePath.contains('R.class') && !filePath.contains("BuildConfig.class")
}
private static void transformPathAndInjectCode(String filePath, int index) {
String className = getClassNameFromFilePath(filePath, index)
injectCode(className)
}
private static String getClassNameFromFilePath(String filePath, int index) {
int end = filePath.length() - CLASS_SUFFIX.length()
String className = filePath.substring(index, end).replace('\\', '.').replace('/', '.')
className
}
private static void injectCode(String className) {
CtClass c = pool.getCtClass(className)
println("CtClass:" + c)
defrostClassIfFrozen(c)
traverseMethod(c)
c.writeFile(path)
c.detach()
}
private static void traverseMethod(CtClass c) {
CtMethod[] methods = c.getDeclaredMethods()
for (ctMethod in methods) {
println("ctMethod:" + ctMethod)
//TODO: 這里可以對方法進(jìn)行操作
}
}
private static void defrostClassIfFrozen(CtClass c) {
if (c.isFrozen()) {
c.defrost()
}
}
}
在 PathTransfrom 里的 TODO 標(biāo)記處調(diào)用注入類
//請注意把 com\\feelschaotic\\javassist 替換為自己想掃描的路徑
TryCatchInject.injectDir(it.file.absolutePath, "com\\feelschaotic\\javassist")
我們再次 clean 后跑一下
我們可以直接按方法的包名切挣郭,也可以按方法的標(biāo)記切(比如:特殊的入?yún)⑵!⒎椒ê灻⒎椒艺稀⒎椒ㄉ系淖⒔狻┲斗牵紤]到我們只需要對特定的方法捕獲異常,我打算用自定義注解來標(biāo)記方法流译。
在 app module 中定義一個(gè)注解
//僅支持在方法上使用
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoTryCatch {
//支持業(yè)務(wù)方catch指定異常
Class[] value() default Exception.class;
}
接著我們要在 TryCatchInject
的 traverseMethod
方法 TODO 中逞怨,使用 Javassist 獲取方法上的注解,再獲取注解的 value福澡。
private static void traverseMethod(CtClass c) {
CtMethod[] methods = c.getDeclaredMethods()
for (ctMethod in methods) {
println("ctMethod:" + ctMethod)
traverseAnnotation(ctMethod)
}
}
private static void traverseAnnotation(CtMethod ctMethod) {
Annotation[] annotations = ctMethod.getAnnotations()
for (annotation in annotations) {
def canonicalName = annotation.annotationType().canonicalName
if (isSpecifiedAnnotation(canonicalName)) {
onIsSpecifiedAnnotation(ctMethod, canonicalName)
}
}
}
private static boolean isSpecifiedAnnotation(String canonicalName) {
PROCESSED_ANNOTATION_NAME.equals(canonicalName)
}
private static void onIsSpecifiedAnnotation(CtMethod ctMethod, String canonicalName) {
MethodInfo methodInfo = ctMethod.getMethodInfo()
AnnotationsAttribute attribute = methodInfo.getAttribute(AnnotationsAttribute.visibleTag)
javassist.bytecode.annotation.Annotation javassistAnnotation = attribute.getAnnotation(canonicalName)
def names = javassistAnnotation.getMemberNames()
if (names == null || names.isEmpty()) {
catchAllExceptions(ctMethod)
return
}
catchSpecifiedExceptions(ctMethod, names, javassistAnnotation)
}
private static catchAllExceptions(CtMethod ctMethod) {
CtClass etype = pool.get("java.lang.Exception")
ctMethod.addCatch('{com.feelschaotic.javassist.Logger.print($e);return;}', etype)
}
private static void catchSpecifiedExceptions(CtMethod ctMethod, Set names, javassist.bytecode.annotation.Annotation javassistAnnotation) {
names.each { def name ->
ArrayMemberValue arrayMemberValues = (ArrayMemberValue) javassistAnnotation.getMemberValue(name)
if (arrayMemberValues == null) {
return
}
addMultiCatch(ctMethod, (ClassMemberValue[]) arrayMemberValues.getValue())
}
}
private static void addMultiCatch(CtMethod ctMethod, ClassMemberValue[] classMemberValues) {
classMemberValues.each { ClassMemberValue classMemberValue ->
CtClass etype = pool.get(classMemberValue.value)
ctMethod.addCatch('{ com.feelschaotic.javassist.Logger.print($e);return;}', etype)
}
}
完成酪我!寫個(gè) demo 遛一遛:
可以看到應(yīng)用沒有崩潰挡鞍,logcat 打印出異常了。
后記
完成本篇過程曲折,最終成稿已經(jīng)完全偏離當(dāng)初擬定的大綱拉宗,本來想詳細(xì)記錄下 AOP 的應(yīng)用如孝,把每種方法都一步步實(shí)踐一遍操禀,但在寫作的過程中君躺,我不斷地質(zhì)疑自己,這種步驟文全網(wǎng)都是笔时,于自己于大家又有什么意義棍好? 想著把寫作方向改為 AOP 開源庫源碼分析,但又難以避免陷入大段源碼分析的泥潭中允耿。
本文的初衷在于 AOP 的實(shí)踐借笙,既然是實(shí)踐,何不拋棄語法細(xì)節(jié)较锡,抽象流程业稼,圖示步驟,畢竟學(xué)習(xí)完能真正吸收的一是魔鬼的細(xì)節(jié)蚂蕴,二是精妙的思想低散。
寫作本身就是一種思考,謹(jǐn)以警示自己骡楼。