Aspect 語法難懂?ASM 字節碼操做繁瑣?APT 難以精準找到切入點?你該試試 AST 了!編輯器級別,效率高,更輕量。html
在開始上手以前,咱們先了解下幾個簡單的概念:java
咱們知道,編程語言再怎麼變,不變的是由「類型」「運算符」「流程語句」「函數」「對象」組成的本質,這些本質概念表達了底層的運算與邏輯,那麼這麼多編程語言,要怎麼抽離出這個邏輯本質呢?android
答案就是:轉化爲統一的結構!git
這個統一的結構不依賴於源語言的語法,只表明源語言中的語法結構,如類型、修飾符、運算符…… 這就是抽象語法樹 AST。AST(abstract syntax tree)即抽象語法樹,是源代碼的抽象語法結構的樹狀表現形式,每個節點表明一個語法結構。那 AST 是怎麼轉化得來的呢?github
不一樣的語言,都會有對應不一樣的語法分析器,語法分析器會把源代碼做爲字符串讀入、解析,並創建語法樹,這是一個程序完成編譯所必要的前期工做。編程
咱們看下 Java 的編譯過程,重點關注步驟一和步驟二:架構
步驟一:詞法分析,將源代碼的字符流轉變爲 Token 列表。app
一個個讀取源代碼,按照預約規則合併成 Token,Token 是編譯過程的最小元素,關鍵字、變量名、字面量、運算符等均可以成爲 Token。eclipse
步驟二:語法分析,根據 Token 流來構造樹形表達式也就是 AST。編程語言
語法樹的每個節點都表明着程序代碼中的一個語法結構,如類型、修飾符、運算符等。通過這個步驟後,編譯器就基本不會再對源碼文件進行操做了,後續的操做都創建在抽象語法樹之上。
- 能夠訪問 Astexplorer 在線玩轉 AST
咱們能夠發現,AST 定義了代碼的結構,經過操做 AST,咱們能夠精準地定位到聲明語句、賦值語句、運算語句等,實現對源代碼的分析、優化、變動等操做。
舉個例子,想要改變 a 的賦值,以下圖:
想改 a 的賦值,能夠對 AST 語法樹的 value 節點下手,一旦改動,編譯器會從新進行編譯流程處理,此時賦值改動就反映到源碼上了。是否是很神奇?其實 Lombok、IDE 語法高亮、IDE 格式化代碼、自動補全、代碼混淆壓縮、甚至大名鼎鼎的 ButterKnife 的 R、R2 文件映射和靜態代碼檢查,都是利用了 AST。
既然要操做 AST,咱們怎麼拿到 AST 呢?
答案是:在註解處理器 APT!
利用 JDK 的註解處理器,可在編譯期間處理註解,還能夠讀取、修改、添加 AST 中的任意元素,讓改動後的 AST 從新參與編譯流程處理,直到語法樹沒有改動爲止。
相比其餘的AOP方法,AST 屬於編輯器級別,時機更爲提早,效率更高。
但語法複雜,推薦經過庫來操做 AST:
總體思路:在編譯期間拿到 AST,掃描是否含有特定日誌語句如:Log,存在則刪除該語句。
@SupportedAnnotationTypes
指定此註解處理器支持的註解,可用 *
指定全部註解 @SupportedSourceVersion
指定支持的java的版本
在註解處理器的 init 函數裏,經過 Trees.instance(env)
拿到抽象語法樹(AST)。 此處把ProcessingEnvironment
強轉成JavacProcessingEnvironment
,後面的操做都變成了IDE編輯器內部的操做了。
在註解處理器的 process 函數中,咱們掃描全部的類,實現一個自定義的 TreeTranslator。
爲何自定義的 TreeTranslator 要複寫 visitBlock?由於咱們的需求場景是掃描全部 log 語句,粒度爲語句塊。AST 支持咱們以不一樣的粒度去訪問,還有哪些粒度呢?咱們看下TreeTranslator 的繼承層次,能夠發現一個 Visitor 類。
打開 Visitor 類:
全部 visit 方法一目瞭然,咱們前面提到 AST 每個節點都表明着源語言中的一個語法結構,因此咱們能夠細粒度到指定訪問 if、return、try等特定類型節點,只需覆寫相應的 visit 方法。
回到咱們的需求場景:掃描全部 log 語句,既然是語句,粒度應該爲語句塊,因此咱們覆寫 visitBlock 進行掃描,當掃描到指定語句好比 Log.
時,就不把整個語句都寫入 AST,以此達到清除 log 語句的效果。
- 想了解更多 AST 操做語法?詳見 java註解處理器——在編譯期修改語法樹
- 想獲取 demo 源碼請戳
有了實戰的基礎,咱們再來看看 ButterKnife 是如何利用 AST 的。全網對這塊的講解少之又少,解析只着重於 APT,實在惋惜。
細心的你會發如今 ButterKnife 的 sample-library 中,註解的都是引用了 R2 :
爲何 library 工程不直接引用 R?當咱們把 R2 改爲 R 以後,編譯器會報錯:
也就是說註解的屬性必須是常量,可是 library 中 R.id.title 的值爲變量。緣由見 Non-constant Fields in Case Labels.、Android主項目和Module中R類的區別。
那咱們能夠拷貝下 R 文件,生成一個 R2,把屬性都改成常量便可解決。爲了讓這個拷貝過程無感知,J 神使用了 gradle 插件來自動化完成,這就是 library 須要引用 butterknife-gradle-plugin 的緣由。
那另外一個問題來了,R2 僅僅是 module 中 R 的複製,只表明了所在 module 編譯期間 R 的值,在運行時主工程的 R 和 R2 徹底對不上,單純地拷貝修改是不行的。咋整呢?
那咱們生成 R2 供編譯期使用,在生成代碼階段把 R2 替換成 R 不就好了?好主意!J 神的思路就是這樣的!咱們打開生成的 XXX_ViewBinding
文件就能夠發現 —— R2 已經被換成了 R。
可是怎麼拿到 R 和 R2 的映射呢?
咱們思考下:以 @BindView(R2.id.view)
爲例,最終生成的代碼是 findViewById(0x7f…)
。那咱們經過 0x7f…
反尋 R2.id.view
這樣的常量名,R 和 R2 同樣,因此也連帶知道了 R.id.view
變量名,因而能夠將生成代碼的結果從 findViewById(0x7f…)
替換成 findViewById(R.id.view)
,這裏的 R
在主工程的編譯過程當中會被 inline 成最終肯定的數值,從而避免在生成代碼的過程當中直接填寫數值帶來的麻煩。
思路肯定了,那接下來第一步就是經過 0x7f…
反尋 R2.id.view
,可是在 APT 裏,咱們只能拿到 Element 的註解值,也就是說,並不知道當前傳入的是 R2 的哪一個 field。如今就該輪到 AST 大顯身手了,根據 Element 反查出真正 Java 文件的樹形結構。
你覺得 AST 的應用場景就這麼多了嗎?
不不不,咱們開下腦洞,既然拿到了源代碼的樹形表達式,咱們不必定要把表達式轉回成源碼,那是否是能夠經過它自動寫代碼?畫個源碼流程圖?畫個類圖?寫個說明文檔?或者其它你想要的東西?
看看這個項目 js-code-to-svg-flowchart,或許能給帶你更多靈感。
也許下面這些資料能夠答疑:
本篇完成耗時 24 個番茄鍾(600 分鐘)
我是 FeelsChaotic,一個寫得了代碼 p 得了圖,剪得了視頻畫得了畫的程序媛,致力於追求代碼優雅、架構設計和 T 型成長。
歡迎關注 FeelsChaotic 的簡書和掘金,若是個人文章對你哪怕有一點點幫助,歡迎 ❤️!你的鼓勵是我寫做的最大動力!
最最重要的,請給出你的建議或意見,有錯誤請多多指正!