欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android?Gradle同步優(yōu)化詳解

 更新時間:2022年06月22日 09:24:52   作者:究極逮蝦戶  
這篇文章主要為大家介紹了Android?Gradle同步優(yōu)化示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

背景

年初開始我們就開始了關(guān)于Gradle Sync階段的優(yōu)化。之前和大家都簡單的介紹過工程相關(guān)的背景情況了,我們大概有400+的Module,然后一次的同步時間就非常的慢,我們迫切的需要對這個問題進(jìn)行優(yōu)化。大部分工作都是和團隊內(nèi)的同學(xué)一起完成的,我也只出了一點點力而已。

方法論

很多人聽到方法論三個字,就覺得我要開始pua,說我阿里味,但是我覺得這個查問題的方式可能會對大家有點幫助。

很多人都會有這樣的困擾,給你的一個工作內(nèi)容是一個你完全陌生的東西,第一選擇是逃避然后開始擺爛。我記得前一陣子和一個網(wǎng)友聊天,他有一次面試的時候也問了這樣的問題。這次同步優(yōu)化其實也相似的問題,是一個對我來說相對比較陌生的東西。

我就是想說下我們是如何來拆解這個問題的。首先需要一些對應(yīng)相關(guān)的基礎(chǔ)知識,我去官網(wǎng)查看了些對應(yīng)的文檔資料,仔細(xì)的了解了Gradle生命周期相關(guān)的,看看能不能對我們后續(xù)有所幫助,這個對于后續(xù)優(yōu)化其實是非常重要的。

然后我通過我們的一個monitor插件,我看了大概一個禮拜的同步相關(guān)的編譯日志,發(fā)現(xiàn)了一蛛絲馬跡的。monitor就是一個通過BuildOperationNotificationListenerRegistrar把編譯信息都記錄到一個本地文件夾下的html中,然后把這些信息都發(fā)布都遠(yuǎn)端,方便后續(xù)排查問題。

這個monitor插件我在github上進(jìn)行了一次kotlin翻譯

問題大概如下:

  • 遍歷工程文件夾速度過慢,耗時大概1分鐘左右
  • 所有依賴全部切換成源碼之后因為工程太多,所以展開速度過慢
  • Configuration之后竟然有個很慢的東西,占據(jù)了大量的耗時

這個就是我的方法論,通常碰到一個比較大的問題,我會把一個問題先嘗試拆解成幾個不同的小問題,然后列出一個優(yōu)先級和難易度,之后從易到難的逐步解決問題。一般情況下當(dāng)你的leader發(fā)現(xiàn)問題有緩解之后才會逐步的更多的投入人力資源。而想要一步登天改完所有問題還是有點異想天開的。

其中我之前在嗶哩嗶哩Android編譯優(yōu)化的獨立編譯單元中,有介紹過對于所有依賴全部切換成源碼之后因為工程太多,所以展開速度過慢的優(yōu)化思路。

簡單的說我們將一個的大的工程結(jié)構(gòu)拆分成若干小的而且獨立的部分,然后業(yè)務(wù)同學(xué)在各自小的獨立的編譯單元中進(jìn)行自己的工作流,之后大家不會改動到的模塊就會自動的切換成aar產(chǎn)物,避免了無效工程結(jié)構(gòu)的展開。最后的編譯階段由我們的大的工程結(jié)構(gòu)來進(jìn)行接管,這樣就能同時保證代碼的更快速展開和代碼的穩(wěn)定性了。

數(shù)據(jù)結(jié)構(gòu)緩存

因為工程目錄結(jié)構(gòu)太復(fù)雜了,導(dǎo)致獲取工程模塊數(shù)據(jù)結(jié)構(gòu)的速度偏慢,大概耗時需要1分鐘左右的時間。但是我們認(rèn)為工程結(jié)構(gòu)本身是處于比較穩(wěn)定的狀態(tài),并沒有必要每次都使用文件展開的方式進(jìn)行數(shù)據(jù)結(jié)構(gòu)的生成。

