天天看點

Android DataBinding 編譯變慢之謎

作者:閃念基因

背景

2018 年初,知乎 Android 用戶端處于元件化中期階段,元件的拆分和建立正在如火如荼的進行。得益于元件化, java 檔案可以提前編譯為 class 檔案, app 整體的編譯時間也得到了一定程度上的提升。然而有一天,主工程的的編譯時間突然從 4 分鐘猛增到了 10 分鐘,對于仍在主工程進行開發的同學來說,這嚴重影響了開發效率,同時也使得 CI 資源耗費幾乎翻番,是以需要排查一下問題到底出在哪裡。

定位問題

首先,在編譯時添加 --profile 參數,發現增長的部分都在 javac 階段,對應的 gradle task 為 compile{flavor}{buildType}JavaWithJavac ,而其他編譯階段的耗時基本沒有變化,很明顯編譯 java 檔案到 class 的時候出了問題。

由于并不是立即發現問題,并且送出的 MR 衆多,不知道立即确定到底是哪裡的改動導緻的問題,于是祭出二分查找大法,最終找到了編譯變慢的那個 commit,然後看代碼并沒有什麼可疑之處,隻是簡單的将一個元件拆分成了三個元件 —— 元件化拆分時賬号元件是第一個拆出來的元件,拆得比較粗糙,連帶着不少其他業務代碼和基礎代碼,是以這個 commit 将賬号元件拆成了三個元件:頁面架構、賬号和一個 Common 元件。難道這次拆分的某些改動觸發了編譯的 bug ?

同時又對比了一下其他各個業務元件的編譯時長,發現各個業務元件 javac 的時間也均有不同程度的增長,其中依賴層級越多的元件,編譯時間增長越大,時長的增長與依賴層級基本成指數關系。

舉例來說,假設有四個元件 ABCD,A 依賴 B、B 依賴 C、C 依賴 D,那麼 D 元件編譯時長增長了 1 分鐘的話,C 元件編譯時間增長大約 2 分鐘,B 元件大約增長 4 分鐘,而 A 則會增長大約 8 分鐘了。

對比元件拆分前後的代碼,絕大部分都隻是檔案的簡單移動,其他改動的地方也非常的簡單,很難想像這種情況會觸發什麼編譯器的問題,畢竟幾乎 100% 的情況下,懷疑編譯器隻會打自己的臉。

那問題可能出在哪裡呢?我們知道,Android 編譯并不僅僅有 javac 階段,還會有其他編譯過程,而 aar 檔案中除了 jar 檔案之外,還有一堆其他的 Android 相關的産物,這是如果代碼看不出問題的話,難道是其他編譯産物導緻了的問題嗎?

找一個最上層的業務元件對比了一下編譯産物 aar 檔案:

Android DataBinding 編譯變慢之謎

如上圖,發現很明顯有一個setter_stores.bin 檔案的體積有了巨幅的增長,不出意外的話,打包時間應該跟這個檔案有關系。

然後再分析了其他元件的 aar,發現此檔案的體積增幅與 javac 編譯時間的增幅基本一緻,而體積與編譯時間的增幅與依賴樹的所有分支中包含 databinding 元件數量最多的那條分支的 databinding 數量大緻呈指數關系。也就是說:

原本編譯 Java 隻要 2 分鐘,依賴層級中多了一層普通的庫,編譯時間不會發生變化,而如果多了一層啟用了 DataBinding 的庫,可能會增長 1 分鐘,如果再多兩層 DataBinding ,則會增長 4 分鐘。

得出這個猜測之後,解決方案就比較明确了:去除一些元件的 DataBinding 屬性。由于頁面架構和 Common 元件并沒有多少 DataBinding 的代碼,是以直接将這兩個元件的 DataBinding 去掉,打包時間恢複。

