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