前言
人閑下來就會對各種各樣的東西感到好奇欢唾,好奇的東西多了就發(fā)現(xiàn)自己是真的菜鳄厌。
今天這篇文章寫出來的原因岂贩,源自一次非常非赤拖辏“詭異的”IDE的語法錯誤提示个扰。
文章是由android的知識引入,但真正想聊的東西是編譯原理葱色。所以:才有了標題《奇怪的知識》递宅。因此各位看官沒必要太糾結(jié)自己沒有學過android或者Java,不影響閱讀~
正文
復現(xiàn)一次語法錯誤的代碼:
android知識部分
IDE提示的也很明白:res的id不能在library級別的module中的switch語法中應(yīng)用苍狰。原因是res的id不是常量办龄。
注意:同樣的代碼在application級別的module中是沒有語法問題的。所以對于res的id來說舞痰,application中是常量土榴,library中不是常量诀姚。如果有同學看過R的內(nèi)容响牛,就會發(fā)現(xiàn)的確如此:
這個是application中的R文件:
這個是library中的R文件:
這個顯現(xiàn)引申出一個android打包的知識點:aapt過程中的資源合并。
一句話描述這個知識點:不同module之間的重復的資源會按優(yōu)先級的進行合并覆蓋赫段。這個流程引發(fā)的問題呀打,很多老司機都遇到過,資源被覆蓋了糯笙,我們引用的資源永遠會被指向唯一的res贬丛。這肯定是不符合預(yù)期的。
因此諸如給資源名加前綴的方案便應(yīng)運而生给涕。
為什么不是final
這里咱們聊一個問題:常量有什么特別之處豺憔?下面的代碼,編譯之后就是能看到常量的特別之處:
class TestFinal {
static final int sInt = 1;
void testFinal(){
int temp = sInt;
System.out.println(temp);
}
}
編譯后的代碼會是這樣:
public void testFinal(){
System.out.println(1);
}
會發(fā)現(xiàn)編譯器的優(yōu)化够庙,會把常量直接內(nèi)聯(lián)到代碼引用之處恭应。那么這邊咱們想想:如果library里的res也是常量會出現(xiàn)什么問題?
常量被內(nèi)聯(lián)耘眨,一旦發(fā)生項目中資源重復昼榛,打包過程中就出現(xiàn)覆蓋,那么內(nèi)聯(lián)的常量已經(jīng)不能映射到真正的資源上了剔难,因為資源已經(jīng)被覆蓋胆屿。
不是final引發(fā)的問題
library中的R引用不是常量,就意味著這種用法也是不能工作的:
可以看到偶宫,注解也是要常量的非迹,所以這個問題對我們印象還是挺大的...等等!Butterknife就是注解的這種用法為什么沒有問題纯趋?憎兽?
深入了解過Butterknife的同學應(yīng)該知道,Butterknife針對這種情況進行了特殊處理:
Butterknife的方案
Butterknife為了不讓注解處出現(xiàn)語法錯誤,自己創(chuàng)造了一個叫做R2
的類唇兑。這個類其實就是原樣copy了R
酒朵,唯一不同就是,R2
都是常量扎附。
的確這樣不會有語法錯誤蔫耽,但是咱們剛才也分析了:常量內(nèi)聯(lián),資源覆蓋留夜。所以一旦滿足case匙铡,那就是crash。所以Butterknife有時如何規(guī)避這個問題的呢碍粥?
看過Butterknife中findViewById()
源碼的同學應(yīng)該都是到鳖眼,此處Butterknife的實現(xiàn)大概是這樣:
public TestActivity_ViewBinding(T target, View source) {
this.target = target;
target.parentLayout = Utils.findRequiredViewAsType(source, R.id.test, "field 'parentLayout'", ViewGroup.class);
}
我們能夠看到,Butterknife最終打進包里的代碼嚼摩,并沒有發(fā)生常量內(nèi)聯(lián)钦讳!所以它是怎么做的呢?
看到這里的同學枕面,不妨停下來愿卒。自己想想如果是你,你會怎么解決這個問題潮秘?這里我說說我能想到的方案:
ASM階段琼开,把內(nèi)聯(lián)的代碼,再給它改寫成R的正常引用枕荞。問題就來了:ASM的輸入是class柜候,這個時機我沒辦法再拿到R的正常引用了。那如果繼續(xù)提前這個干預(yù)的過程躏精,放到APT階段呢渣刷?
試了一下,也沒有搞定玉控。APT階段拿到的注解value也已經(jīng)是被內(nèi)聯(lián)的常量了...
這就有點奇怪了飞主,Butterknife是如何做到通過內(nèi)聯(lián)的常量和R引用的映射呢?翻看了Butterknife的源碼高诺,發(fā)現(xiàn)Butterknife是在APT階段執(zhí)行的碌识,關(guān)鍵類在ButterKnifeProcessor。
Butterknife通過JCTree
這個api拿到了R的引用虱而,然后把內(nèi)聯(lián)的代碼又改回了R的引用筏餐。具體的api實現(xiàn)咱們就不看了,有興趣的同學可以自行g(shù)ithub牡拇。
咱們接下來聊一聊這個JCTree
是干啥的魁瞪?
編譯原理
我們都知道:日常我們寫下的代碼穆律,最終想要運行在目標機器上都需要編譯成目標機器能夠識別的機器碼。而做這些工作的我們稱之為編譯器导俘。一般編譯器就是干了如下的事情:
圖片來自《編譯原理》第二版
在各種源碼編譯的實現(xiàn)中峦耘,基本都不約而同地抽象出一個概念個:抽象語法樹(AST),以求在整個編譯實現(xiàn)過程更加的方便旅薄。
一句話解釋抽象語法樹:源代碼語法結(jié)構(gòu)的一種抽象表示辅髓。它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個節(jié)點都表示源代碼中的一種結(jié)構(gòu)少梁。
咱們粗略了解了編譯器的的實現(xiàn)流程洛口,那么編譯器又是怎么實現(xiàn)的呢?當然是用代碼實現(xiàn)的咯凯沪,而且它們的實現(xiàn)往往離我們很近...以我們java編譯器為例第焰。
入坑Java時,我們應(yīng)該都試過javac
妨马。而這個命令的實現(xiàn)在哪挺举?就在JDK里的tools.jar
中的com.sun.tools.javac.Main
包下。核心邏輯在于com.sun.tools.javac.main.JavaCompiler
身笤。
這里邊就實現(xiàn)了如何分析我們的源碼豹悬,如何轉(zhuǎn)化成class。也就上那個圖中編譯器該干的事液荸。
那么JCTree
在整個編譯過程中充當什么角色呢?一句話:JCTree
是對源碼的一種api級別的描述脱篙〗壳或者說JCTree
是java編譯流程中語法樹的實現(xiàn)。
也就是說通過JCTree
相關(guān)api绊困,我們可以訪問到源碼結(jié)構(gòu)文搂。說起來似乎很抽象,我們debug個一段代碼就能get到它存在的意義了:
fun main() {
val context = Context()
val scanner = RScanner()
val javaCompiler = JavaCompiler.instance(context)
val testJavaCodeFile = File("/Users/x/xx/xxx/TestAutoCode.java")
ToolProvider
.getSystemJavaCompiler()
.getStandardFileManager(DiagnosticCollector(), null, null)
.getJavaFileObjectsFromFiles(listOf(testJavaCodeFile))
.forEach {
javaCompiler.parse(it).defs.forEach {
scanner.scan(it)
}
}
}
class RScanner : TreeScanner() {
override fun visitMethodDef(tree: JCTree.JCMethodDecl?) {
super.visitMethodDef(tree)
}
}
基于這一套api我們是能夠獲取到源碼的任何信息的秤朗。而且這段demo代碼煤蹭,只需要導入tools.jar
就可以快速運行,成本非常的低取视。
用代碼run代碼
上述我們同過JavaCompiler
的實例硝皂,對java源碼進行了動態(tài)的編譯,拿到的結(jié)果就是這個java源碼的class文件作谭。有了class文件稽物,我們就可以通過ClassLoader
去加載這個class。
有了上邊的基礎(chǔ)折欠,實現(xiàn)源碼已經(jīng)不重要贝或,這里貼一個鏈接大家自取吧:How do you dynamically compile and load external java classes?
尾聲
我個人沒有正經(jīng)的學過編譯原理吼过,所以了解這部分內(nèi)容時,覺得還是挺神奇的咪奖。也希望這篇文章能對同樣沒有學過編譯原理的同學帶來一些思考和啟發(fā)~