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

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

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

背景

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

方法論

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

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

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

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

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

問題大概如下:

  • 遍歷工程文件夾速度過慢,耗時(shí)大概1分鐘左右
  • 所有依賴全部切換成源碼之后因?yàn)楣こ烫?,所以展開速度過慢
  • Configuration之后竟然有個(gè)很慢的東西,占據(jù)了大量的耗時(shí)

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

其中我之前在嗶哩嗶哩Android編譯優(yōu)化的獨(dú)立編譯單元中,有介紹過對(duì)于所有依賴全部切換成源碼之后因?yàn)楣こ烫?,所以展開速度過慢的優(yōu)化思路。

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

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

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

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

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

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()}"
}

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

另外在數(shù)據(jù)結(jié)構(gòu)中本身是還有父類,子類對(duì)應(yīng)文件的信息的,但是這部分?jǐn)?shù)據(jù)并沒有辦法進(jìn)行緩存,因?yàn)榫彺嫦聛碇笾匦路葱蛄谢鰜淼木褪切碌囊粋€(gè)對(duì)象。這部分需要我們重新通過自己的遍歷方法,補(bǔ)充這部分?jǐn)?shù)據(jù)機(jī)構(gòu)的關(guān)系。

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

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

最有意思但最難的問題

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

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

所以jetifier在后續(xù)的AGP源碼中就替換了原來的方式,進(jìn)而對(duì)工程內(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; {
}

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

但是也正是因?yàn)門ransformAction寫法,導(dǎo)致了jetifier操作被放在了同步階段完成了。而且因?yàn)槲覀兊膍odule數(shù)量太多以及我們的快編等等,更導(dǎo)致了這個(gè)問題被放大了好幾倍。

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

android.useAndroidX=true
android.enableJetifier=true

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

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

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

hook agp ProjectsServices

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

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

這里因?yàn)槲覀兊牟寮枰袛喈?dāng)前的Project內(nèi)是否存在agp插件,并在他的 afterEvaluate執(zhí)行之前調(diào)用,所以我們選擇了 project.plugins.withType這個(gè)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;()

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

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

allProject{
  apply plguins:"jetifier_closs.class"
}

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

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

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

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

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

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

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

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

總結(jié)

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

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

相關(guān)文章

最新評(píng)論