今日頭條 Android '秒' 級編譯速度優化

背景介紹

Android 項目一般使用 gradle 作爲構建打包工具,而其執行速度慢也一直爲人所詬病,對於今日頭條 Android 項目這種千萬行級別的大型工程來說,全量編譯一次的時間可能高達六七分鐘,在某些需要快速驗證功能的場景,改動一行代碼的增量編譯甚至也需要等兩三分鐘,這般龜速嚴重影響了開發體驗與效率,因此針對 gradle 編譯構建耗時進行優化顯得尤爲重要。

在今日頭條 Android 項目上,編譯構建速度的優化和惡化一直在交替執行,18 年時由於模塊化拆分等影響,clean build 一次的耗時達到了頂峯 7 分 30s 左右,相關同學通過模塊 aar 化,maven 代理加速,以及增量 java 編譯等優化手段,將 clean build 耗時優化到 4 分鐘,增量編譯優化到 20~30s 。但是後面隨着 kotlin 的大規模使用,自定義 transform 以及 apt 庫氾濫,又將增量編譯速度拖慢到 2 分 30s ,且有進一步惡化的趨勢。爲了優化現有不合理的編譯耗時,防止進一步的惡化,最近的 5,6 雙月又針對編譯耗時做了一些列專項優化(kapt,transform,dexBuilder,build-cache 等) 並添加了相關的防惡化管控方案。從 4.27 截止到 6.29 ,整體的優化效果如下:

歷史優化方案

由於 18 年左右客戶端基礎技術相關同學已經對今日頭條 Android 工程做了許多 gradle 相關的優化,且這些優化是近期優化的基礎,因此先挑選幾個具有代表性的方案進行介紹,作爲下文的背景同步。

maven 代理優化 sync 時間

背景

gradle 工程往往會在 repositories 中添加一些列的 maven 倉庫地址,作爲組件依賴獲取的查找路徑,早期在今日頭條的項目中配置了十幾個 maven 的地址,但是依賴獲取是按照 maven 倉庫配置的順序依次查找的,如果某個組件存在於最後一個倉庫中,那前面的十幾個倉庫得依次發起網絡請求查找,並在網絡請求返回失敗後才查找下一個,如果項目中大多組件都在較後倉庫的位置,累加起來的查找時間就會很長。

優化方案

  1. 使用公司內部搭建的 maven 私服,在私服上設置代理倉庫,爲其他倉庫配置代理(例如 google、jcenter、mavenCentral 等倉庫),代理倉庫創建好後,在 Negative Cache 配置項中關閉其 cache 開關:如果查找時沒有找到某版本依賴庫時會緩存失敗結果,一段時間內不會重新去 maven 倉庫查找對應依賴庫,即使 maven 倉庫中已經有該版本的依賴庫,查找時仍然返回失敗的結果。
  2. 建立倉庫組,將所有倉庫歸放到一個統一的倉庫組裏,依賴查找時只需要去這個組倉庫中查找,這樣能大大降低多次發起網絡請求遍歷倉庫的耗時。

模塊 aar 化

背景

今日頭條項目進行了多次組件化和模塊化的重構,分拆出了 200 多個子模塊,這些子模塊如果全都 include 進項目,那麼在 clean build 的時候,所有子模塊的代碼需要重新編譯,而對於大多數開發人員來說,基本上只關心自己負責的少數幾個模塊,根本不需要改動其他模塊的代碼,這些其他 project 的配置和編譯時間就成爲了不必要的代價。

優化方案

對於以上子模塊過多的解決方案是:將所有模塊發佈成 aar ,在項目中全部默認通過 maven 依賴這些編譯好的組件,而在需要修改某個模塊時,通過配置項將該模塊的依賴形式改爲源碼依賴,做到在編譯時只編譯改動的模塊。但是這樣做會導致模塊漸漸的又全部變爲源碼依賴的形式,除非規定每次修改完對應模塊後,開發人員自己手動將模塊發佈成 aar ,並改回依賴形式。這種嚴重依賴開發人員自覺,並且在模塊數量多依賴關係複雜的時候會顯得異常繁瑣,因此爲了開發階段的便利,設計了一整套更完整細緻的方案:

  1. 開發時,從主分支拉取的代碼一定是全 aar 依賴的,除了 app 模塊沒有任何子模塊是源碼引入。
  2. 需要修改對應模塊時,通過修改 local.properties 裏的 INCLUDES 參數指定源碼引入的模塊。
  3. 開發完成後,push 代碼至遠端,觸發代碼合併流程後,在 ci 預編譯過程與合碼目標分支對比,檢測修改的模塊,將這些模塊按照依賴關係依次發佈成 aar ,並在工程中修改依賴爲新版本的 aar, 這一步保證了每次代碼合入完成後,主分支上的依賴都是全 aar 依賴的。