所以打算結(jié)合當(dāng)前的工程分支信息以及各個子git工程的信息等,將這部分?jǐn)?shù)據(jù)緩存復(fù)用,從而繞開這個文件展開過程,已達(dá)到對這部分提速的能力。

因為知道當(dāng)前工程含有幾個git工程,但是并不是所有人都有工程的權(quán)限的,然后會判斷該git工程是否存在,以及文件夾下是否存在有一個settings.gradle或者build.gradle,如果都符合則認(rèn)為該子倉是一個符合標(biāo)準(zhǔn)的工程倉庫,需加入作為緩存唯一key值的計算中,不符合的工程就會跳過。

val rootDir = FileTools.rootProjectDir
val resolves = mutableListOf<XXX>()
val cacheKey by lazy {
    localCacheKey()
}
init {
    resolves.add(rootDir.getLog().resolve())
    allBabels.forEach {
        val file = File(rootDir, it)
        val hasSettings = file.walkTopDown()
            .firstOrNull { walkFile -> walkFile.name == "settings.gradle" || walkFile.name == "build.gradle" } != null
        if (file.exists() && hasSettings) {
            resolves.add(file.getLog().resolve())
        }
    }
}
private fun localCacheKey(): String {
    var key = ""
    resolves.forEach {
        key += it.commitSha + "_"
    }
    val file = rootDir
    return "${GitUtils.currentBranch(file.path).replace("\\/", "_")}_${key.hashCode()}"
}

然后我們在數(shù)據(jù)結(jié)構(gòu)獲取的時候會先判斷本地是否存在改緩存key的文件夾,文件夾下面是否有對應(yīng)的文件,之后基于這個來重新反序列化出對應(yīng)的數(shù)據(jù)結(jié)構(gòu)。如果沒有則按照原來的文件訪問操作進(jìn)行數(shù)據(jù)結(jié)構(gòu)獲取了。

另外在數(shù)據(jù)結(jié)構(gòu)中本身是還有父類,子類對應(yīng)文件的信息的,但是這部分?jǐn)?shù)據(jù)并沒有辦法進(jìn)行緩存,因為緩存下來之后重新反序列化出來的就是新的一個對象。這部分需要我們重新通過自己的遍歷方法,補充這部分?jǐn)?shù)據(jù)機構(gòu)的關(guān)系。

另外的一部分邊界情況就是我們要判斷當(dāng)前的git status中是否存在新增的對應(yīng)的數(shù)據(jù)結(jié)構(gòu)存在,如果有則需要單獨添加一份數(shù)據(jù)結(jié)構(gòu)。因為我們繞開了文件訪問,所以需要對這部分進(jìn)行補充。

從本地測試結(jié)果來看,第一次展開情況下耗時60s時間,如果從緩存內(nèi)讀取則時間壓縮到9s左右就完成數(shù)據(jù)結(jié)構(gòu)還原了。所以這個算是我們加快工程同步速度的第二步了。

最有意思但最難的問題

先說結(jié)論,我們發(fā)現(xiàn)同步階段的后期耗時是android jetifier,會在aar或者jar資源下載完畢之后會執(zhí)行jetifier的清洗androidx的操作。

為什么jetifier會選擇在這個時機,而不是在打包流程進(jìn)行對應(yīng)的替換呢?其實在于他們并不僅僅要完成字節(jié)碼上的轉(zhuǎn)化操作,另外還要對資源文件也進(jìn)行同樣的清洗,比如layout文件中的。

所以jetifier在后續(xù)的AGP源碼中就替換了原來的方式,進(jìn)而對工程內(nèi)所有的aar和jar產(chǎn)物進(jìn)行替換操作,也就是Gradle官方提供的TransformAction相關(guān)的api。

