<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Android編譯提速黑科技—Wade Plugin

          共 3374字,需瀏覽 7分鐘

           ·

          2021-12-17 12:56

          作者:潘濤


          隨著得物App業(yè)務(wù)高速發(fā)展,Android項(xiàng)目的代碼量與組件數(shù)量迅速增加,項(xiàng)目編譯時(shí)長(zhǎng)也明顯升高。今年初增量編譯平均耗時(shí)接近3.9分鐘,嚴(yán)重影響了開(kāi)發(fā)效率,也促使我們探索各種措施縮短編譯時(shí)間提升開(kāi)發(fā)效率。


          背景

          ???

          ????

          四月初通過(guò)一系列常規(guī)優(yōu)化,如改造增量注解處理器、 增量Transform、組件化、工程化、優(yōu)化項(xiàng)目配置等等,耗時(shí)縮短到2.3分鐘。六月,Wade Plugin 第一版上線,進(jìn)一步縮短到1.3分鐘。八月, Wade第二個(gè)大版本上線,最終將增編耗時(shí)降低到了0.8分鐘。本文主要介紹Wade Plugin的技術(shù)原理和實(shí)現(xiàn)思路。


          簡(jiǎn)介

          Wade Plugin是得物Android自研的Gradle插件,用于提升編譯速度。常規(guī)優(yōu)化手段用盡以后, 項(xiàng)目的增編耗時(shí)仍需2.3分鐘, 其中DexArchiveBuilder、MergeProjectDex、 MergeLibDex、MergeExtDex占了1.7分鐘。不難看出,如果要進(jìn)一步降低耗時(shí), 應(yīng)該挖掘DexBuild和DexMerge的優(yōu)化空間。



          Wade Plugin通過(guò)Hook Android原生的編譯流程, 將原生的DexBuildTask替換為WadeDexBuild, 原生的DexMergeTask替換為WadeDexMerge。原生DexBuild平均耗時(shí)60秒, WadeDexBuild只需12秒;?原生DexMerge平均耗時(shí)42秒, WadeDexMerge只需2秒。

          WadeDexBuild?


          原理

          調(diào)用Dx或D8工具完成Class到Dex的轉(zhuǎn)換這一過(guò)程稱為DexConvert, 它占了DexBuild大部分耗時(shí)。原生DexBuild以Jar和Class為粒度執(zhí)行DexConvert。得物工程中平均1個(gè)Jar包含200+個(gè).class, 相當(dāng)于增量時(shí)每改動(dòng)一個(gè)類會(huì)觸發(fā)200個(gè)類執(zhí)行DexConvert。


          理想情況是只有改動(dòng)的Class參與DexConvert.?


          優(yōu)化方案


          在DexConvert執(zhí)行前, 解壓縮Jar, 以.class為粒度執(zhí)行DexConvert。并且只有其中變更的.class參與, 未變更的.class執(zhí)行結(jié)果復(fù)用上次編譯緩存。具體有四種變更類型對(duì)應(yīng)的緩存復(fù)用策略: 對(duì)于新增的.class, 需要參與DexConvert;改動(dòng)的.class參與DexConvert;移除的.class不參與DexConvert;未改變的也不參與。



          其中, 移除.class的情況要特殊處理. 例如Demo.class移除后, 除了相應(yīng)刪除產(chǎn)物Demo.dex, 還要尋找它的內(nèi)部類的產(chǎn)物Demo$1.dex, Demo$2.dex等。


          主要實(shí)現(xiàn)?


          輸入(Task Inputs)?



          首先要根據(jù)Consumer Transform決定參與WadeDexBuild的.class文件路徑, 消費(fèi)Transform Inputs的Transform即Consumer Transform。當(dāng)接入了一個(gè)Consumer Transform, 它的輸出路徑參與編譯; 沒(méi)有Consumer Transform時(shí), Java Compile、Kotlin Compile的輸出路徑參與編譯; 如有多個(gè)Consumer Transforms, 取最后一個(gè)Transform的輸出路徑作為DexBuild輸入。


          增量編譯觸發(fā)條件

          觸發(fā)條件決定了本次編譯是否走增量邏輯, 以及上次編譯的緩存是否可用。WadeDexBuild的增量條件包括五大類共28條(AGP3和AGP4略有不同):


          • Gradle配置

          1. AndroidJarClasspath

          2. DesugaringClasspathClasses

          1. ErrorFormatMode

          2. MinSdkVersion

          1. Dexer

          2. UseGradleWorkers

          1. InBufferSize

          2. Debuggable

          1. Java8LangSupportType

          2. ProjectVariant

          1. NumberOfBuckets

          2. DxNoOptimizeFlagPresent?

          • Wade配置

          1. WadeExtension.scope

          2. WadeExtension.duplicateClass

          1. WadeExtension.dexBucketSize

          2. WadeExtension.jarBucketSize

          • Wade緩存

          1. ProjectWorkspaceDir

          2. SubProjectWorkspaceDir

          1. ExternalLibWorkspaceDir

          2. MixedScopeWorkspaceDir

          • 輸入文件

          1. ProjectClasses

          2. SubProjectClasses

          1. ExternalLibClasses

          2. MixedScopeClasses

          • 產(chǎn)物文件

          1. ProjectOutputDex

          2. SubProjectOutputDex

          1. ExternalLibOutputDex

          2. MixedScopeOutputDex?


          其中Gradle配置相關(guān)的條件和原生的觸發(fā)條件相似。
          觸發(fā)全量編譯的情況, 例如Gradle配置中的Dexer由D8 Dexer改為DX Dexer, 上次編譯緩存肯定無(wú)法復(fù)用, 需要重新完整編譯。
          觸發(fā)增量編譯的情況, 例如修改了一個(gè)Kotlin類, 導(dǎo)致輸入文件中的MixedScopeClasses有變化, 此時(shí)編譯緩存應(yīng)可復(fù)用, 則觸發(fā)增量編譯。

          Dex Convert?

          WadeDexBuild關(guān)鍵步驟是將原生Dex Convert由Jar為粒度轉(zhuǎn)換為Class為粒度執(zhí)行。首先解壓縮Jar, 解壓后的.class寫入緩存目錄, 再將參與上次編譯的Class與參與本次編譯的Class文件逐個(gè)對(duì)比, 只有新增和變更的Class參與Dex Convert, 移除和未改變的直接刪除或沿用對(duì)應(yīng)緩存。


          性能優(yōu)化


          Dex Convert粒度由Jar轉(zhuǎn)換為Class后耗時(shí)明顯降低。但項(xiàng)目中共有423個(gè)Jar, 解壓后83000+個(gè)Class, 導(dǎo)致Dex Convert前解壓縮和文件對(duì)比兩個(gè)步驟非常耗時(shí)。對(duì)這兩步的優(yōu)化主要有三方面。

          • ForkJoinPool

          用ForkJoinPool替代傳統(tǒng)的ExecutorService做并發(fā), 因?yàn)樗腤ork Steeling算法特別適合小文件, 任務(wù)數(shù)特別多的場(chǎng)景, 能夠最大化利用CPU空閑時(shí)間。

          • mmap

          文件對(duì)比是I/O密集型任務(wù), 普通文件流的讀寫速度較慢。Wade Plugin所有I/O操作都用mmap實(shí)現(xiàn), 包括讀、寫、拷貝等。文件流替換為mmap對(duì)整體速度提升有很明顯的效果。

          • CRC-32代替MD5

          對(duì)比兩文件是否相同的常規(guī)做法是先比較文件長(zhǎng)度, 再校驗(yàn)文件MD5是否一致。由于Class數(shù)量太多, 計(jì)算MD5的耗時(shí)非??捎^。用CRC-32算法計(jì)算文件Hash, 作為Checksum來(lái)代替MD5能減少文件對(duì)比的時(shí)間。

          CRC-32計(jì)算的Checksum可靠性不如MD5, 理論上會(huì)有Hash碰撞, 導(dǎo)致修改Class修改后被誤判為未修改, 接著使用緩存而非最新文件參與編譯, 反映到產(chǎn)物APK上意味著這次修改無(wú)效。但是實(shí)際發(fā)生概率極低, 整體來(lái)看值得犧牲理論上的正確性來(lái)保證每次編譯的效率。


          • 優(yōu)化效果

          優(yōu)化后解壓縮、寫緩存平均耗時(shí)5700ms, 文件對(duì)比耗時(shí)得益于CRC-32算法只需10ms, DexBuild整體耗時(shí)從原生的60秒降低到12秒。



          WadeDexMerge?

          優(yōu)化方案


          DexMerge通過(guò)合并.dex文件來(lái)降低APK內(nèi)Dex文件數(shù)量和體積, 提升安裝速度和首次運(yùn)行速度。原生DexMerge的缺點(diǎn)是不支持增量編譯, 耗時(shí)和Dex文件數(shù)量成正比, 得物項(xiàng)目的DexMerge耗時(shí)在30~60秒之間。


          對(duì)于代碼量少, 類總數(shù)不多的項(xiàng)目可以不執(zhí)行DexMerge。AGP本身也有自動(dòng)跳過(guò)DexMergingTask的邏輯, 當(dāng)MinSDKVersion>23時(shí), Dex數(shù)量小于500個(gè)不會(huì)執(zhí)行DexMerge, MinSDKVersion<23時(shí), Dex數(shù)量小于50個(gè)則自動(dòng)跳過(guò)DexMerge。


          Hook DexMergingTask可以做到忽略AGP的Dex數(shù)量閾值強(qiáng)行跳過(guò)DexMerge。但對(duì)于Dex數(shù)非常多的工程, 強(qiáng)行跳過(guò)DexMerge的副作用明顯, 在得物App上強(qiáng)行跳過(guò)會(huì)導(dǎo)致包體積增加40M左右、安裝APK耗時(shí)增加15秒、首次啟動(dòng)耗時(shí)增加約10秒。


          WadeDexMerge支持了強(qiáng)行跳過(guò)DexMerge與增量Merge兩種策略, 默認(rèn)使用增量Merge。跳過(guò)DexMerge的實(shí)現(xiàn)比較簡(jiǎn)單, 只需注意隨后的PackageTask只識(shí)別.dex, 而不能識(shí)別.jar, 要先處理DexBuild產(chǎn)物中的.jar文件, 再和.dex產(chǎn)物一起拷貝到PackageTask的inputDir即可, 其中inputDir可以通過(guò)反射PackageAndroidArtifact.getDexFolders()獲得。這里主要介紹WadeDexMerge增量編譯的實(shí)現(xiàn)。

          主要實(shí)現(xiàn)



          DexMerge輸入文件有.jar和.dex, 輸出.dex文件。增量實(shí)現(xiàn)的核心是對(duì)輸入文件作分桶, 只對(duì)變更的桶Merge, 其他桶復(fù)用緩存。



          假設(shè)本次編譯只有Bucket0中一個(gè)文件發(fā)生變更, 其他Bucket均無(wú)變化, 那么只需對(duì)Bucket0做Merge。分桶后, 需要找出本次編譯相比于上次編譯變更了哪些文件以及它們的變更類型。這個(gè)場(chǎng)景類似于經(jīng)典算法題“如何找出兩個(gè)數(shù)組中不相同的元素?”,因此可以用快慢指針來(lái)計(jì)算文件變更。


          如圖,慢指針指向上次編譯的文件數(shù)組, 快指針指向本次編譯的文件數(shù)組, 對(duì)比兩個(gè)指針的文件, 如果相同則快指針指向下一個(gè)文件, 直到找到不同, 此時(shí)慢指針指向下一個(gè)文件, 再開(kāi)始下一輪對(duì)比。偽代碼如下:

          long fast = 0long slow = 0while (slow < prev.size()) {    long temp = fast    while (temp < curr.size()) {        if (prev[slow] == curr[temp]) {            break        }        temp++    }    if (temp != curr.size()) {        fast = temp        boolean isModified = isModified(prev[slow], curr[fast], reuseScope)        if (isModified) { //found difference            fileChanges.add(new DefaultFileChange(prev[slow], ChangeType.MODIFIED))        }    } else {//not found        fileChanges.add(new DefaultFileChange(prev[slow], ChangeType.REMOVED))    }    slow++}


          桶總數(shù)和桶內(nèi)文件數(shù)(Bucket Size)直接影響到增量效果。理論上, 分桶越多越好, 如果有100個(gè)Bucket, 相當(dāng)于增量只需1/100的全量Merge時(shí)間。但Bucket越多意味著APK內(nèi).dex越多, 又會(huì)影響到包體積、安裝時(shí)間和首次啟動(dòng)耗時(shí)。經(jīng)過(guò)多次試驗(yàn), Bucket總數(shù)在50~100個(gè)時(shí)綜合效果最好, Merge耗時(shí)降低明顯, 副作用也不大。目前得物工程中共有66個(gè)Bucket, 其中Jar類型23個(gè), Dex類型43個(gè)。


          高可用


          在高可用建設(shè)方面, 主要通過(guò)數(shù)據(jù)統(tǒng)計(jì)、建立編譯情況監(jiān)控、編譯指標(biāo)周報(bào)及時(shí)獲取大盤情況和發(fā)現(xiàn)問(wèn)題; 兼容不同AGP和Gradle版本以提高插件的兼容性; 持續(xù)監(jiān)控編譯異常并迭代修復(fù)問(wèn)題提高穩(wěn)定性。


          七大指標(biāo)




          七個(gè)指標(biāo)反映團(tuán)隊(duì)的編譯總體情況:

          • 增量編譯耗時(shí)

          • 平均編譯耗時(shí)

          • 全量編譯耗時(shí)

          • 增量編譯耗時(shí)50分位值

          • 增量占比

          • 編譯成功率

          • 人均編譯總時(shí)長(zhǎng)

          指標(biāo)的計(jì)算依賴埋點(diǎn)數(shù)據(jù)上報(bào), 埋點(diǎn)中部分字段的值較難獲取。例如本次編譯的JavaCompileTask是否為增量, 需通過(guò)對(duì)AGP和Gradle插樁實(shí)現(xiàn), 有三處Hook點(diǎn)可以切入。

          Wade早期版本使用方案一, 實(shí)際使用發(fā)現(xiàn)Hook Gradle的類兼容性較差。目前使用方案二, Hook AGP的com.android.build.gradle.tasks.JavaCompileCreationAction類, 注入WadeJavaCompile類代替原生的org.gradle.api.tasks.compile.JavaCompile類。WadeJavacCompileJavaCompile的包裝類, 重寫compile()取到Javac的增量標(biāo)識(shí)inputs.isIncremental. 偽代碼如下:?


          public class WadeJavaCompile extends JavaCompile {    ...    private static File mFile;
          @Override protected void compile(IncrementalTaskInputs inputs) { ... boolean isIncremental = inputs.isIncremental(); try { FileUtils.writeStringToFile(mFile, "isIncremental:" + isIncremental + "\n", true); } catch (IOException e) { ... } super.compile(inputs); }
          ...}


          對(duì)AGP原生類的Hook過(guò)程大致可分為3步, 獲取Gradle的VisitableURLClassLoader, 用ASM或Javassist編輯目標(biāo)類的字節(jié)碼, 反射調(diào)用ClassLoader.defineClass()加載編輯后的字節(jié)碼。


          Gradle進(jìn)程和Gradle Daemon進(jìn)程一般常駐后臺(tái), Android Studio打開(kāi)后第一次編譯會(huì)觸發(fā)加載AGP類的字節(jié)碼, 之后再編譯都不會(huì)觸發(fā)類加載, 所以只有一次Hook機(jī)會(huì), 必須保證Hook的字節(jié)碼比AGP"搶先"加載到VisitableURLClassLoader。因此, Wade插件接入要求在Root Project中apply wade plugin, 以確保Hook代碼能在App Project的apply android plugin之前執(zhí)行。


          兼容性


          主要兼容了AGP3和AGP4、Gradle5和Gradle6兩套版本。


          插件中的關(guān)鍵步驟如增量編譯觸發(fā)條件、反射獲取Consumer Transform、WadeDexMergeTask等都針對(duì)不同版本分別做了適配。


          穩(wěn)定性


          實(shí)際使用過(guò)程中遇到了各種疑難雜癥, 這里列出前10個(gè)常見(jiàn)異常。

          1. java.io.IOException: The input doesn't contain any classes. Did you specify the proper '-injars' options?

          2. java.io.FileNotFoundException: /Users/panes/app/build/intermediates/compile_and_runtime_not_namespaced_r_class_jar/debug/R.jar (No such file or directory)

          1. Caused by: com.android.tools.r8.utils.b: Error:YeezyCompleteListener.class, Type com.xxx is defined multiple times

          2. Caused by: org.gradle.api.UncheckedIOException:java.util.zip.ZipException: error in opening zip file

          1. Caused by: com.android.tools.r8.utils.b: Error: Class content provided for type descriptor xxx.r actually defines class com.xxx.R

          2. A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade

          1. com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: Type com.xxx.R is defined multiple times

          2. base.apk code is missing

          1. Archive is not readable : /Users/panes/android/app/build/intermediates/mixed_scope_dex_archive/developerDebug/out/c6795cc73f81ff9c1c0b5d0adb06b1b4161c540cbf761ba11415aae4856b11b4_4.jar

          2. Could not determine dependencies of app:wadeInputChangesInspect

          經(jīng)過(guò)近30個(gè)版本的迭代, 這些問(wèn)題都已解決。最近版本v2.6.4上線至今經(jīng)歷6800次編譯, 異常次數(shù)4次。


          基準(zhǔn)測(cè)試?


          Benchmark跑分顯示, 10次增量編譯(只改動(dòng)一行代碼)的平均耗時(shí)14.4秒, 10次無(wú)量編譯(代碼不變)平均耗時(shí)6.2秒。跑分時(shí)清理后臺(tái)任務(wù)、關(guān)閉了其他占用資源的進(jìn)程, 但實(shí)際編譯環(huán)境比理想環(huán)境復(fù)雜得多, 基準(zhǔn)測(cè)試只用于驗(yàn)證理論是否有效。


          總結(jié)

          Wade Plugin開(kāi)發(fā)過(guò)程中困難重重, 重寫Android原生的編譯流程做到既大幅提升速度又保證穩(wěn)定可靠并非易事。其中還有更多細(xì)節(jié)未介紹到, 如增編時(shí)識(shí)別熱點(diǎn)代碼、復(fù)用文件變更計(jì)算結(jié)果、Hook PackageTask做Apk內(nèi)文件兜底防止出包異常。同時(shí)也期待后續(xù)版本能有更多提升。




          ? 耗時(shí)2年,Android進(jìn)階三部曲第三部《Android進(jìn)階指北》出版!

          ? 『BATcoder』做了多年安卓還沒(méi)編譯過(guò)源碼?一個(gè)視頻帶你玩轉(zhuǎn)!

          ? 『BATcoder』我去!安裝Ubuntu還有坑?

          ? 重生!進(jìn)階三部曲第一部《Android進(jìn)階之光》第2版 出版!

          ?BATcoder技術(shù)群,讓一部分人先進(jìn)大廠

          大家,我是劉望舒,騰訊最具價(jià)值專家TVP,著有三本業(yè)內(nèi)知名暢銷書,連續(xù)四年蟬聯(lián)電子工業(yè)出版社年度優(yōu)秀作者,百度百科收錄的資深技術(shù)專家。


          想要加入?BATcoder技術(shù)群,公號(hào)回復(fù)BAT?即可。

          為了防止失聯(lián),歡迎關(guān)注我的小號(hào)

          ??微信改了推送機(jī)制,真愛(ài)請(qǐng)星標(biāo)本公號(hào)??
          瀏覽 85
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  2019天天操夜夜操 | 精品久久久久久中文字幕无码专区 | 乱伦av导航| 美日韩三级片 | 东京热视频专区 |