一文應(yīng)用 AOP | 最全選型考量 + 邊剖析經(jīng)典開源庫邊實(shí)踐,美滋滋

AOP系列思維導(dǎo)圖

前言


繁多的 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 工作流程

直接上圖說明系忙。

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

APT工作流程

可以看到關(guān)鍵步驟就幾項(xiàng):

  1. 定義注解
  2. 編寫注解處理器
  3. 掃描注解
  4. 編寫代理類內(nèi)容
  5. 生成代理類
  6. 調(diào)用代理類

我們標(biāo)出重點(diǎn)慌申,也就是我們需要實(shí)現(xiàn)的步驟陌选。如下:

APT工作流程重點(diǎ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 支持役电。

  1. 通過 Java Annotation Tool 的 Filer 可以幫助我們以文件的形式輸出JAVA源碼。
  2. 通過 Java Annotation Tool 的 Elements 可以幫助我們處理掃描過程中掃描到的所有的元素節(jié)點(diǎn)餐弱,比如包(PackageElement)宴霸、類(TypeElement)囱晴、方法(ExecuteableElement)等膏蚓。
  3. 通過 Java Annotation Tool 的 TypeMirror 可以幫助我們判斷某個(gè)元素是否是我們想要的類型。
(2)JavaPoet

你當(dāng)然可以直接通過字符串拼接的方式去生成 java 源碼畸写,怎么簡單怎么來驮瞧,一張圖 show JavaPoet 的厲害之處。

生成同樣的類枯芬,使用JavaPoet前论笔,字符串拼接

生成同樣的類,使用JavaPoet后千所,以面向?qū)ο蟮姆绞絹砩稍创a
(3)APT 插件

注解處理器已經(jīng)有了狂魔,那么怎么執(zhí)行它?這個(gè)時(shí)候就需要用到 android-apt 這個(gè)插件了淫痰,使用它有兩個(gè)目的:

  1. 允許配置只在編譯時(shí)作為注解處理器的依賴最楷,而不添加到最后的APK或library
  2. 設(shè)置源路徑,使注解處理器生成的代碼能被Android Studio正確的引用

項(xiàng)目引入了 butterknife 之后就無需引入 apt 了,如果繼續(xù)引入會報(bào) Using incompatible plugins for the annotation processing

(4)AutoService

想要運(yùn)行注解處理器籽孙,需要繁瑣的步驟:

  1. 在 processors 庫的 main 目錄下新建 resources 資源文件夾烈评;
  2. 在 resources文件夾下建立 META-INF/services 目錄文件夾;
  3. 在 META-INF/services 目錄文件夾下創(chuàng)建 javax.annotation.processing.Processor 文件犯建;
  4. 在 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é):

Aspect之hugo工作流程

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 語法根蟹?

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工作流程:

Aspect侵入式工作流程
Aspect非侵入式工作流程

前面選擇合適的 AOP 方法第 2 點(diǎn)我們提到性雄,以 Pointcut 切入點(diǎn)作為區(qū)分没卸,AspectJ 有兩種用法:

  1. 用自定義注解修飾切入點(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 {
        //...
    }
}
  1. 不需要在切入點(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 的腳本容器嫉沽。

Task執(zhí)行流程
(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
  1. Gradle 使用 Groovy 語言實(shí)現(xiàn)站粟,想要自定義 Gradle 插件就需要使用 Groovy 語言。
  2. Groovy 語言 = Java語言的擴(kuò)展 + 眾多腳本語言的語法曾雕,運(yùn)行在 JVM 虛擬機(jī)上奴烙,可以與 Java 無縫對接。Java 開發(fā)者學(xué)習(xí) Groovy 的成本并不高剖张。

2. 小結(jié)

所以我們需要怎么做切诀?流程總結(jié)如下:

Javassist應(yīng)用流程

3. 實(shí)戰(zhàn) —— 自動(dòng)TryCatch

代碼里到處都是防范性catch

既然說了這么多,是時(shí)候?qū)崙?zhàn)了搔弄,每次看到項(xiàng)目代碼里充斥著防范性 try-catch幅虑,我就

我們照著流程圖,一步步來實(shí)現(xiàn)這個(gè)自動(dòng) try-Catch 功能:

(1)自定義 Plugin
  1. 新建一個(gè) module顾犹,選擇 library module倒庵,module 名字必須為 buildSrc
  2. 刪除 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'
}
  1. 新建 groovy 目錄


  2. 新建 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;
}

接著我們要在 TryCatchInjecttraverseMethod方法 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 打印出異常了。

完整demo請戳

后記


完成本篇過程曲折,最終成稿已經(jīng)完全偏離當(dāng)初擬定的大綱拉宗,本來想詳細(xì)記錄下 AOP 的應(yīng)用如孝,把每種方法都一步步實(shí)踐一遍操禀,但在寫作的過程中君躺,我不斷地質(zhì)疑自己,這種步驟文全網(wǎng)都是笔时,于自己于大家又有什么意義棍好? 想著把寫作方向改為 AOP 開源庫源碼分析,但又難以避免陷入大段源碼分析的泥潭中允耿。

本文的初衷在于 AOP 的實(shí)踐借笙,既然是實(shí)踐,何不拋棄語法細(xì)節(jié)较锡,抽象流程业稼,圖示步驟,畢竟學(xué)習(xí)完能真正吸收的一是魔鬼的細(xì)節(jié)蚂蕴,二是精妙的思想低散。

寫作本身就是一種思考,謹(jǐn)以警示自己骡楼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末熔号,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子鸟整,更是在濱河造成了極大的恐慌引镊,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異弟头,居然都是意外死亡吩抓,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門亮瓷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來琴拧,“玉大人降瞳,你說我怎么就攤上這事嘱支。” “怎么了挣饥?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵除师,是天一觀的道長。 經(jīng)常有香客問我扔枫,道長汛聚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任短荐,我火速辦了婚禮倚舀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘忍宋。我一直安慰自己痕貌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布糠排。 她就那樣靜靜地躺著舵稠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪入宦。 梳的紋絲不亂的頭發(fā)上哺徊,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音乾闰,去河邊找鬼落追。 笑死,一個(gè)胖子當(dāng)著我的面吹牛涯肩,可吹牛的內(nèi)容都是我干的淋硝。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼宽菜,長吁一口氣:“原來是場噩夢啊……” “哼谣膳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起铅乡,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤继谚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后阵幸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體花履,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡芽世,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了诡壁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片济瓢。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖妹卿,靈堂內(nèi)的尸體忽然破棺而出旺矾,到底是詐尸還是另有隱情,我是刑警寧澤夺克,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布箕宙,位于F島的核電站,受9級特大地震影響铺纽,放射性物質(zhì)發(fā)生泄漏柬帕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一狡门、第九天 我趴在偏房一處隱蔽的房頂上張望陷寝。 院中可真熱鬧,春花似錦其馏、人聲如沸凤跑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽饶火。三九已至,卻和暖如春致扯,著一層夾襖步出監(jiān)牢的瞬間肤寝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工抖僵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鲤看,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓耍群,卻偏偏與公主長得像义桂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子蹈垢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容