收益

通過上述改造,將源碼模塊切換成 aar 依賴後,clean build 耗時從 7,8 分鐘降低至 4,5 分鐘,收益接近 50%,效果顯著。

增量 java/kotlin 編譯

背景

在非 clean build 的情況下,更改 java/kotlin 代碼雖然會做增量編譯,但是爲了絕對的正確性,gradle 會根據一些列依賴關係計算,選擇需要重新編譯的代碼,這個計算粒度比較粗,稍微改動一個類的代碼,就可能導致大量代碼重新執行 apt, 編譯等流程。

由於 gradle 作爲通用框架,其設計的基本原則是絕對的正確,因此很容易導致增量編譯失效,在實際開發中,爲了快速編譯展示結果,可以在編譯正確性和編譯速度上做一個折中的方案:

  1. 禁用原始的 javac/kotlinCompile 等 task, 自行實現代碼增量修改判斷,只編譯修改的代碼。
  2. 動態禁用 kapt 相關的 task, 降低 kapt,kaptGenerateStub 等 task 的耗時。

以上方案(下文全部簡稱爲 fastbuild) 雖然在涉及常量修改,方法簽名變更方面 存在一定的問題(常量內聯等),但是能換來增量編譯從 2 分多降低至 20~30s,極大的提升編譯效率,且有問題的場景並不常見,因此整體上該方案是利大於弊的。

編譯耗時惡化

通過上文介紹的幾個優化方案和其他優化方式,在 18 年時,今日頭條 Android 項目的整體編譯速度(clean build 4~5min, fast 增量編譯 20~30s)在同量級的大型工程中來說是比較快的 ,然而後期隨着業務發展的需求,編譯腳本添加了很多新的邏輯:

  1. kotlin 大規模使用,kapt 新增了很多註解處理邏輯。
  2. 引入對 java8 語法的支持 , java8 語法的 desugar(脫糖)操作增加了編譯耗時。
  3. 大量的字節碼插樁需求,添加了許多 transform ,大幅度提升了增量編譯耗時。

這些邏輯的引入,使得增量編譯耗時惡化到 2 分 30s,即使採用 fastbuild,改動一行代碼編譯也需要 1 分 30s 之多,開發體驗非常差。而下文將着重描述最近一段時間對上述問題的優化過程。

近期優化方案

app 殼模塊 kapt 優化

背景

