Kotlin 編譯緩存 Bug

問(wèn)題

項(xiàng)目最近遇到一個(gè)奇怪的問(wèn)題, 設(shè)置了 Log 的開(kāi)關(guān)為 true, 但是實(shí)際上卻不生效, 需要每次 clear 后才會(huì)生效

斷點(diǎn)調(diào)試到對(duì)應(yīng)的地方:

_001.png

此時(shí)通過(guò) Debug 窗口, 查看 ApBuildCofig.LOGCAT_DISPLAY 的值是 true :

_002.png

斷點(diǎn)進(jìn)入 Plog 的方法里, 發(fā)現(xiàn)此時(shí)的值變成了 false :

_003.png

由于項(xiàng)目開(kāi)發(fā)的過(guò)程中, 需要經(jīng)常對(duì)該值進(jìn)行修改, 則每次 clear + build 的時(shí)間, 會(huì)變得很長(zhǎng)

一次完整的編譯大型項(xiàng)目, 時(shí)間可能超過(guò) 10+ 分鐘, 這是完全無(wú)法接受的.

分析

此問(wèn)題是最近才出現(xiàn)的, 之前并沒(méi)有出現(xiàn)過(guò)

考慮是最新修改了 gradle 版本, kotlin 版本, 或者升級(jí)了 IDE 引起的, 或相關(guān)的代碼改動(dòng)引起

需要 clear 才能正常, 不影響完整的編譯打包

說(shuō)明該問(wèn)題和編譯有關(guān), 準(zhǔn)確說(shuō)和編譯緩存有關(guān)系

還原問(wèn)題

此問(wèn)題是最近才出現(xiàn)的, 之前并沒(méi)有出現(xiàn)過(guò)

這個(gè)問(wèn)題比較好解, 查看了最近的 kotlin 版本, 上一次升級(jí)是在兩個(gè)月前, 說(shuō)明不是 kotlin 版本的問(wèn)題.

再看看 gradle 版本也是如此.

IDE 的情況, 自己確實(shí)升級(jí)了最新的 Android Studio 4.1 版本, 不過(guò)有另外同事的 IDE 版本沒(méi)有升級(jí), 也出現(xiàn)了這個(gè)問(wèn)題, 可排除由于編譯版本升級(jí)更新導(dǎo)致的問(wèn)題.

剩下的是改動(dòng)了某段代碼引起的問(wèn)題, 但由于近期修改提交較多, 較難定位, 而且問(wèn)題的表現(xiàn)可能還是和編譯有關(guān), 先看看第二個(gè)問(wèn)題有沒(méi)有結(jié)果, 再反推改動(dòng)的代碼

需要 clear 才能正常, 不影響完整的編譯打包

首先通過(guò) IDE 直接反編譯 kotlin ,得到編譯后的 java 文件:


kotlin_showbyte.jpg
kotlin_decompile.png
public final class MainActivity extends AppCompatActivity {
   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(1300051);
      //可以看到, 編譯后的結(jié)果, 是直接設(shè)置了一個(gè)值, 而不是將 ApBuildCofig.LOGCAT_DISPLAY 傳入
      Plog.setLogcatSwitch(false);
   }
}

到了這一步, 已經(jīng)很好的解釋了文章最開(kāi)頭的問(wèn)題:

ApBuildCofig.LOGCAT_DISPLAY 的值是 true, 但是進(jìn)入的 Plog 里面, 得到的值是 false.

因?yàn)?kotlin 編譯 static final 屬性(即常量) 的時(shí)候, 認(rèn)為此常量的值是不會(huì)變化的, 則直接將常量的值取出來(lái), 不再需要引用該常量.

至此, 問(wèn)題已經(jīng)很清晰了: 應(yīng)該是在編譯 kotlin 的時(shí)候, 對(duì)應(yīng)的 gradle task 認(rèn)為所引用的常量(ApBuildCofig.LOGCAT_DISPLAY)沒(méi)有變化, 則不需要重新編譯當(dāng)前 kotlin 文件, 從而導(dǎo)致 Plog 得到的是一個(gè)舊的值.
而對(duì)于第一個(gè)問(wèn)題也比較清晰了, 改動(dòng)的代碼之前是用 java 語(yǔ)言寫的, 近期才改用 kotlin

測(cè)試還原場(chǎng)景

問(wèn)題雖然已經(jīng)定位清楚, 但是還沒(méi)有找到根本原因, 即:
為什么 kotlin 會(huì)認(rèn)為 ApBuildCofig.LOGCAT_DISPLAY 值沒(méi)有變化, 從而跳過(guò)了重新編譯階段, 直接使用了上一個(gè)的緩存?

相關(guān)的類

為此, 我特地將項(xiàng)目的情況直接用一個(gè) demo 還原. 下面是還原 demo 的文件, 建議直接下載 demo 查看關(guān)系, 或者直接看類關(guān)系圖:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Plog.setLogcatSwitch(AppBuildConfig.LOGCAT_DISPLAY)
    }
}
public class Plog {
    private static boolean logcatSwitch;
    public static void setLogcatSwitch(boolean logcatSwitch) {
        Plog.logcatSwitch = logcatSwitch;
    }
}
public class AppBuildConfig {
    public static final boolean LOGCAT_DISPLAY = BuildConfig.LOG;
}

