前言
Aspect 語(yǔ)法難懂览效?ASM 字節(jié)碼操作繁瑣却舀?APT 難以精準(zhǔn)找到切入點(diǎn)?你該試試 AST 了锤灿!編輯器級(jí)別挽拔,效率高,更輕量但校。
一螃诅、概念
在開(kāi)始上手之前,我們先了解下幾個(gè)簡(jiǎn)單的概念:
什么是 AST 状囱?AST 的作用术裸?
我們知道,編程語(yǔ)言再怎么變亭枷,不變的是由「類(lèi)型」「運(yùn)算符」「流程語(yǔ)句」「函數(shù)」「對(duì)象」組成的本質(zhì)袭艺,這些本質(zhì)概念表達(dá)了底層的運(yùn)算與邏輯,那么這么多編程語(yǔ)言叨粘,要怎么抽離出這個(gè)邏輯本質(zhì)呢匹表?
答案就是:轉(zhuǎn)化為統(tǒng)一的結(jié)構(gòu)!
這個(gè)統(tǒng)一的結(jié)構(gòu)不依賴于源語(yǔ)言的語(yǔ)法宣鄙,只代表源語(yǔ)言中的語(yǔ)法結(jié)構(gòu)袍镀,如類(lèi)型、修飾符冻晤、運(yùn)算符……
這就是抽象語(yǔ)法樹(shù) AST苇羡。AST(abstract syntax tree)即抽象語(yǔ)法樹(shù),是源代碼的抽象語(yǔ)法結(jié)構(gòu)的樹(shù)狀表現(xiàn)形式鼻弧,每一個(gè)節(jié)點(diǎn)代表一個(gè)語(yǔ)法結(jié)構(gòu)设江。那 AST 是怎么轉(zhuǎn)化得來(lái)的呢?
AST 的生成過(guò)程
不同的語(yǔ)言攘轩,都會(huì)有對(duì)應(yīng)不同的語(yǔ)法分析器叉存,語(yǔ)法分析器會(huì)把源代碼作為字符串讀入、解析度帮,并建立語(yǔ)法樹(shù)歼捏,這是一個(gè)程序完成編譯所必要的前期工作。
我們看下 Java 的編譯過(guò)程笨篷,重點(diǎn)關(guān)注步驟一和步驟二:
步驟一:詞法分析瞳秽,將源代碼的字符流轉(zhuǎn)變?yōu)?Token 列表。
一個(gè)個(gè)讀取源代碼率翅,按照預(yù)定規(guī)則合并成 Token练俐,Token 是編譯過(guò)程的最小元素,關(guān)鍵字冕臭、變量名腺晾、字面量燕锥、運(yùn)算符等都可以成為 Token。
步驟二:語(yǔ)法分析悯蝉,根據(jù) Token 流來(lái)構(gòu)造樹(shù)形表達(dá)式也就是 AST归形。
語(yǔ)法樹(shù)的每一個(gè)節(jié)點(diǎn)都代表著程序代碼中的一個(gè)語(yǔ)法結(jié)構(gòu),如類(lèi)型泉粉、修飾符、運(yùn)算符等榴芳。經(jīng)過(guò)這個(gè)步驟后嗡靡,編譯器就基本不會(huì)再對(duì)源碼文件進(jìn)行操作了,后續(xù)的操作都建立在抽象語(yǔ)法樹(shù)之上窟感。
- 可以訪問(wèn) Astexplorer 在線玩轉(zhuǎn) AST
怎么利用 AST讨彼?
我們可以發(fā)現(xiàn),AST 定義了代碼的結(jié)構(gòu)柿祈,通過(guò)操作 AST哈误,我們可以精準(zhǔn)地定位到聲明語(yǔ)句、賦值語(yǔ)句躏嚎、運(yùn)算語(yǔ)句等蜜自,實(shí)現(xiàn)對(duì)源代碼的分析、優(yōu)化卢佣、變更等操作重荠。
舉個(gè)例子,想要改變 a 的賦值虚茶,如下圖:
想改 a 的賦值戈鲁,可以對(duì) AST 語(yǔ)法樹(shù)的 value 節(jié)點(diǎn)下手,一旦改動(dòng)嘹叫,編譯器會(huì)重新進(jìn)行編譯流程處理婆殿,此時(shí)賦值改動(dòng)就反映到源碼上了。是不是很神奇罩扇?其實(shí) Lombok婆芦、IDE 語(yǔ)法高亮、IDE 格式化代碼喂饥、自動(dòng)補(bǔ)全寞缝、代碼混淆壓縮、甚至大名鼎鼎的 ButterKnife 的 R仰泻、R2 文件映射和靜態(tài)代碼檢查荆陆,都是利用了 AST。
既然要操作 AST集侯,我們?cè)趺茨玫?AST 呢被啼?
答案是:在注解處理器 APT帜消!
利用 JDK 的注解處理器,可在編譯期間處理注解浓体,還可以讀取泡挺、修改、添加 AST 中的任意元素命浴,讓改動(dòng)后的 AST 重新參與編譯流程處理娄猫,直到語(yǔ)法樹(shù)沒(méi)有改動(dòng)為止。
AST優(yōu)缺點(diǎn)
相比其他的AOP方法生闲,AST 屬于編輯器級(jí)別媳溺,時(shí)機(jī)更為提前,效率更高碍讯。
但語(yǔ)法復(fù)雜悬蔽,推薦通過(guò)庫(kù)來(lái)操作 AST:
二、實(shí)踐
實(shí)現(xiàn)一個(gè)清除 log 功能
整體思路:在編譯期間拿到 AST捉兴,掃描是否含有特定日志語(yǔ)句如:Log蝎困,存在則刪除該語(yǔ)句。
1. 實(shí)現(xiàn) AbstractProcessor
2. 添加注解
@SupportedAnnotationTypes
指定此注解處理器支持的注解倍啥,可用 *
指定所有注解
@SupportedSourceVersion
指定支持的java的版本
3. 獲取 AST
在注解處理器的 init 函數(shù)里禾乘,通過(guò) Trees.instance(env)
拿到抽象語(yǔ)法樹(shù)(AST)。
此處把ProcessingEnvironment
強(qiáng)轉(zhuǎn)成JavacProcessingEnvironment
虽缕,后面的操作都變成了IDE編輯器內(nèi)部的操作了盖袭。
4. 操作 AST
在注解處理器的 process 函數(shù)中,我們掃描所有的類(lèi)彼宠,實(shí)現(xiàn)一個(gè)自定義的 TreeTranslator鳄虱。
為什么自定義的 TreeTranslator 要復(fù)寫(xiě) visitBlock?因?yàn)槲覀兊男枨髨?chǎng)景是掃描所有 log 語(yǔ)句凭峡,粒度為語(yǔ)句塊拙已。AST 支持我們以不同的粒度去訪問(wèn),還有哪些粒度呢摧冀?我們看下TreeTranslator 的繼承層次倍踪,可以發(fā)現(xiàn)一個(gè) Visitor 類(lèi)。
打開(kāi) Visitor 類(lèi):
所有 visit 方法一目了然索昂,我們前面提到 AST 每一個(gè)節(jié)點(diǎn)都代表著源語(yǔ)言中的一個(gè)語(yǔ)法結(jié)構(gòu)建车,所以我們可以細(xì)粒度到指定訪問(wèn) if、return椒惨、try等特定類(lèi)型節(jié)點(diǎn)缤至,只需覆寫(xiě)相應(yīng)的 visit 方法。
回到我們的需求場(chǎng)景:掃描所有 log 語(yǔ)句康谆,既然是語(yǔ)句领斥,粒度應(yīng)該為語(yǔ)句塊嫉到,所以我們覆寫(xiě) visitBlock 進(jìn)行掃描,當(dāng)掃描到指定語(yǔ)句比如 Log.
時(shí)月洛,就不把整個(gè)語(yǔ)句都寫(xiě)入 AST何恶,以此達(dá)到清除 log 語(yǔ)句的效果。
- 想了解更多 AST 操作語(yǔ)法嚼黔?詳見(jiàn) java注解處理器——在編譯期修改語(yǔ)法樹(shù)
- 想獲取 demo 源碼請(qǐng)戳
剖析 ButterKnife
有了實(shí)戰(zhàn)的基礎(chǔ)细层,我們?cè)賮?lái)看看 ButterKnife 是如何利用 AST 的。全網(wǎng)對(duì)這塊的講解少之又少唬涧,解析只著重于 APT疫赎,實(shí)在可惜。
細(xì)心的你會(huì)發(fā)現(xiàn)在 ButterKnife 的 sample-library 中爵卒,注解的都是引用了 R2 :
為什么 library 工程不直接引用 R虚缎?當(dāng)我們把 R2 改成 R 之后撵彻,編譯器會(huì)報(bào)錯(cuò):
也就是說(shuō)注解的屬性必須是常量钓株,但是 library 中 R.id.title 的值為變量。原因見(jiàn) Non-constant Fields in Case Labels.陌僵、Android主項(xiàng)目和Module中R類(lèi)的區(qū)別轴合。
那我們可以拷貝下 R 文件,生成一個(gè) R2碗短,把屬性都改為常量即可解決受葛。為了讓這個(gè)拷貝過(guò)程無(wú)感知,J 神使用了 gradle 插件來(lái)自動(dòng)化完成偎谁,這就是 library 需要引用 butterknife-gradle-plugin 的原因总滩。
那另一個(gè)問(wèn)題來(lái)了,R2 僅僅是 module 中 R 的復(fù)制巡雨,只代表了所在 module 編譯期間 R 的值闰渔,在運(yùn)行時(shí)主工程的 R 和 R2 完全對(duì)不上,單純地拷貝修改是不行的铐望。咋整呢冈涧?
那我們生成 R2 供編譯期使用,在生成代碼階段把 R2 替換成 R 不就行了正蛙?好主意督弓!J 神的思路就是這樣的!我們打開(kāi)生成的 XXX_ViewBinding
文件就可以發(fā)現(xiàn) —— R2 已經(jīng)被換成了 R乒验。
但是怎么拿到 R 和 R2 的映射呢愚隧?
我們思考下:以 @BindView(R2.id.view)
為例,最終生成的代碼是 findViewById(0x7f…)
锻全。那我們通過(guò) 0x7f…
反尋 R2.id.view
這樣的常量名奸攻,R 和 R2 一樣蒜危,所以也連帶知道了 R.id.view
變量名,于是可以將生成代碼的結(jié)果從 findViewById(0x7f…)
替換成 findViewById(R.id.view)
睹耐,這里的 R
在主工程的編譯過(guò)程中會(huì)被 inline 成最終確定的數(shù)值辐赞,從而避免在生成代碼的過(guò)程中直接填寫(xiě)數(shù)值帶來(lái)的麻煩。
思路確定了硝训,那接下來(lái)第一步就是通過(guò) 0x7f…
反尋 R2.id.view
响委,但是在 APT 里,我們只能拿到 Element 的注解值窖梁,也就是說(shuō)赘风,并不知道當(dāng)前傳入的是 R2 的哪個(gè) field。現(xiàn)在就該輪到 AST 大顯身手了纵刘,根據(jù) Element 反查出真正 Java 文件的樹(shù)形結(jié)構(gòu)邀窃。
- 拿到AST樹(shù);
- 在掃描資源時(shí)假哎,獲取 Element AST 樹(shù)瞬捕,注入自定義的 TreeScanner 訪問(wèn)器 RScanner 來(lái)訪問(wèn)子節(jié)點(diǎn);
- RScanner 尋找 R 文件內(nèi)部類(lèi)(id舵抹、string等))肪虎,建立 view 與 id 的關(guān)系;
- 拿到映射關(guān)系后惧蛹,進(jìn)行代碼拼接扇救。
擴(kuò)展
AST 應(yīng)用場(chǎng)景擴(kuò)展
你以為 AST 的應(yīng)用場(chǎng)景就這么多了嗎?
不不不香嗓,我們開(kāi)下腦洞迅腔,既然拿到了源代碼的樹(shù)形表達(dá)式,我們不一定要把表達(dá)式轉(zhuǎn)回成源碼靠娱,那是不是可以通過(guò)它自動(dòng)寫(xiě)代碼沧烈?畫(huà)個(gè)源碼流程圖?畫(huà)個(gè)類(lèi)圖饱岸?寫(xiě)個(gè)說(shuō)明文檔掺出?或者其它你想要的東西?
看看這個(gè)項(xiàng)目 js-code-to-svg-flowchart苫费,或許能給帶你更多靈感汤锨。
對(duì) AST 仍有疑問(wèn)?
也許下面這些資料可以答疑:
想了解其他 AOP 方法百框?
本篇完成耗時(shí) 24 個(gè)番茄鐘(600 分鐘)
我是 FeelsChaotic,一個(gè)寫(xiě)得了代碼 p 得了圖,剪得了視頻畫(huà)得了畫(huà)的程序媛柬泽,致力于追求代碼優(yōu)雅慎菲、架構(gòu)設(shè)計(jì)和 T 型成長(zhǎng)。
歡迎關(guān)注 FeelsChaotic 的簡(jiǎn)書(shū)和掘金锨并,如果我的文章對(duì)你哪怕有一點(diǎn)點(diǎn)幫助露该,歡迎 ??!你的鼓勵(lì)是我寫(xiě)作的最大動(dòng)力第煮!
最最重要的解幼,請(qǐng)給出你的建議或意見(jiàn),有錯(cuò)誤請(qǐng)多多指正包警!