相信你們在入門 AOP 時,經常被繁多的術語、方法和框架繞暈。AOP 好像有點耳熟?Javaseopt 是個什麼?Javassist 又是啥?Dexposed、APT 也是 AOP?本篇將輔助你快速理清概念,掌握 AOP 思想,找到最適合本身業務場景的 AOP 方法。java
上文 也談代碼 —— 重構兩年前的代碼 中,咱們提到最佳的系統架構由模塊化的關注面領域組成,每一個關注面均用純 Java 對象實現。不一樣的領域之間用最不具備侵害性的「方面」或「類方面」工具整合起來。android
反思本身的項目,有不少模塊沒有作到恰當地切分關注面,每每在業務邏輯中耦合了業務埋點、權限申請、登錄狀態的判斷、對不可預知異常 try-catch 和一些持久化操做。git
雖然說保證代碼最簡單化和可運行化頗有必要,但咱們仍是能夠嘗試小範圍的重構。就如「代碼整潔之道」中所說:經過方面式的手段切分關注面的威力不可低估,假如你能用 POJO 編寫應用程序的領域邏輯,在代碼層面與架構關注面分離開,就有可能真正地用測試來驅動架構。github
這裏的切分關注面的思想就是 AOP。數據庫
AOP 是 Aspect Oriented Programming 的縮寫,譯爲面向切向編程。用咱們最經常使用的 OOP 來對比理解:編程
舉個小例子:設計模式
設計一個日誌打印模塊。按 OOP 思想,咱們會設計一個打印日誌 LogUtils 類,而後在須要打印的地方引用便可。api
public class ClassA {
private void initView() {
LogUtils.d(TAG, "onInitView");
}
}
public class ClassB {
private void onDataComplete(Bean bean) {
LogUtils.d(TAG, bean.attribute);
}
}
public class ClassC {
private void onError() {
LogUtils.e(TAG, "onError");
}
}
複製代碼
看起來沒有任何問題是吧?緩存
可是這個類是橫跨並嵌入衆多模塊裏的,在各個模塊裏分散得很厲害,處處都能見到。從對象組織角度來說,咱們通常採用的分類方法都是使用相似生物學分類的方法,以「繼承」關係爲主線,咱們稱之爲縱向,也就是 OOP。設計時只使用 OOP思想可能會帶來兩個問題:安全
對象設計的時候通常都是縱向思惟,若是這個時候考慮這些不一樣類對象的共性,不只會增長設計的難度和複雜性,還會形成類的接口過多而難以維護(共性越多,意味着接口契約越多)。
須要對現有的對象 動態增長 某種行爲或責任時很是困難。
而AOP就能夠很好地解決以上的問題,怎麼作到的?除了這種縱向分類以外,咱們從橫向的角度去觀察這些對象,無需再去處處調用 LogUtils 了,聲明哪些地方須要打印日誌,這個地方就是一個切面,AOP 會在適當的時機爲你把打印語句插進切面。
// 只須要聲明哪些方法須要打印 log,打印什麼內容
public class ClassA {
@Log(msg = "onInitView")
private void initView() {
}
}
public class ClassB {
@Log(msg = "bean.attribute")
private void onDataComplete(Bean bean) {
}
}
public class ClassC {
@Log(msg = "onError")
private void onError() {
}
}
複製代碼
若是說 OOP 是把問題劃分到單個模塊的話,那麼 AOP 就是把涉及到衆多模塊的某一類問題進行統一管理。AOP的目標是把這些功能集中起來,放到一個統一的地方來控制和管理。利用 AOP 思想,這樣對業務邏輯的各個部分進行了隔離,從而下降業務邏輯各部分之間的耦合,提升程序的可重用性,提升開發效率。
面向目標不一樣:簡單來講 OOP 是面向名詞領域,AOP 面向動詞領域。
思想結構不一樣:OOP 是縱向結構,AOP 是橫向結構。
注重方面不一樣:OOP 注重業務邏輯單元的劃分,AOP 偏重業務處理過程當中的某個步驟或階段。
二者之間是一個相互補充和完善的關係。
那AOP既然這麼有用,除了上面提到的打印日誌場景,還有沒有其餘用處呢?
固然有!
只要系統的業務模塊都須要引用通用模塊,就可使用AOP。如下是一些經常使用的業務場景:
系統之間在進行接口調用時,每每是有入參傳遞的,入參是接口業務邏輯實現的先決條件,有時入參的缺失或錯誤會致使業務邏輯的異常,大量的異常捕獲無疑增長了接口實現的複雜度,也讓代碼顯得雍腫冗長,所以提早對入參進行驗證是有必要的,能夠提早處理入參數據的異常,並封裝好異常轉化成結果對象返回給調用方,也讓業務邏輯解耦變得獨立。
避免處處都是申請權限和處理權限的代碼
好比全局的登陸狀態流程控制。
防止View被連續點擊觸發屢次事件
檢測方法耗時其實已經有一些現成的工具,好比 trace view。痛點是這些工具使用起來都比較麻煩,效率低下,並且沒法針對某一個塊代碼或者某個指定的sdk進行查看方法耗時。能夠採用 AOP 思想對每一個方法作一個切點,在執行以後打印方法耗時。
聲明方法,爲特定方法加上事務,指定狀況下(好比拋出異常)回滾事務
替代防護性的 try-Catch。
緩存某方法的返回值,下次執行該方法時,直接從緩存裏獲取。
使用 Hook 修改軟件的驗證類的判斷邏輯。
AOP 可讓咱們在執行一個方法的前插入另外一個方法,運用這個思路,咱們能夠把有 bug 的方法替換成咱們下發的新方法。
本篇爲入門篇,重在理解 AOP 思想和應用,輔助你快速進行 AOP 方法選型,因此 AOP 方法這塊暫不會深刻原理和術語。
Android AOP 經常使用的方法有 JNI HOOK 和 靜態織入。
在運行期,目標類加載後,爲接口動態生成代理類,將切面植入到代理類中。相對於靜態AOP更加靈活。但切入的關注點須要實現接口。對系統有一點性能影響。
Dexposed
Xposed
epic
在 native 層修改 java method 對應的 native 指針
Cglib 是一個強大的,高性能的 Code 生成類庫, 原理是在運行期間目標字節碼加載後,經過字節碼技術爲一個類建立子類,並在子類中採用方法攔截的技術攔截全部父類方法的調用,順勢織入橫切邏輯。因爲是經過子類來代理父類,所以不能代理被 final 字段修飾的方法。
可是 Cglib 有一個很致命的缺點:底層是採用著名的 ASM 字節碼生成框架,使用字節碼技術生成代理類,也就是經過操做字節碼來生成的新的 .class 文件,而咱們在 Android 中加載的是優化後的 .dex 文件,也就是說咱們須要能夠動態生成 .dex 文件代理類,所以 Cglib 不能在 Android 中直接使用。有大神根據 Dexmaker 框架(dex代碼生成工具)來仿照 Cglib 庫動態生成 .dex 文件,實現了相似於 Cglib 的 AOP 的功能。詳細的用法可參考:將cglib動態代理思想帶入Android開發
靜態織入對系統無性能影響。但靈活性不夠。
APT
AspectJ
ASM
Javassist
DexMaker
ASMDEX
這麼多方法?有什麼區別?
一圖勝千言
AOP 是思想,上面的方法其實都是工具,只不過是插入時機和方式不一樣。
同:均可以織入邏輯,都體現了 AOP 思想
異:做用的時機不同,且適用的註解的類型不同。
方法 | 做用時機 | 操做對象 | 優勢 | 缺點 | 爲了上手,我須要掌握什麼? |
---|---|---|---|---|---|
APT | 編譯期:還未編譯爲 class 時 | .java 文件 | 1. 能夠織入全部類;2. 編譯期代理,減小運行時消耗 | 1. 須要使用 apt 編譯器編譯;2. 須要手動拼接代理的代碼(可使用 Javapoet 彌補);3. 生成大量代理類 | 設計模式和解耦思想的靈活應用 |
AspectJ | 編譯期、加載時 | .java 文件 | 功能強大,除了 hook 以外,還能夠爲目標類添加變量,接口。也有抽象,繼承等各類更高級的玩法。 | 1. 不夠輕量級;2. 定義的切點依賴編程語言,沒法兼容Lambda語法;3. 沒法織入第三方庫;4. 會有一些兼容性問題,如:D八、Gradle 4.x等 | 複雜的語法,但掌握幾個簡單的,就能實現絕大多數場景 |
Javassist | 編譯期:class 還未編譯爲 dex 時或運行時 | class 字節碼 | 1. 減小了生成子類的開銷;2. 直接操做修改編譯後的字節碼,直接繞過了java編譯器,因此能夠作不少突破限制的事情,例如,跨 dex 引用,解決熱修復中 CLASS_ISPREVERIFIED 問題。 | 運行時加入切面邏輯,產生性能開銷。 | 1. 自定義 Gradle 插件;2. 掌握groovy 語言 |
ASM | 編譯期或運行期字節碼注入 | class 字節碼 | 小巧輕便、性能好,效率比Javassist高 | 學習成本高 | 須要熟悉字節碼語法,ASM 經過樹這種數據結構來表示複雜的字節碼結構,並利用 Push 模型來對樹進行遍歷,在遍歷過程當中對字節碼進行修改。 |
ASMDEX | 編譯期和加載時:轉化爲 .dex 後 | Dex 字節碼,建立 class 文件 | 能夠織入全部類 | 學習成本高 | 須要對 class 文件比較熟悉,編寫過程複雜。 |
DexMaker | 同ASMDEX | Dex 字節碼,建立 dex 文件 | 同ASMDEX | 同ASMDEX | 同ASMDEX |
Cglib | 運行期生成子類攔截方法 | 字節碼 | 沒有接口也能夠織入 | 1. 不能代理被final字段修飾的方法;2. 須要和 dexmaker 結合使用 | -- |
xposed | 運行期hook | -- | 能hook本身應用進程的方法,能hook其餘應用的方法,能hook系統的方法 | 依賴三方包的支持,兼容性差,手機須要root | -- |
dexposed | 運行期hook | -- | 只能hook本身應用進程的方法,但無需root | 1. 依賴三方包的支持,兼容性差;2. 只能支持 Dalvik 虛擬機 | -- |
epic | 運行期hook | -- | 支持 Dalvik 和 Art 虛擬機 | 只適合在開發調試中使用,碎片化嚴重有兼容性問題 | -- |
業務中經常使用的 AOP 方式爲靜態織入,接下來詳細介紹靜態織入中最經常使用的三種方式:APT、AspectJ、Javassist。
APT (Annotation Processing Tool )即註解處理器,是一種處理註解的工具,確切的說它是 javac 的一個工具,它用來在編譯時掃描和處理註解。註解處理器以 Java 代碼( 或者編譯過的字節碼)做爲輸入,生成 .java 文件做爲輸出。簡單來講就是在編譯期,經過註解生成 .java 文件。使用的 Annotation 類型是 SOURCE。
表明框架:DataBinding、Dagger二、ButterKnife、EventBus三、DBFlow、AndroidAnnotation
目前 Android 註解解析框架主要有兩種實現方法,一種是運行期經過反射去解析當前類,注入相應要運行的方法。另外一種是在編譯期生成類的代理類,在運行期直接調用代理類的代理方法,APT 指的是後者。
若是不使用APT基於註解動態生成 java 代碼,那麼就須要在運行時使用反射或者動態代理,好比大名鼎鼎的 butterknife 以前就是在運行時反射處理註解,爲咱們實例化控件並添加事件,然而這種方法很大的一個缺點就是用了反射,致使 app 性能降低。因此後面 butterknife 改成 apt 的方式,能夠留意到,butterknife 會在編譯期間生成一個 XXX_ViewBinding.java
。雖然 APT 增長了代碼量,可是再也不須要用反射,也就無損性能。
性能問題解決了,又帶來新的問題了。咱們在處理註解或元數據文件的時候,每每有自動生成源代碼的須要。難道咱們要手動拼接源代碼嗎?不不不,這不符合代碼的優雅,JavaPoet 這個神器就是來解決這個問題的。
JavaPoet 是 square 推出的開源 java 代碼生成框架,提供 Java Api 生成 .java 源文件。這個框架功能很是有用,咱們能夠很方便的使用它根據註解、數據庫模式、協議格式等來對應生成代碼。經過這種自動化生成代碼的方式,可讓咱們用更加簡潔優雅的方式要替代繁瑣冗雜的重複工做。本質上就是用建造者模式來替代手工拼寫源文件。
JavaPoet詳細用法可參考:javapoet——讓你從重複無聊的代碼中解放出來
目前最好、最方便、最火的 AOP 實現方式當屬 AspectJ,它是一種幾乎和 Java 徹底同樣的語言,並且徹底兼容 Java。
可是在 Android 上集成 AspectJ 是比較複雜的。
咱們須要使用 andorid-library gradle 插件在編譯時作一些 hook。使用 AspectJ 的編譯器(ajc,一個java編譯器的擴展)對全部受 aspect 影響的類進行織入。在 gradle 的編譯 task 中增長一些額外配置,使之能正確編譯運行。等等等等……
有不少庫幫助咱們完成這些工做,能夠方便快捷接入 AspectJ。
庫 | 大小 | 兼容性 | 缺點 | 備註 |
---|---|---|---|---|
Hugo | 131kb | -- | 不支持AAR或JAR切入 | -- |
gradle-android-aspectj-plugin | -- | -- | 沒法兼容databinding,不支持AAR或JAR切入 | 該庫已經棄用 |
AspectJx(推薦) | 44kb | 會和有transform功能的插件衝突,如:retroLambda | 在前二者基礎上擴展支持AAR, JAR及Kotlin的應用 | 僅支持annotation的方式,不支持 *.aj 文件的編譯 |
表明框架:熱修復框架HotFix 、Savior(InstantRun)
Javassist 是一個編輯字節碼的框架,做用是修改編譯後的 class 字節碼,ASM也有這個功能,不過 Javassist 的 Java 風格 API 要比 ASM 更容易上手。
既然是修改編譯後的 class 字節碼,首先咱們得知道何時編譯完成,而且咱們要在 .class文件被轉爲 .dex 文件以前去作修改。在 Gradle Transfrom 這個 api 出來以前,想要監聽項目被打包成 .dex 的時機,就必須自定義一個 Gradle Task,插入到 predex 或者 dex 以前,在這個自定義的 Task 中使用 Javassist 或者 ASM 對 class 字節碼進行操做。而 Transform 更爲方便,咱們再也不須要插入到某個Task前面。Tranfrom 有本身的執行時機,一經註冊便會自動添加到 Task 執行序列中,且正好是 class 被打包成dex以前。
AOP 重在理解這種思想:
任何的技術都須要有業務依託和落地,想要一步步實現 AOP 應用落地?請戳 一文應用 AOP | 最全選型考量 + 邊剖析經典開源庫邊實踐,美滋滋。
我是 FeelsChaotic,一個寫得了代碼 p 得了圖,剪得了視頻畫得了畫的程序媛,致力於追求代碼優雅、架構設計和 T 型成長。
歡迎關注 FeelsChaotic 的簡書和掘金,若是個人文章對你哪怕有一點點幫助,歡迎 ❤️!你的鼓勵是我寫做的最大動力!
最最重要的,請給出你的建議或意見,有錯誤請多多指正!