雖然暫時解決了眼前的問題,但是并沒有真正解決問題,說不定那一天 DataBinding 依賴層級會再次變多,問題會再次出現。果然,在元件化最後一次拆分完畢之後,編譯時間再次暴漲。當時是将最後的社群業務全部從主工程中拆走,由于主工程業務複雜,拆走後變成了多個業務元件,這些業務元件由于曆史原因是有耦合關系的,是以導緻最終多了幾個 DataBinding 依賴層級,而這些元件都使用了很多的 DataBinding 代碼,使得去除 DataBinding 依賴變得有些不現實:首先是改造這些代碼的開發成本,更重要的是 QA 的回歸成本很高,還有就是這樣不能真正解決問題,誰也不能确定日後會不會再有依賴層級出現,除非我們禁止再使用 DataBinding。是時候深究一下 DataBinding 導緻編譯變慢的根本原因了。

具體慢在哪裡

要看到底慢在哪裡的,我們就要知道,打包的這一段時間,它都在幹什麼,我們使用工具 visual vm 看一下( jstack 也可以):

1 ) 打開 Visual VM

2 ) 執行主工程編譯指令,定位編譯的程序 id (例子中為 7689)

Android DataBinding 編譯變慢之謎

3 ) 在編譯執行到 javac 階段的時候 ( 比如 compileDebugJavaWithJavac ),在程序名上點選右鍵 → Thread Dump,将線程 dump 下來,可以多執行幾次

4 ) 由于我們已經猜測是 DataBinding 導緻的問題,是以直接在 dump 出的資訊中搜尋 databinding,發現每次都卡在了同一個地方:

"Task worker for ':'" #522 prio=5 os_prio=0 tid=0x00007f4af4447800 nid=0x30a0 runnable [0x00007f4af8a70000]
   java.lang.Thread.State: RUNNABLE
        at java.util.HashMap$TreeNode.find(HashMap.java:1864)
        at java.util.HashMap$TreeNode.find(HashMap.java:1874)
        at java.util.HashMap$TreeNode.find(HashMap.java:1874)
        at java.util.HashMap$TreeNode.find(HashMap.java:1874)
        at java.util.HashMap$TreeNode.find(HashMap.java:1874)
        at java.util.HashMap$TreeNode.putTreeVal(HashMap.java:1994)
        at java.util.HashMap.putVal(HashMap.java:638)
        at java.util.HashMap.put(HashMap.java:612)
        at android.databinding.tool.store.SetterStore.merge(SetterStore.java:1173)
        at android.databinding.tool.store.SetterStore.merge(SetterStore.java:1153)
        at android.databinding.tool.store.SetterStore.load(SetterStore.java:185)
        at android.databinding.tool.store.SetterStore.create(SetterStore.java:176)
        at android.databinding.tool.Context.init(Context.kt:49)
        at android.databinding.annotationprocessor.ProcessDataBinding.doProcess(ProcessDataBinding.java:95)
        at android.databinding.annotationprocessor.ProcessDataBinding.process(ProcessDataBinding.java:73)
           

所有 dump 的資訊都卡在了 android.databinding.tool.store.SetterStore.merge(SetterStore.java:1173) 這一行上,很明顯,直接原因就是這裡了:SetterStore#merge 方法有鬼。

SetterStore 是做什麼的

在 Android DataBinding 中,可以在 xml 中将資料與 view 的屬性綁定,比如要把一個 String 綁定到 TextView 的 text 屬性上去,那隻需要在 xml 中聲明

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{string_content}"/>           

然後給 TextView 的 text 屬性定義如何綁定資料

@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
 view.setText(text);
}           

編譯時 databinding-compiler 會找到 xml 屬性對應的 BindingAdapter,并将 xml 中的綁定文法轉換成 BindingAdapter 注解的方法,這樣就實作了資料綁定。

但是,databinding-compiler 是通過 annotationProcessor 處理源碼來生成代碼的,一般來說 annotationProcessor 不會處理本子產品以外的注解,比如說其他 aar 檔案中的注解,那麼如果在 Base 庫中定義了 BindingAdapter 的話,如何讓依賴 Base 庫的工程也能使用 Base 庫的 BindingAdapter 呢?

研究 databinding-compiler 的源碼後發現答案就在 setter_stores.bin 中:databinding-compiler 會把得到的 BindingAdapter 及其他一些元素都存儲一個 SetterStore.Intermediate 類執行個體中,而 setter_stores.bin 是這個對象被序列化後的結果,它最終被打包到 aar 中供引用者使用。引用者在編譯時會把它依賴的所有的庫的 setter_store.bin 都反序列化得到若幹個 Intermediate 類執行個體,然後生成一個合并的 SetterStore 供 annotationProcessor 使用,這樣 annotationProcessor 就可以使用其他工程定義的 BindingAdapter 了,而合并的資料最終又會被序列化成 setter_stores.bin 檔案。