官方文檔 As described in different kinds of configurations, there may be different variants for the same dependency. For example, an external Maven dependency has a variant which should be used when compiling against the dependency (java-api), and a variant for running an application which uses the dependency (java-runtime). A project dependency has even more variants, for example the classes of the project which are used for compilation are available as classes directories (org.gradle.usage=java-api, org.gradle.libraryelements=classes) or as JARs (org.gradle.usage=java-api, org.gradle.libraryelements=jar).

@CacheableTransform
abstract class JetifyTransform : TransformAction&lt;JetifyTransform.Parameters&gt; {
}

這個是從agp源碼中摳出來的,我看了下4.0.0和7.0+版本的agp,都已經(jīng)是TransformAction寫法了。另外沒有掃描前是不確定當(dāng)前輸入aar或者jar是否含有非androidx的代碼的,就需要對所有的aar和jar進(jìn)行一次掃描,之后重新生成一個新的aar或者jar。

但是也正是因為TransformAction寫法,導(dǎo)致了jetifier操作被放在了同步階段完成了。而且因為我們的module數(shù)量太多以及我們的快編等等,更導(dǎo)致了這個問題被放大了好幾倍。

動態(tài)修改gradle配置

android.useAndroidX=true
android.enableJetifier=true

因為jetifier的開關(guān)設(shè)置在gradle.properties中,所以我們打算在插件內(nèi)判斷是否是同步操作,如果是同步則主動關(guān)閉jetifier,從而繞開TransformAction的耗時。

我嘗試通過添加android.enableJetifier=false和android.useAndroidX=false參數(shù)到gradle.startParameter.projectProperties或者gradle.startParameter.systemPropertiesArgs中去,這兩個配置是gradle的全局配置參數(shù)。

但是嘗試重新通過setProjectProperties和setSystemPropertiesArgs函數(shù)去重新賦值,但是測試下來發(fā)現(xiàn)沒有生效。這個值已經(jīng)在內(nèi)存中被Gradle持有,重新設(shè)置是無效的。然后我們嘗試了下通過反射去修改這個值,最后發(fā)現(xiàn)個更尷尬的事情,這個值是在AGP內(nèi)通過ProjectsServices來進(jìn)行讀取的,所以我們只能放棄這個方案了。

hook agp ProjectsServices

當(dāng)發(fā)現(xiàn)這個值是在AGP中去進(jìn)行讀取的。后續(xù)就決定從修改AGP的ProjectsServices進(jìn)行入手,從而達(dá)到關(guān)閉jetifier。有了上一次的反射經(jīng)驗,然后我們也順利的沿用到了這次。

因為AGP相關(guān)的時機其實并不是特別靠前,而是在Android插件被執(zhí)行之后的afterEvaluateapi中,所以我們只要在這個執(zhí)行之前通過反射去修改projectServices就行了。

這里因為我們的插件需要判斷當(dāng)前的Project內(nèi)是否存在agp插件,并在他的 afterEvaluate執(zhí)行之前調(diào)用,所以我們選擇了 project.plugins.withType這個api來執(zhí)行。

override fun apply(project: Project) {
       project.plugins.withType(BasePlugin::class.java) {
           val service = it.getProjectService() ?: return@withType
           val service = it.getProjectService() ?: return@withType
val projectOptions = service.projectOptions
val projectOptionsReflect = Reflect.on(projectOptions)
val optionValueReflect = Reflect.onClass(
        "com.android.build.gradle.options.ProjectOptions\$OptionValue",
        projectOptions.javaClass.classLoader
)
val defaultProvider = DefaultProvider() { false }
val optionValueObj = optionValueReflect.create(projectOptions, BooleanOption.ENABLE_JETIFIER).get&lt;Any&gt;()
Reflect.on(optionValueObj)
        .set("valueForUseAtConfiguration", defaultProvider)
        .set("valueForUseAtExecution", defaultProvider)
val map = getNewMap(projectOptionsReflect, optionValueObj)
projectOptionsReflect.set("booleanOptionValues", map)
      }
}
private fun BasePlugin&lt;*, *, *&gt;?.getProjectService() =
        Reflect.on(this)
                .field("projectServices")
                .get&lt;ProjectServices?&gt;()