今日頭條工程經過多次模塊化,組件化重構後, app 模塊(NewsArticle)的大部分代碼都已經遷移到子模塊(上文已經介紹過子模塊可以採用 aar 化用於編譯速度優化,app 模塊只剩下一個殼而已。

但是從 build profile 數據(執行 gradle 命令時添加 --profile 參數會在編譯完成後輸出相關 task 耗時的統計文件) 中發現到一個異常 case:明明只有 2 個類的 app 模塊 kapt(annotationProcessor 註解處理) 相關耗時近 1 分鐘。

通過進一步觀察,雖然 app 模塊拆分後只有 2 個簡單類的代碼,但是卻用了 6 種 kapt 庫, 且實際生效的只是其中 ServiceImpl 一個註解 (內部 ServiceManager 框架,用於指示生產 Proxy 類,對模塊之間代碼調用進行解耦)。如此一頓操作猛如虎,每次編譯卻只生成固定的兩個 Proxy 類,與 53s 的高耗時相比,投入產出比極低。

優化方案

把固定生成的 Proxy 類從 generate 目錄移動到 src 目錄,然後禁止 app 模塊中 kapt 相關 task ,並添加相關管控方案(如下圖: 檢測到不合理情況後立刻拋出異常),防止其他人添加新增的 kapt 庫。

收益

  1. 在 mac clean build 中平均有 40s 收益
  2. 在 ci clean build 中平均有 20s 收益

kapt 隔離優化

背景

通過上文介紹在 app 模塊發現的異常的 kapt case, 進而發現在工程中爲了方便,定義了一個 library.gradle ,該文件的作用是定義項目中通用的 Android dsl 配置和共有的基礎依賴,因此項目中所有子模塊均 apply 了這個文件,但是這個文件陸陸續續的被不同的業務添加新的 kapt 註解處理庫,在全源碼編譯時,所有子模塊都得執行 library 模塊中定義的全部 6 個 kapt ,即使該模塊沒有任何註解相關的處理也不例外。

而上述情況的問題在於:相比純 java 模塊的註解處理,kotlin 代碼需要先通過 kaptGenerateStub 將 kt 文件轉換成爲 java ,讓 apt 處理程序能夠統一的面向 java 做註解掃描和處理。但是上面講到其實有很多模塊是根本不會有任何實際 kapt 處理過程的,卻白白的做了一次 kt 轉 java 的操作,源碼引入的模塊越多,這種無意義的耗時累加起來也非常可觀。

爲了能夠弄清楚到底有哪些子模塊真正用到了 kapt ,哪些沒用到可以禁用掉 kapt 相關 task ,對項目中所有子模塊進行了一遍掃描:

  1. 獲取 kapt configuration 的所有依賴,可以得到 kapt 依賴庫的 jar 包,利用 asm 獲取所有 annotation.
  2. 遍歷所有 subproject 的 sourceset 下所有 .java,.kt 源文件,解析 import 信息,看是否有步驟 1 中解析的 annotation
  3. package task 完成後遍歷 所有 subproject 所有 generate/apt ,generate/kapt 目錄下生成的 java 文件

使用上述方案,通過全源碼打包最終掃描出來大概是 70+模塊不會進行任何 kapt 的實際輸出,且將這些不會進行輸出的 kapt,kaptGenerateStub 的 task 耗時累加起來較高 217s (由於 task 併發執行所以實際總時長可能要少一些).

獲取到不實際生成 kapt 內容的模塊後,開始對這些模塊進行細粒度的拆分,讓它們從 apply library.gradle 改爲沒有 kapt 相關的 library-api.gradle ,該文件除了禁用 kapt 外,與 library 邏輯一致。

但是這樣做算是在背後偷偷做了些更改,很可能後續新來的同學不知道有這種優化手段,可能新增了註解後卻沒有任何輸出且找不到原因,而優化效果最好是儘量少給業務同學帶來困擾。爲了避免這種情況,便對這些 library-api 模塊依賴的註解做隔離優化,即:把這些模塊依賴的註解庫全部 自動 exclude 掉,在嘗試使用註解時會因獲取不到引用(如下圖所示),第一時間發現到依賴被移除的問題。

另一方面在編譯出現錯誤時,對應 gradle 插件會自動解析找不到的符號,如果發現該符號是被隔離優化的註解,會提示將 library-api 替換成 library,儘可能降低優化方案對業務的負面影響。

收益

  1. mac 全源碼場景中有 58s 左右的加速收益。
  2. ci 機器上由於 cpu 核數更多 ,task 併發性能更好,只有 10s 左右的收益。

transform 優化

背景

transform 作爲 Android gradle plugin 提供給開發者的 API,用於在 apk 構建過程中,對 class 字節碼,resources 等文件內容進行插樁修改,例如官方的 dex, proguard 等功能均由此 api 實現。

對於今日頭條這種大型工程來說,有很多諸如性能插樁、自動埋點插樁等相關需求,因此基於此 api 開發了大量 transform,用於實現特定功能,但是這些 transform 基本上都是不支持增量編譯的,即使只改動了一行代碼,這 些 transform 都會遍歷所有 class 文件,解析字節碼中的方法字段信息,關鍵是這類 transform 數量有十幾個,將這些遍歷耗時乘以 10 累加之後,增量編譯耗時自然居高不下。

根據分析,其中性能插樁等相關 transform 做的一些面向線上的插樁方案是完全可以只在 release 打包時打開的,因此可以直接在 debug 編譯時禁用這些功能,用於提升開發期間的編譯速度。而剩下的 9 個 transform 特徵比較相似,可能在一些插樁細節上有所不同,它們大致的處理邏輯爲:

  1. 在各個模塊中使用 apt processor 收集模塊 xx 註解的 class 信息然後生成一個 xxCollect 類,該類的作用是收集好 apt 階段解析到的本模塊的類信息
  2. 將所有模塊收集到的信息進行彙總,利用 transform 階段掃描出所有的 xxCollect 信息,通過 javaassit 或者 asm 往一個 xxCollectMgr 的某個 collectXxx 方法插樁注入之前收到的信息
  3. 業務代碼可通過 xxCollectMgr 的 collectXxx 方法獲取到在各個模塊動態生成的所有 xxCollect 信息。(例: 頁面路由相關框架便是通過該邏輯收集到所有子模塊的路由註冊信息)

由於這 9 個自定義 transform 的功能如此類似,便決定將這些 transform 合併成一個,這樣同一個文件的讀寫操作只執行一次,並且可以做定製化的增量編譯優化。雖然公司內有類似的 transform 合併優化方案 byteX ( 已在 github 開源),但是由於今日頭條項目在 debug 階段未開啓該功能,且 ByteX 做了一些諸如 ClassGrapth 的構建,對類文件做兩次遍歷等操作,對於實現類信息收集和信息注入 這個功能來說,byteX 顯得比較重 ,於是仍然針對類信息收集註入功能這個細分場景開發了一個收斂框架。

收益

該框架完成了內部 9 種類信息收集註入相關框架的收斂,編譯耗時的絕對值加速了 25s 左右,且由於提供了統一的增量緩存功能,使得改動一行代碼的耗時可以從 2 分 30s 降低到 35~40s ,實現了增量編譯速度大的飛躍。最關鍵的是將所有自定義 transform 統一管控後,後續可以做統一定製化的需求,進一步優化編譯速度。

dexBuilder 優化

背景

在 Android debug 編譯 過程中,最主要的耗時在 transform 上,而上文 介紹 今日頭條項目自定義 transform 已經被高度優化過,剩下的 dexBuilder(將 class 轉換成 dex ) ,dexMerge 等 task 耗時就成爲了性能瓶頸,dexBuilder 全量編譯耗時 60s 左右,增量編譯耗時 22s 左右。

根據 DexArchiveBuilderTransform 關鍵方法 launchProcessing 裏面關鍵一行 isDirectoryBased,如果是目錄類型的輸入,會根據具體變動 class 文件做增量的 dex 編譯 ,但是如果是 jar 輸入類型,那隻要 jar 裏任何一個類變動,則整個 jar 所有類都需要重執行 dex,但是由於 gradle 的依賴特性,基本上只有 app 模塊是目錄類的輸入,其他 library 都是 jar 輸入類型,對於比較大的業務模塊 ,如果該模塊有幾千個類,那每改動一次類,就會有幾千類連帶重新 執行 dex 編譯。

dexBuilder 增量效果量化

在優化前爲了得到真正的重新執行 dex 編譯的數值,做到最佳優化,設計了一套 hook dex 編譯流程的方法(該方法理論上可以 hook Android gradle plugin 任意類:大致就是 hook classLoader ,提前用 asm 修改 D8DexArchiveBuilder 中的 convert 方法

通過對 D8DexArchiveBuilder 的 hook ,統計到優化前改動一行代碼會連帶着 24968 個類重新執行 dex 編譯,增量效果非常差。

優化方案

既然 jar 輸入相比於 目錄輸入來說增量編譯效果非常差,那麼可以想到 hook TransformInvocation 中的 input 方法,動態將 project 的 jar 類型輸入(JarInput)映射爲一個 目錄輸入(DirectoryInput),那麼子模塊修改對應代碼時,只重新編譯目錄中被修改的 class 爲 dex(而不是原來的整個 jar 內所有 class 重新執行 dex 編譯),整體 dex 重新編譯的數量將大幅度減少。實現具體方案如下:

  • 自動發現源碼依賴的子模塊 project,配置經常需要變更的注入類所在的 SDK jar
  • hook TransformInvocation 的 input 將上面步驟中的 JarInput 映射爲 DirectoryInput
  • 每次 hook input 前檢查與上一次需要優化的 project,sdk 是否一致,否則直接拋異常(影響增量判斷)

而 jar 轉 目錄的映射細節爲:

  • 如果是新增的 jar, 那解壓該 jar 所有類文件到目錄,將該目錄下所有類定義爲 ADD
  • 如果是移除的 jar, 檢查之前解壓的目錄,該目錄下所有類文件定義爲 REMOVE
  • 如果 jar 沒有變更,那定義爲之前解壓的目錄中沒有任何子文件變更 NOT_CHANGE
  • 如果 jar 有修改,需要進一步判斷內容有哪些修改,如果 jar 中有的文件在 解壓目錄不存在,該文件定義爲 ADD,如果目錄有的文件在 jar 中不存在,該文件定義爲 REMOVE,如果都同時存在,比較文件內容(大小,hash) ,相同定義爲 NOT_CHANGED 否則爲 CHANGED

在第一次增量修改完成後,重新執行 dex 編譯的類數量降低至 2152 個,但是其中仍然有很多迷惑的不該執行 dex 編譯的類,預期是修改多少類,就重新執行 多少次 dex,因此繼續對其中原因進行進一步的探索

desugarGraph 異常

由於 java8 的字節碼有些指令在 Android 虛擬機中並不能得到支持,會在編譯流程中,將這些指令進行脫糖,轉換成已有的指令,而 d8 中 desugar 的流程合併到了 dexBuilder 中,爲了避免某些類 desugar 後,依賴它的類的行爲正確,需要把依賴它的所有類重新執行一遍 dex 編譯。

而 d8 會根據 DesugaringGraph 查找 desguar 有變動的類及其依賴的 jar 包,如圖下面獲得到的 addtionalPaths 是 desguar 類可能直接間接相關的 jar 包,即使這些 jar 包沒有任何文件內容變更,其中所有類也得重新全部執行一次 dex 編譯。

DesugaringGraph 邏輯概述

該類用來輔助獲取依賴或間接依賴到變更文件的所有文件,而它的生成邏輯爲: 全量或增量編譯類的時候記錄類型之間的依賴和被依賴關係,依賴關係的判斷條件有

  1. 父類
  2. 直接實現的接口
  3. 調用 dynamic 方法指令時的返回類型

DesugaringGraph 不僅記錄了類依賴的類,和依賴它的類,同時也記錄了一個文件路徑包含了哪些類

  1. 如果文件路徑是 class 文件,那路徑就包含 1 個類
  2. 如果路徑是 jar 文件,包含這個 jar 下所有類。

在增量編譯時檢查到變動的文件時,會檢查這個文件路徑包含的所有類, 然後遞歸查找所有直接/間接依賴它的類,並且找到這些依賴它的類後,會把這個類所在的 jar 包作爲額外的處理類型(即使 jar 本身沒有任何變動,裏面所有的類仍然需要重新 dex 編譯)

順着這個解析關係,找到了一個不正常的 jar 包 bdjson_api ,這個 jar 只有 3 個文件 (IBDJson,BDJsonCollector, BDJsonConstants) 。但是 BDJsonCollector 是一個 stub 類,每次執行 transform 會收集到其他類的信息然後往該類的方法中注入,因此該文件每次編譯時都會變動。這個類本身並沒有多少直接依賴它的類,主要是 它所在的 jar 包還有個 IBDJson 接口。

按照之前的 DesugaringGraph 依賴關係,所有 IBDJson 接口的實現類被判斷爲依賴它,然後這些實現類如果出現在某個 dynamic 方法中,又會被層層查找,查找完了之後,還得計算所有依賴類所在的 jar 包,jar 包中其他沒有依賴它的類也會被重新 dex 編譯, 在這個 case 的依賴查找中,連帶重新執行 dex 編譯的類數量並不多,大概爲 4 個 jar 包共 2000 多個類重新執行了無意義的 dex 流程,但是如果是其他 sdk jar 包,則可能就會給 dexBuilder 增量帶來毀滅性的打擊。上述問題的解決方法:

  1. 把每次都會修改的 Stub 注入類和其他接口類分離,放在不同 jar 包。(需要改造業務,比較麻煩)
  2. 動態把這個 sdk jar 輸入轉換成目錄輸入。(上文介紹的方法,也與上面 jar 轉目錄的初衷相符,只不過是漏掉了這個 case,但是卻意外證明了:除了包含業務代碼多的 project 的 jar 輸入需要轉換爲目錄外,sdk jar 同樣有必要)

修復後修改一行代碼重新執行 dex 的數量爲 10 ,其中 9 個是每次 transform 會修改的 stub 類,1 個是實際修改類。做到了真正的 改多少類,執行多次 dex 編譯。

收益

assemebleDebug 的增量編譯中從原來(上文 transform 優化後)的 35s~40s 是降低至均值 17s,在 fast build 中效果最明顯(屏蔽了 apt),第二次增量編譯能突破到 9s 實現秒級編譯。

而經過上面所有的優化後,耗時數據裏耗時最嚴重的 dexBuilder 和 dex-merge 基本都降低在 1s 左右,自定義 transform 也是 1s 左右,其他 task 基本都是零點幾秒。在不使用 hotfix 方案的情況下(由於今日頭條項目使用了過多的自定義 transform 和插件方案,所以不好使用 instantrun 等 hostfix 方案),相關 task 的耗時基本達到了優化的極限。

build-cache 優化踩坑

Build-cache 是 gralde 提供的一個編譯緩存方案,目的是在構建過程中當兩個 task 的輸入相同時,可以複用緩存內容,直接跳過 task 的執行拿到緩存好的執行結果。由於緩存結果既可以放在本地磁盤,也可以從遠程獲取,因此容易想到利用 ci 提前 構建緩存包,在其他 ci 機器和開發時利用緩存包獲得加速效果。

那麼如何判斷 task 可以直接獲取 之前 task 的緩存內容作爲輸出呢?定義爲可緩存的 task ,會定義一些緩存相關的屬性,task 執行時通過文件指紋,緩存屬性等一大堆屬性計算出緩存 key ,用於查找是否命中緩存,計算維度有:

  • 輸入屬性(如 jvm 參數,sourceCompatibility 等參數)。涉及到 各種 ValueSnapShot(值類型快照,string,file,list,等…)計算。以及 task 實現類 classpath 相關
  • 輸入文件集相關:涉及到 依賴的輸入文件的 hash 計算
  • 輸出屬性相關
  • 不可緩存屬性相關

但是原生的 build-cahce 在緩存命中率上慘不忍睹,公司內抖音團隊基於 gradle4.x 的源碼做過一些提高命中率的修改,不過今日頭條用的 gradle 版本是 5.1 ,受抖音團隊的啓發,也對 gradle5.1 源碼做了些定製化的修改,用於 dump 緩存 key 的計算流程,快速發現緩存問題。相比於抖音發現的一些影響緩存命中的問題,額外發現了一些諸如 mbox , kapt 元素遍歷順序不固定的問題,這裏只挑一個典型的 apt 順序不一致的問題進行介紹:

apt 順序不一致導致的緩存失效問題

經過修改 gradle5.1 源碼後對編譯流程的信息採集,發現有的 task 緩存無法命中是因爲 kapt 時,很多生成代碼塊邏輯是一樣的,但是順序不一樣(如下圖 demo :下面兩個生成方法的邏輯一致,但是判斷順序不一致,這應該是在 processor 中通過 RoundEnviroment 獲取到 註解元素 elemnts 順序不一致導致的 )

其內部的原因可能是文件遍歷目錄時獲取子文件的順序不一致,導致了子文件對應註解元素的順序也不一致, 總之這個操作影響了生成文件內代碼的順序,也影響了該文件的 hash 計算結果,導致 build-cache 在計算 javac task 的 key 時會錯亂導致緩存無法命中。

解決方案

但是注意到 AbstractProcessor 的核心方法 process 的兩個參數都是接口,因此想到可以代理原來的 RoundEnvironment 接口,將其 getElementXx 的方法經過固定排序後返回,使得 apt 註解元素的順序能夠固定下來。

由於篇幅影響,其他影響緩存命中相關的 case 略(主要是一些涉及到文件絕對路徑, classPath 相關的問題)

收益

  1. 由於大多開發場景是引入多少模塊就修改多少模塊內容,很難獲得命中緩存, 收益很小
  2. 主要是全源碼場景能穩定獲得一些編譯加速,基本上在 22~99s 左右。

編譯耗時防惡化管控

在今日頭條這種大型工程中,有很多業務部門參與開發,僅 Android 工程 開發人員就有幾百人且人員變動頻繁,因此內部任何一項優化工作必然是得搭配上一些管控措施的,否則一邊優化一邊惡化,空浪費人力。

爲此制定了一些管控方案,首先是 debug 階段的 新增 transform 管控,設置爲白名單形式,如果在開發階段新增了 transform 直接終止編譯流程,通過說明文檔告知管控的規則,當然,管控的目的是儘可能減少一些不必要的不合理的編譯問題,並不是與業務團隊作對,如果某一個操作拖慢了整體的編譯耗時,但是在 app 性能/穩定性方面有更大收益,且無法在編譯期做更多的優化,仍然是允許添加的,只不過是得提前把這個問題暴露出來而已,能更快的找出更多的解決思路,比如引導使用 byteX 等 transform 收斂方案。

另一方面的是合碼流程方面的阻塞 :今日頭條 爲了保障 app 的性能穩定性,在合碼流程上設置了許多自動化的卡點:如 包大小檢測,插件依賴變更檢查, so 變更檢查,啓動性能檢測等,檢測到對應問題(如包大小增加異常)會阻塞合碼流程。爲了管控編譯速度 ,使其不至於惡化的太快,也加上了對應的 基於 task 級別的管控,當某一個 task 耗時異常波動,或者新增全新類型的 task 時,能夠自動的發現問題,通過機器人將相關人員拉到 mr 討論羣中, 儘量在 合碼完成前能發現問題。

總結

爲了持續穩定的保持較快的編譯速度,可能需要做到以下幾點:

  1. 項目需要有良好的工程結構,對業務模塊進行適當粒度的拆分,做好 aar/源碼的切換不僅能節省 javac/kotlinCompile 的耗時,也是其他優化方案的基礎。
  2. 工程配置要有區分度,不要所有子模塊都用同樣的配置,比如根本不會用到 kapt 功能的模塊就別打開 kapt task 了。
  3. transform 若無必要,無須新加,或者按級別劃分,如今日頭條在 debug,devMode,release 不同的構建級別用到的 transform 數量是不一致的,儘量讓絕大多數人能獲得相對最快的編譯速度體驗,而不會被用不到的功能拖慢速度。
  4. 一定要新增的 transform 可以先多用現有的增量方案,如 byteX 以及本文提供的類信息注入框架, 儘量把不要的文件 io 合併。
  5. 很多高耗時的 官方 task(dexBuilder) 都是有直接或間接的辦法提升其效率的,並且如果除了耗時之外有其他的衡量手段,如本文提到的重新 dex 率,通過量化數據可以快速的發現問題,進而找到耗時的罪魁禍首。
  6. 與 app 性能優化等工作類似,編譯速度優化既需要持續進行,也需要一定的問題發現手段,儘量避免問題出現很長一段時間後再去查找原因(那時候可能業務依賴程度會非常高,難以修改)。

本文轉載自公衆號字節跳動技術團隊(ID:toutiaotechblog)。

原文鏈接

https://mp.weixin.qq.com/s?__biz=MzI1MzYzMjE0MQ==&mid=2247486165&idx=1&sn=0611dc9f2ed258fb08750cf5a7f73707&chksm=e9d0c137dea74821c56cc695aadadb836f3cb6cf7729c7af3056ca104e925265c4896ce8505b&mpshare=1&scene=1&srcid=0721K7ynDiKM7ZGRdmSb9390&sharer_sharetime=1595289389568&sharer_shareid=942119afdfbc37ad9eb04201dfe5b060&key=a6a963ce8dc9af21aaa42ab217d79171cb42db3c99ec358b72f20d5dc66615d4b2ff460c52f2006acea9dbb728d1dd382803b110ce591d04ca7a63efa144dece66d4b20ddf8646ac5ae9591d7ef862b1&ascene=1&uin=MjA0NTE5Njk0Mg%3D%3D&devicetype=Windows+10+x64&version=62090529&lang=zh_CN&exportkey=AREMMM%2FbZeCO%2BZZb1gu7VZ4%3D&pass_ticket=Da%2BXyUj0fy3NdU8eD2%2FyLZDIFXutL8nBNIINoBE8pzwGl4qLheOpZGRVlvoajo%2BQInfoQ丁曉昀:作者:字節跳動技術團隊

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章