是以 SetterStore 主要做的就是序列化、反序列化與合并其他的 setter_stores.bin 檔案。

為什麼會變慢

我們先看一下 merge 的過程:

首先是拿到所有的 setter_store.bin 檔案并反序列化然後合并,每一個循環都是在将一個 library 的 setter_store 合并到新的 store 中,看起來沒毛病

private static SetterStore load(ModelAnalyzer modelAnalyzer,
                                GenerationalClassUtil generationalClassUtil) {
    IntermediateV3 store = new IntermediateV3();
    List<Intermediate> previousStores = generationalClassUtil
            .loadObjects(GenerationalClassUtil.ExtensionFilter.SETTER_STORE);
    for (Intermediate intermediate : previousStores) {
        merge(store, intermediate);
    }
    return new SetterStore(modelAnalyzer, store);
}           

再看合并單個 setter_store 的方法,看起來很正常,依次合并各個類型的資料

private static void merge(IntermediateV3 store, Intermediate dumpStore) {
    IntermediateV3 intermediateV3 = (IntermediateV3) dumpStore.upgrade();
    merge(store.adapterMethods, intermediateV3.adapterMethods); // 堆棧資訊表示卡在這一行
    merge(store.renamedMethods, intermediateV3.renamedMethods);
    merge(store.conversionMethods, intermediateV3.conversionMethods);
    store.multiValueAdapters.putAll(intermediateV3.multiValueAdapters);
    store.untaggableTypes.putAll(intermediateV3.untaggableTypes);
    merge(store.inverseAdapters, intermediateV3.inverseAdapters);
    merge(store.inverseMethods, intermediateV3.inverseMethods);
    store.twoWayMethods.putAll(intermediateV3.twoWayMethods);
}           

看一下合并單項的 merge 方法,Map 合并,好像也沒有什麼問題

private static <K, V, D> void merge(HashMap<K, HashMap<V, D>> first,
        HashMap<K, HashMap<V, D>> second) {
    for (K key : second.keySet()) {
        HashMap<V, D> firstVals = first.get(key);
        HashMap<V, D> secondVals = second.get(key);
        if (firstVals == null) {
            first.put(key, secondVals);
        } else {
            for (V key2 : secondVals.keySet()) {
                if (!firstVals.containsKey(key2)) {
                    firstVals.put(key2, secondVals.get(key2)); // 堆棧資訊表示卡在這一行
                }
            }
        }
    }
}           

setter_store 的内部是一個個的 Map(見 SetterStore.java#IntermediateV1 ),是以如果不出意外,最終會得到一個小的去重後的 setter_store 。但是我們打開這些生成的 setter_store.bin 檔案,會發現裡面有巨量的重複,同一個 BindindAdapter 在同一個 Map 中出現了多次,而 BindingAdapter 是存儲 IntermediateV1#adapterMethods 這個字段中的,類型是 HashMap<String, HashMap<AccessorKey, MethodDescription>> ,這個 Map 難道有什麼問題麼?

猜測大量的重複應該是跟 key 的 hashcode 和 equals 設計不當有關,adapterMethods 是一個雙重 Map,第一層 key 為 String,顯然沒有問題,pass,第二層的 key 是一個類 AccessorKey,看一下這個類的源碼:

private static class AccessorKey implements Serializable {
 
    private static final long serialVersionUID = 1;
    public final String viewType;
    public final String valueType;
 
    public AccessorKey(String viewType, String valueType) {
        this.viewType = viewType;
        this.valueType = valueType;
    }
 
    @Override
    public int hashCode() {
        return mergedHashCode(viewType, valueType);
    }
 
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof AccessorKey) {
            AccessorKey that = (AccessorKey) obj;
            return viewType.equals(that.valueType) && valueType.equals(that.valueType);
        } else {
            return false;
        }
    }
 
    @Override
    public String toString() {
        return "AK(" + viewType + ", " + valueType + ")";
    }
}           