在這個階段上,我們能獲取到getProjectService,然后就可以為所欲為了。雖然聽起來挺離譜的,但是貌似也雀食是可以。

這次我們?nèi)甘吵晒α?,這種方式確實能在同步階段自動的去把jetifier給關(guān)閉掉,然后我們就打算嘗試性的在工程內(nèi)進(jìn)行實驗了。

allProject{
  apply plguins:"jetifier_closs.class"
}

最后我們還是失敗了,以前介紹過項目內(nèi)含有很多個復(fù)合構(gòu)建的項目,然后我們是通過所有子工程apply from根的build.gradle的方式完成這部分配置同步的。但是前面說到j(luò)etifier讀取的時機實在afterEvaluate。但是好巧不巧,這次所有復(fù)合構(gòu)建的工程因為apply from的緣故,導(dǎo)致了時機觸發(fā)都在afterEvaluate,導(dǎo)致了反射修改的值沒有生效。所以我們又失敗了。

方法簽名檢查是否存在support包

最后我們仔細(xì)想了想,這種修改還是太過于黑魔法了,萬一后面AGP有修改我們也要跟隨一起改動。最后決定移除項目內(nèi)所有的support庫,主動關(guān)閉同步和編譯階段的jetifier,這樣既能同時加快打包速度也可以讓同步速度變得更快,一舉兩得。

這次移除操作就大部分是人力堆疊了,通過dependcies把所有依賴了support都進(jìn)行移除,另外比如微博這種jar包內(nèi)的,則采取在一個開啟了jetifier的工程中,先完成轉(zhuǎn)化之后再拿到j(luò)ar包之后二次上傳我們的私有maven,從而完成項目內(nèi)所有庫的support移除。

另外作為一個工程師,我們不能只看到眼前的茍且。移除所有support一時間我們可能可以解決這個問題,但是作為一個巨大無比的工程,你不開啟jetifier的時候,后續(xù)的新增接入的代碼都需要確保剔除了support庫,否則最后上線就是會出各種問題。另外有個小注意的點就是在support整改之后,需要在Configuration的時候去把support的依賴全部進(jìn)行移除。這樣就能保證以后所有的support包就算新增了也不會被帶到apk中。

allprojects {
    configurations.all { Configuration c ->
        if (c.state == Configuration.State.UNRESOLVED) {
            exclude group: 'androidx.lifecycle', module: "lifecycle-extensions"
        }
    }
}

項目需要一個長期有效的手段去確定新增的依賴庫已經(jīng)沒有用到support。最后采取了之前說的方法簽名驗證,因為已經(jīng)移除了所有support庫,所以最后apk產(chǎn)物內(nèi)必然是缺失對應(yīng)的依賴的,這樣在方法簽名校驗的過程中就會出現(xiàn)異常。我們的A8檢查會加載android.jar以及所有的dex文件,如果調(diào)用的方法找不到的情況下則會報錯。這樣就能確保后續(xù)引入的新的aar或者jar中如果調(diào)用了support則無法完成代碼合入。

(R8 class check)有興趣的可以看看這部分,我們這部分檢查就是基于R8來完成的。

總結(jié)

之后可能文章更新的頻率估計也就類似現(xiàn)在這樣了呢,大部分時間都是在一個修修補補的狀態(tài),其實挺難做一些0-1的優(yōu)化的,更多的時候是做一些1-100的努力。

看起來本文的內(nèi)容不多,但是其實我們從年初就開始定位問題以及做一些嘗試性的修復(fù)了。發(fā)現(xiàn)問題的時間以及基于工程去解決當(dāng)下的困擾都是挺費時費力的,更多關(guān)于Android Gradle同步優(yōu)化的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論