其中, BuildConfig 這個(gè)類, 是通過(guò) IDE 編譯自動(dòng)生成的:

//自動(dòng)生成的類
public final class BuildConfig {
  //other.....
  // Field from default config.
  public static final boolean LOG = false;
}

gradle 寫入該值:

image

/**
 * 獲取當(dāng)前 Log 開(kāi)關(guān)
 */
private String getCurrentProperties() {
    Properties property = new Properties()
    File propertyFile = new File(rootDir.getAbsolutePath(), "project.properties")
    property.load(propertyFile.newDataInputStream())
    return property.getProperty("log")
}

而對(duì)應(yīng)的 project.properties 是整個(gè)項(xiàng)目的配置文件, 里面的內(nèi)容:

log=false

類關(guān)系圖

類關(guān)系圖.png

其中 MainActivity 是 kotlin 編寫. 根據(jù)上面的分析, 由于 MainActivity.kt 沒(méi)有重新編譯, 導(dǎo)致當(dāng)我們修改 project.properties 的值時(shí), Plog 得到的還是上一次 MainActivity.kt 的編譯值.

查看編譯任務(wù)

為了驗(yàn)證上面的結(jié)論, 修改 project.properties 的內(nèi)容:

log=true

改動(dòng)后, 點(diǎn)擊 Run 運(yùn)行, 查看 Build 窗口:

uptodate.png

可以看到, kotlin 的 task 任務(wù)后面直接顯示: UP-TO-DATE, 即跳過(guò)了編譯, 直接使用緩存.

眾所周知, kotlin 在 1.2.20 的版本后, 開(kāi)始支持 Gradle 的構(gòu)建緩存, 對(duì)應(yīng)的 compileDebugKotlin 這個(gè) task , 會(huì)根據(jù)計(jì)算, 看是否需要跳過(guò)運(yùn)行, 直接使用上一次的編譯結(jié)果.

Gradle 的構(gòu)建緩存規(guī)則, 可直接在看文最后的參考鏈接, 其中有一個(gè)比較重要的規(guī)則, 即: 輸入沒(méi)有變化, 所以 compileDebugKotlin 跳過(guò)了此次任務(wù).

而輸入的內(nèi)容, 也包含很多, 比如 kotlin 文件是否有更改, 路徑有沒(méi)有變化, 以及它關(guān)聯(lián)的類有沒(méi)有變化等等.

導(dǎo)致該 bug 的原因是:

kotlin 文件(Mainactivity.kt) 本身并沒(méi)有變化, 它關(guān)聯(lián)的類 AppBuildConfig 也沒(méi)有變化, 所以 compileDebugKotlin 這個(gè)任務(wù)跳過(guò)了編譯, 直接使用了上一次的編譯結(jié)果, 而 kotlin 在編譯的時(shí)候, 又會(huì)自動(dòng)將常量引用直接替換成值, 所以哪怕 AppBuildConfig 關(guān)聯(lián)的類 BuildConfig 發(fā)生變化了, 但是沒(méi)有影響到 Mainactivity.kt, 從而導(dǎo)致 它傳了一個(gè)錯(cuò)誤的值給 Plog, 這也是為什么 clear 后即可, 因?yàn)?clear 會(huì)將上一次的緩存清理掉.

擴(kuò)展

根據(jù)上面的結(jié)論, 我測(cè)試發(fā)現(xiàn), kotlin → A.常量 → B.常量. 如果修改 B 的常量值, kotlin 的編譯任務(wù)無(wú)法察覺(jué)到此時(shí)輸入已經(jīng)改變了, kotlin 需要重新編譯, 這大概是 kotlin 構(gòu)建緩存的一個(gè) Bug

解決方案

找到了問(wèn)題, 其實(shí)已經(jīng)很好解決, 最好的方式就是讓編譯 kotlin 的任務(wù) compileDebugKotlin 能夠識(shí)別這種變化, 這種需要修改 kotlin 的編譯插件.

方案一

比較簡(jiǎn)單的解決方法是, 直接讓 kotlin 的編譯任務(wù)緩存失效:

this.afterEvaluate { Project project ->
    //獲取編譯 kotlin 的任務(wù)
    def buildTask = project.tasks.getByName('compileDebugKotlin')
     //要求該任務(wù)不可跳過(guò)
    buildTask.outputs.upToDateWhen {
        false
    }
}

上面的方式簡(jiǎn)單粗暴, 但是每次都需要重新編譯 kotlin, 代價(jià)也很高, 特別是當(dāng)項(xiàng)目中的 kotlin 文件較多的時(shí)候, 我們可以監(jiān)聽(tīng)配置文件有沒(méi)有改變, 如果有改變的時(shí)候才強(qiáng)制任務(wù)不可跳過(guò):

