掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
本文轉(zhuǎn)載自微信公眾號(hào)「咸魚正翻身」,作者M(jìn)Dove。轉(zhuǎn)載本文請(qǐng)聯(lián)系咸魚正翻身公眾號(hào)。

10年積累的網(wǎng)站制作、成都做網(wǎng)站經(jīng)驗(yàn),可以快速應(yīng)對(duì)客戶對(duì)網(wǎng)站的新想法和需求。提供各種問(wèn)題對(duì)應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認(rèn)識(shí)你,你也不認(rèn)識(shí)我。但先網(wǎng)站制作后付款的網(wǎng)站建設(shè)流程,更有威縣免費(fèi)網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
前言
人閑下來(lái)就會(huì)對(duì)各種各樣的東西感到好奇,好奇的東西多了就發(fā)現(xiàn)自己是真的菜。
今天這篇文章寫出來(lái)的原因,源自一次非常非?!霸幃惖摹盜DE的語(yǔ)法錯(cuò)誤提示。
文章是由android的知識(shí)引入,但真正想聊的東西是編譯原理。所以:才有了標(biāo)題《奇怪的知識(shí)點(diǎn)》。因此各位看官?zèng)]必要太糾結(jié)自己沒(méi)有學(xué)過(guò)android或者Java,不影響閱讀~
復(fù)現(xiàn)一次語(yǔ)法錯(cuò)誤的代碼:
正文
一、android知識(shí)部分
IDE提示的也很明白:res的id不能在library級(jí)別的module中的switch語(yǔ)法中應(yīng)用。原因是res的id不是常量。
注意:同樣的代碼在application級(jí)別的module中是沒(méi)有語(yǔ)法問(wèn)題的。所以對(duì)于res的id來(lái)說(shuō),application中是常量,library中不是常量。如果有同學(xué)看過(guò)R的內(nèi)容,就會(huì)發(fā)現(xiàn)的確如此:
這個(gè)是application中的R文件:
這個(gè)是library中的R文件:
這個(gè)顯現(xiàn)引申出一個(gè)android打包的知識(shí)點(diǎn):aapt[1]過(guò)程中的資源合并[2]。
一句話描述這個(gè)知識(shí)點(diǎn):不同module之間的重復(fù)的資源會(huì)按優(yōu)先級(jí)的進(jìn)行合并覆蓋。這個(gè)流程引發(fā)的問(wèn)題,很多老司機(jī)都遇到過(guò),資源被覆蓋了,我們引用的資源永遠(yuǎn)會(huì)被指向唯一的res。這肯定是不符合預(yù)期的。
因此諸如給資源名加前綴的方案便應(yīng)運(yùn)而生。
為什么不是final
這里咱們聊一個(gè)問(wèn)題:常量有什么特別之處?下面的代碼,編譯之后就是能看到常量的特別之處:
- class TestFinal {
- static final int sInt = 1;
- void testFinal(){
- int temp = sInt;
- System.out.println(temp);
- }
- }
編譯后的代碼會(huì)是這樣:
- public void testFinal(){
- System.out.println(1);
- }
會(huì)發(fā)現(xiàn)編譯器的優(yōu)化,會(huì)把常量直接內(nèi)聯(lián)到代碼引用之處。那么咱們想想:如果library里的res也是常量會(huì)出現(xiàn)什么問(wèn)題?
常量被內(nèi)聯(lián),一旦發(fā)生項(xiàng)目中資源重復(fù),打包過(guò)程中就出現(xiàn)覆蓋,那么內(nèi)聯(lián)的常量已經(jīng)不能映射到真正的資源上了,畢竟資源已經(jīng)被覆蓋。
也就是會(huì)出現(xiàn):資源找不到的crash
不是final引發(fā)的問(wèn)題
library中的R引用不是常量,就意味著這種用法是不能工作的:
可以看到,注解也是要常量的,所以這個(gè)問(wèn)題對(duì)我們?nèi)粘S绊戇€是挺大的...等等!Butterknife就是注解的這種用法,為什么沒(méi)有問(wèn)題??
深入了解過(guò)Butterknife的同學(xué)應(yīng)該知道,Butterknife針對(duì)這種情況進(jìn)行了特殊處理:
Butterknife的方案
Butterknife為了不讓注解處出現(xiàn)語(yǔ)法錯(cuò)誤,自己創(chuàng)造了一個(gè)叫做R2的類。這個(gè)類其實(shí)就是原樣copy了R,唯一不同就是R2都是常量。
的確這樣不會(huì)有語(yǔ)法錯(cuò)誤,但是咱們剛才也分析了:常量?jī)?nèi)聯(lián),資源覆蓋。所以一旦滿足case,那就是crash。所以Butterknife又是如何規(guī)避這個(gè)問(wèn)題的呢?
看過(guò)Butterknife中findViewById()源碼的同學(xué)應(yīng)該都知道,此處Butterknife的實(shí)現(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最終打進(jìn)包里的代碼,并沒(méi)有發(fā)生常量?jī)?nèi)聯(lián)!所以它是怎么做的呢?
看到這里的同學(xué),不妨停下來(lái)想想,如果是你會(huì)怎么解決這個(gè)問(wèn)題?這里我說(shuō)說(shuō)我能想到的方案:
ASM階段,把內(nèi)聯(lián)的代碼,再給它改寫成R的正常引用。問(wèn)題就來(lái)了:ASM的輸入是class,這個(gè)時(shí)機(jī)我沒(méi)辦法再拿到R的正常引用了。
那如果繼續(xù)提前這個(gè)干預(yù)的過(guò)程,放到APT階段呢?試了一下,也沒(méi)有搞定。APT階段拿到的注解value也已經(jīng)是被內(nèi)聯(lián)的常量了...
這就有點(diǎn)奇怪了,Butterknife是如何做到通過(guò)內(nèi)聯(lián)的常量和R引用的映射呢?翻看了Butterknife的源碼,發(fā)現(xiàn)Butterknife是在APT階段執(zhí)行的,關(guān)鍵類在ButterKnifeProcessor[3]。
Butterknife通過(guò)JCTree這個(gè)api拿到了R的引用,然后把內(nèi)聯(lián)的代碼又改回了R的引用。具體的api實(shí)現(xiàn)咱們就不看了,有興趣的同學(xué)可以自行g(shù)ithub。
咱們接下來(lái)聊一聊這個(gè)JCTree是干啥的?
二、編譯原理
我們都知道:日常我們寫下的代碼,最終想要運(yùn)行在目標(biāo)機(jī)器上都需要編譯成目標(biāo)機(jī)器能夠識(shí)別的機(jī)器碼。而做這些工作的我們稱之為編譯器。一般編譯器就是干了如下的事情:
圖片來(lái)自《編譯原理》第二版
在各種源碼編譯的實(shí)現(xiàn)中,基本都不約而同地抽象出一個(gè)概念:抽象語(yǔ)法樹(AST),以求在整個(gè)編譯實(shí)現(xiàn)過(guò)程更加的方便。
一句話解釋抽象語(yǔ)法樹:源代碼語(yǔ)法結(jié)構(gòu)的一種抽象表示。它以樹狀的形式表現(xiàn)編程語(yǔ)言的語(yǔ)法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。
咱們粗略了解了編譯器的的實(shí)現(xiàn)流程,那么編譯器又是怎么實(shí)現(xiàn)的呢?當(dāng)然是用代碼實(shí)現(xiàn)的咯,而且它們的實(shí)現(xiàn)往往離我們很近...以我們java編譯器為例。
入坑Java時(shí),我們應(yīng)該都試過(guò)javac。而這個(gè)命令的實(shí)現(xiàn)在哪?就在JDK里的tools.jar中的com.sun.tools.javac.Main包下。核心邏輯在于com.sun.tools.javac.main.JavaCompiler。
這里邊就實(shí)現(xiàn)了如何分析我們的源碼,如何轉(zhuǎn)化成class。也就上那個(gè)圖中編譯器該干的事。
那么JCTree在整個(gè)編譯過(guò)程中充當(dāng)什么角色呢?一句話:JCTree是對(duì)源碼的一種api級(jí)別的描述?;蛘哒f(shuō)JCTree是java編譯流程中語(yǔ)法樹的實(shí)現(xiàn)。
也就是說(shuō)通過(guò)JCTree相關(guān)api,我們可以訪問(wèn)到源碼結(jié)構(gòu)。說(shuō)起來(lái)似乎很抽象,我們debug個(gè)一段代碼就能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代碼,只需要導(dǎo)入tools.jar就可以快速運(yùn)行,成本非常的低。
三、用代碼run代碼
上述我們通過(guò)JavaCompiler的實(shí)例,對(duì)java源碼進(jìn)行了動(dòng)態(tài)的編譯,拿到的結(jié)果就是這個(gè)java源碼的class文件。有了class文件,我們就可以通過(guò)ClassLoader去加載這個(gè)class。
有了上邊的基礎(chǔ),實(shí)現(xiàn)源碼已經(jīng)不重要,這里貼一個(gè)鏈接大家自取吧:How do you dynamically compile and load external java classes?[4]
尾聲
我個(gè)人沒(méi)有正經(jīng)的學(xué)過(guò)編譯原理,所以了解這部分內(nèi)容時(shí),覺得還是挺神奇的。也希望這篇文章能對(duì)同樣沒(méi)有學(xué)過(guò)編譯原理的同學(xué)帶來(lái)一些思考和啟發(fā)~
References
[1] aapt: https://developer.android.com/studio/command-line/aapt2?hl=zh_cn
[2] 資源合并: https://developer.android.com/studio/write/add-resources?hl=zh-cn#resource_merging
[3] ButterKnifeProcessor: https://github.com/JakeWharton/butterknife/blob/fcdebedf3276096db2f51bf6372b849b5a9c75ed/butterknife-compiler/src/main/java/butterknife/compiler/ButterKnifeProcessor.java#L1470
[4] How do you dynamically compile and load external java classes?: https://stackoverflow.com/questions/21544446/how-do-you-dynamically-compile-and-load-external-java-classes

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流