高兼容低成本,開箱即用的首頁性能優化方式被我們找到了
2020年初,小紅書首頁 UI 的復雜度顯著提升,在優化布局 xml 和使用一些 stub 方式的同時,我們也在尋找一些成本更低、性能更好的方式。
X2C 是當時業界熟知的一種優化方式,其原理是編譯期將 xml 翻譯成代碼,可以有效避免反射以及讀取資源文件的損耗。由于小紅書 APP 中存在著很多自定義 View 的場景,X2C 同時也會帶來較高的維護成本。
經過對 LayoutInflater 耗時的深入分析,我們找到了可以兼容各種 View 場景的 APT 方案。這一方案既避免了反射所帶來的損耗,也不會增加額外的維護成本,成為了一個開箱即用的工具。
1、方案的探索
我們的探索靈感來自于 ViewCompiler 。作為 Google 的一個實驗性工具,ViewCompiler 可以手動地將 xml 布局轉化為 java 文件或者 dex 文件,但它并不支持 merge 和 include 標簽。
ViewCompiler 在 Android Q(Android 10)的時候被引入,目前來說也還是一個實驗性質的工具,因此我們平時并沒有辦法使用它。下圖為Android S(Android 12)中的源碼,大家可以看到這項功能未被開啟。
其原理也很簡單,先生成一個模板代碼片段,然后再生成遍歷 xml 的邏輯代碼。
這樣做的主要好處是可以節省掉反射帶來的時間消耗,官方在 AppCompatViewInflater 中已經處理了原生 View 的創建,通過直接匹配名稱 new 對象,避免了使用反射造成的性能開銷。
在日常使用中,反射性能開銷主要集中在自定義 View 這部分,我們的 App 本身就是一個自定義 View 非常多的場景,所以天然適合這種 VIewCompiler 的這種方式。同時,因為在遍歷 xml 的時候,每一個 attrs 都會遍歷到,所以它在維護性上也有著巨大的優勢,我們不需要對自定義的 attrs 做任何處理。
基于對 X2C 和 ViewCompiler 的源碼和生成代碼的閱讀,我們決定做一個可以生成 Kotlin 代碼,同時也解決 ViewCompiler 不支持的 include 和 merge 兩個標簽。我們用到的工具比較常規,有 kapt 和 kotlinpoet,整體的思路是通過 Resources.getLayout 取到 XmlResourceParser,然后通過 parser 的不斷 next 來遍歷每一個 xml 中的 tag,生成的代碼示意如下:
在遇到 merge 和 include 時,我們需要特殊處理遞歸調用的邏輯,以便可以將父子布局連在一起。
用這種新的方式替換掉首頁中一些布局的實現后,我們發現,線上首頁部分 p90 的布局時間減少了 200ms+,時長、CES、留存等指標均得到了顯著提升。
2、探入分析
LayoutInflater 的工作過程
LayoutInflater 的工作過程可以用下圖來簡易表示:
本文所闡述的方案就是利用 apt 在編譯期間生成代碼,在便利解析 layout 文件之后,我們使用生成的代碼直接創建實例,其效率與命中 AppCompat 基礎組件邏輯之后的效率在理論上是一致的。
AppCompat 基礎組件可以查看 AppCompatViewInflater.java 源碼(上文也有部分展示),其中包括了諸如 TextView、Button 等十幾個常用的基礎組件。
就一個具體的布局而言,能夠通過 Layout2Code 的使用得以提升的性能只有除了基礎組件之外的其他組件,尤其是當布局使用了大量自定義組件時,效果尤為明顯。
這也給了我們另一個提醒。如在 xml 中寫 TextView / TextViewCompat,在 AppCmpatViewInflater 的作用下最終創建的實例都是 TextViewCompat。但在不使用 Layout2Code 或類 X2C 方案時,它們的效率是不同的,前者命中上圖的直接創建邏輯,而后者則會通過反射創建。
X2C的不足
X2C 除了做了以上優化,還將 layout 文件的讀取和解析也一并移到了編譯階段,以此來降低 IO 開銷。但編譯期解析 xml 最大的困難在于我們需要逐條翻譯 View 的屬性,原因是編譯期間并沒有 SDK 的依賴,因此無法生成 AtrributeSet 對象直接供以 View 的構造器消費。
這樣一來,需要人工維護翻譯規則,將一條條 xml 屬性轉換成設置 View 屬性的代碼,這帶來了幾個問題:
1. 生成的代碼量指數級增加
2. 需要極高的維護成本來支持自定義 View 的屬性
3. 某些 xml 屬性并沒有相對應的方法或不是一一對應的。
總而言之,在此基礎上要維持健壯完備的功能是非常困難的。而我們所探索的 Layout2Code 的新方案與之相比,兼容性和維護成本都有著巨大優勢,唯一需要權衡考慮的就是運行時讀取 layout 文件的優化空間有多少,是否值得這樣的投入。
layout 文件的特殊性
提到 xml 文件,條件反射般地就會想到是 IO 操作,性能差,這沒錯,但 layout 文件卻比較特殊。在 Andorid 應用打包過程中,AAPT 會對資源進行打包,會將除了 asset 文件夾下的 xml 文件通過字符串池復用、二進制轉換等方式進行壓縮,最終生成壓縮后的資源文件和資源文件索引 resources.arsc 還有 R 文件。而在使用 AssetManager 對資源文件進行加載時,我們也會使用 mmap 來降低 IO 成本。
通過分析以上種種手段的利弊,我們在實際應用場景中測試后發現讀取 layout 文件的耗時通常不超過 1ms。因此,考慮到將 layout 文件的讀取和解析移到編譯階段所帶來的維護成本,權衡之下我們最終選擇了直接放棄這一部分的優化。
3、總結
在當下的開發環境中,Layout2Code 這一方案在性能提升方面仍然能夠發揮很大的作用,當然有效使用這一方案的前提是開發者足夠了解方案原理,以及知曉其具體的適用范圍(非 AppComapt 組件)。
相比于傳統的 X2C 方案,Layout2Code 的適用范圍更廣,維護成本也更低。目前,該方案已經在小紅書 APP 中得到了廣泛的應用,并為我們帶來了良好的收益和效果。我們對 Layout2Code 的研究由 kotlin 實現,使用 kapt,在未來我們也計劃接入 ksp,來減少編譯期耗時,持續優化這一方案。
4、作者簡介
殤不患 (blv@xiaohongshu.com) 小紅書商業技術 Android 工程師
綾人(lingren@xiaohongshu.com) 小紅書商業技術 Android 工程師