仔細審查一下 equals 那一行

return viewType.equals(that.valueType) && valueType.equals(that.valueType);           
Android DataBinding 編譯變慢之謎

viewType.equals(that.valueType) 肯定是恒為 false。

根據上面的分析,原本 merge 做的工作是将所有依賴的庫的 setter_store 去重合并,現在因為 equals 寫法錯誤,導緻每個 Key 必然不一樣,完全沒有達到去重的效果

我們假設最簡單的情況,我們有庫 D 依賴 C、C 依賴 B,B 依賴 A,均開啟了 databinding 且都沒有定義任何的 adapterMethods,假設 databinding 庫本身已經包含了 50 個 adapterMethods,那麼:

A 依賴了 databinding 庫,A 最終的 setter_store 中包含了 50 個 adapterMethods

B 依賴了 A 和 databinding 庫,B 最終的 setter_store 中包含了 50 + 50 = 100個 adapterMethods

C 依賴了 A、B 和 databinding 庫,C 最終的 setter_store 中包含了 100 + 50 + 50 = 200個 adapterMethods

D 依賴了 A、B、C 和 databinding 庫,D 最終的 setter_store 中包含了 200 + 100 + 50 + 50 = 400個 adapterMethods

與上面結論「體積的增幅與依賴樹中包含 databinding 元件最長的那條鍊的長度大緻呈指數關系」一緻

而我們的主工程的依賴層級已經達到了 8 層,是以算起來重複率為 1/2^7 ,實際的使用環境中,依賴關系不僅僅是單鍊的依賴,每一個依賴層級可能會有多個庫,是以事實上最終依賴會再翻幾番,最終我們自定義 BindingMethod 的哈希沖突率接近 99.9% (862/863),而 databinding 自帶的 BindingMethod 又翻了兩番,哈希沖突率達到了 99.97% (3497/3498),整個 setter_store.bin 檔案已經達到了 33M ,而事實上去重之後隻有大約 100 個 BindingMethod。

解決方案

一、修改 databinding-compiler。

知道問題在哪裡後,修改也就很簡單了,修改出問題的那一行

return viewType.equals(that.viewType) && valueType.equals(that.valueType);           

重新打包 databinding-compiler,使用後速度果然快了

二、禁止不合理的依賴

雖然 DataBinding 導緻的打包變慢的問題已經得到了解決,但是工程依賴層級過多也是一個問題,造成依賴問題的情況很多:一是某個大型業務拆分的時候,一次拆成了幾個互相依賴的元件,導緻層級變多,二是有些同學有開發的時候貪圖友善,在發生元件間互動的時候,采用了直接引用其他元件(而不是引用元件接口)的方式,導緻依賴關系變得複雜。是以我們的解決方案是對元件進行分級:

  1. 明确業務元件、業務中間件與基礎元件的劃分
  2. 業務元件間禁止發生直接依賴,下層元件禁止依賴上層元件,禁止循環依賴
  3. 對現存的不合理依賴按定級進行重新梳理和解決
  4. 對元件的定級落實到一個集中的配置檔案,并使用 gradle 插件禁止錯誤的依賴

後續

這個 bug 已經存在了一年,2018 年初 Android Gradle Plugin 還是 3.0 版本的時候就遇到過,當初隻是發現了是 DataBinding 的問題,但是并沒有想到可能是一個 bug,是以隻是簡單的去掉了幾個元件的 DataBinding 代碼了事,後來再次探索這個問題的時候已經是 3.2.1 版本,一直沒有被修複。而在寫這篇文章之前幾天,官方又出了 3.3 版本,這個問題已經修複了:SetterStore.java#1357

不僅如此,還悄悄的加了一個 compareTo 方法:

public int compareTo(@NonNull AccessorKey other) {
    int viewTypeCmp = nullableCompare(viewType, other.viewType);
    if (viewTypeCmp == 0) {
        return nullableCompare(valueType, other.valueType);
    } else {
        return viewTypeCmp;
    }
}           

不了解 compareTo 的可以看這個 廖雪峰 # Java Map的正确使用方式

作者:Peter Porker

出處:https://zhuanlan.zhihu.com/p/58010428