this.afterEvaluate { Project project ->
    //獲取編譯 kotlin 的任務(wù)
    def buildTask = project.tasks.getByName('compileDebugKotlin')
    //讀取上一次的值
    def (String logCat, File propertyFile) = getLastProperties()
    //讀取當(dāng)前值
    def currentLog = getCurrentProperties()
    System.out.println("upToDateWhen:" + (logCat == currentLog))
    //對(duì)比這兩個(gè)值是否相等, 如果相等, 允許 UP-TO-DATE, 即允許使用緩存, 跳過(guò) kotlin 編譯
    buildTask.outputs.upToDateWhen {
        logCat == currentLog
    }
    //寫入當(dāng)前的 logcat 值, 供下一次編譯判斷
    propertyFile.write("log=$currentLog")
}

方案二

kotlin 的編譯任務(wù), 之所以使用緩存, 是因?yàn)樗妮斎霑r(shí)一致的, 我們只需要破壞它的輸入即可

有兩個(gè)修改點(diǎn), 一個(gè)是修改編譯后的產(chǎn)物, 直接將 app/build/tmp/kotlin-class 對(duì)應(yīng)的文件刪除, 則 kotlin 會(huì)發(fā)現(xiàn)上一次的產(chǎn)物和存下來(lái)的哈希值不一樣, 則會(huì)自動(dòng)重新編譯整個(gè) kotlin, 但是這種速度較慢, 和上面一個(gè)強(qiáng)制任務(wù)不使用緩存的原理是一樣的

還有一個(gè)修改點(diǎn)是, 直接修改源文件, 在目標(biāo)文件里追加一些注釋, 則 kotlin 認(rèn)為目標(biāo)文件改動(dòng)了, 就僅編譯指定的 kotlin 文件:

this.afterEvaluate { Project project ->
    //讀取上一次的值
    def (String logCat, File propertyFile) = getLastProperties()
    //讀取當(dāng)前值
    def currentLog = getCurrentProperties()
    System.out.println("upToDateWhen:" + (logCat == currentLog))
    
   //第二種方案
    File file = new File(rootDir.getAbsolutePath() + "/app/src/main/java/com/siyehua/kotlincomplierbug", "MainActivity.kt")
    System.out.println("upToDateWhen:" + file.path)

    if (logCat != currentLog && file.exists()) {
        //開(kāi)關(guān)不不一樣, 且緩存存在, 則直接將緩存刪除
        def list = file.text
        if (!list.endsWith("\n/*gradle change file*/")) {
            file.append("\n/*gradle change file*/")
            System.out.println("upToDateWhen:" + "change targe file1")
        } else {
            list = list.replace("\n/*gradle change file*/", "")
            file.write(list.toString())
            System.out.println("upToDateWhen:" + "change cache file2")
        }

    }


    if(logCat != currentLog &&file.exists()){
        //開(kāi)關(guān)不不一樣, 且緩存存在, 則直接將緩存刪除
        file.delete()
        System.out.println("upToDateWhen:" + "delete cache file")
    }   
    //寫入當(dāng)前的 logcat 值, 供下一次編譯判斷
    propertyFile.write("log=$currentLog")
}

方案二的優(yōu)化的速度要比方案一快上不少, 最主要是的是僅編譯目標(biāo) kotlin 文件

工程

https://github.com/siyehua/KotlinCompilerBug

參考資料

kotlin 構(gòu)建緩存特性: https://www.oschina.net/news/92528/kotlin-1-2-20-released

gradle task up-to-date : http://www.reibang.com/p/eb3fb33e4287

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市剑鞍,隨后出現(xiàn)的幾起案子蚁署,更是在濱河造成了極大的恐慌,老刑警劉巖哪痰,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晌杰,死亡現(xiàn)場(chǎng)離奇詭異肋演,居然都是意外死亡爹殊,警方通過(guò)查閱死者的電腦和手機(jī)梗夸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門反症,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)惰帽,“玉大人该酗,你說(shuō)我怎么就攤上這事士嚎。” “怎么了爵嗅?”我有些...
    開(kāi)封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)括细。 經(jīng)常有香客問(wèn)我,道長(zhǎng)锉试,這世上最難降的妖魔是什么呆盖? 我笑而不...
    開(kāi)封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任应又,我火速辦了婚禮丁频,結(jié)果婚禮上席里,老公的妹妹穿的比我還像新娘奖磁。我一直安慰自己咖为,他們只是感情好秕狰,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著躁染,像睡著了一般鸣哀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吞彤,一...
    開(kāi)封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天我衬,我揣著相機(jī)與錄音,去河邊找鬼饰恕。 笑死挠羔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的埋嵌。 我是一名探鬼主播破加,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼雹嗦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了捶惜?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后散劫,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年墓贿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了调炬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片代嗤。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡硝逢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出徽缚,到底是詐尸還是另有隱情似芝,我是刑警寧澤吧恃,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏韭脊。R本人自食惡果不足惜象浑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一蚪拦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧孩擂,春花似錦城须、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)澳骤。三九已至肤京,卻和暖如春饭庞,著一層夾襖步出監(jiān)牢的瞬間熬荆,已是汗流浹背若债。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓惑申,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

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