1. MTFlexbox
MTFlexbox是美團內部應用的非常成熟的一種跨平台動態化解決方案,它遵循了CSS3中提出的Flexbox規范來抹平多平台的差異。MTFlexbox適用於重展示、輕交互的業務場景,與現有HTML、React Native、Weex等跨平台方案相比,MTFlexbox具備着性能高、渲染速度快、兼容性高、原生功能支持度高等優勢。但其缺點在於不支持復雜的交互邏輯,不適合復雜交互的業務場景。目前,MTFlexbox已經廣泛應用在美團首頁、搜索、外賣等重要業務場景。本文主要介紹在MTFlexbox中使用Litho優化性能的實踐經驗。
1.1 MTFlexbox的原理
MTFlexbox首先定義一份跨平台統一的DSL布局描述文件,前端通過“所見即所得”的編輯器編輯產生布局,客戶端下載布局文件后,根據布局中的描述綁定JSON數據,並最終完成視圖的渲染。MTFlexbox框架圖如下圖所示:
圖中分為五層,分別是:
- 業務應用層:業務使用MTFlexbox的編輯器定義符合Flexbox規范的DSL文件(XML模版)。
- 模版下載:負責XML模版下載相關的工作,包括模版緩存、預加載和異常監控等。
- 模版解析:負責模版解析相關的工作,包括標簽節點的預處理、數據綁定、標簽節點的緩存復用和數據異常監控等。
- 視圖渲染:負責視圖渲染相關的工作,包括把標簽結點按照Flexbox規范解析成Native視圖,並完成視圖屬性的設置、點擊曝光事件的處理、視圖渲染、異常監控等。
- 自定義標簽擴展:提供支持業務擴展自定義標簽的能力。
鑒於本篇博客主要涉及渲染相關的內容,下面將着重介紹MTFlexbox從模版解析到渲染的過程。如下圖所示,MTFlexbox首先會把XML模版解析成Java中的標簽樹,然后和JSON數據綁定結合成一顆具有完整數據信息的節點樹。至此,模版解析工作就完成了。解析完成的節點樹會交給視圖引擎進行Native視圖樹的創建和渲染。
2. MTFlexbox在美團動態化實踐中面臨的挑戰
隨着MTFlexbox在美團內部被廣泛使用,我們遇到了兩個問題:
- 復雜視圖因層級過深,導致滑動卡頓問題。
- 生成視圖耗時過長,導致滑動卡頓問題。
2.1 問題一:視圖層級過深
2.1.1 原因分析
MTFlexbox使用的是Flexbox布局,Flexbox布局可以理解成Android LinearLayout布局的一種擴展。Flexbox在布局過程中使用到大量的布局嵌套,如果布局酷炫復雜,無疑會出現布局層級過深、視圖樹遍歷耗時、繪制耗時等問題,最終引發滑動卡頓。下圖是美團正在使用的一個模版的視圖層級情況(布局最深處有8層):
2.1.2 影響
布局層級過深在布局的計算和渲染過程中會導致過多的遞歸調用,影響視圖的繪制效率,引發頁面滑動FPS下降問題,這會直接影響到用戶體驗。
2.2 問題二:生成視圖耗時過長
2.2.1 原因分析
視圖生成耗時原因如下圖所示:RecyclerView在使用MTFlexbox布局條目時,需要對條目模版進行下載並解析生成節點樹,這樣會導致生成視圖的過程耗時過長。為了提高視圖生成速度,我們增加了復用機制,但是滑動過程中,如果遇到新的布局樣式仍然需要重新下載和解析。另外,MTFlexbox綁定的數據是未經解析的JSON字符串,所以也要比正常情況下的數據綁定更耗時一些。 正是上面兩個原因,導致了MTFlexbox生成視圖耗時過長的問題,這也會導致滑動時FPS出現突然下降的現象,產生卡頓感。
2.2.2 影響
由於視圖的創建會阻塞主線程,創建視圖耗時過長會導致RecyclerView列表滑動時卡頓感明顯,也嚴重影響到了用戶體驗。
3. Litho
3.1 Litho原理
Litho是一套聲明式UI框架,或者說是一個渲染引擎,它主要優化復雜RecyclerView列表的滑動性能問題。Litho實現了視圖的細粒度復用、異步計算布局和扁平化視圖,可以顯著提升滑動性能,減少RecyclerView滑動時的內存占用。詳細介紹可以參考美團技術團隊之前發布的另一篇博客:Litho的使用及原理剖析。
3.2 Litho的優勢
通過對Litho原理的了解,我們可以看到Litho主要針對RecyclerView復雜滑動列表做了以下幾點優化:
- 視圖的細粒度復用,可以減少一定程度的內存占用。
- 異步計算布局,把測量和布局放到異步線程進行。
- 扁平化視圖,把復雜的布局拍成極致的扁平效果,優化復雜列表滑動時由布局計算導致的卡頓問題。
扁平化視圖剛好可以優化MTFlexbox遇到的視圖層級過深的問題。異步計算布局雖然不能直接解決MTFlexbox生成視圖耗時過長問題,但是給問題的解決提供了新的思路——異步提前完成視圖創建。而且使用Litho還能帶來一定程度的內存優化。所以如何將Litho應用到MTFlexbox中,進而來解決MTFlexbox現存的問題,是我們解下來要討論的重點。
4. Litho + MTFlexbox是怎么解決上述兩個問題的?
4.1 解決問題一:視圖層級過深問題
Litho實現了布局的扁平化,所以最直接的方式就是使用Litho來替換MTFlexbox現有的視圖引擎。視圖引擎最主要的作用,是把XML文件解析出來的節點樹變成Litho可以展示的視圖,所以視圖引擎替換的主要工作是把節點樹轉換成Litho能展示的視圖。如下圖所示。由於Litho使用的是組件化思想,需要先把節點轉化成組件,再把組件樹設置給LithoView,而LithoView是Litho用於兼容原生View的容器,它負責把Litho和系統視圖引擎橋接起來。
不過視圖引擎的替換並不是一帆風順的,我們在替換過程中也遇到了4個比較大的挑戰。
難點一:復用視圖無法更新數據問題
問題描述:完成了節點樹到組件樹的轉化以后,我們發現了一個嚴重的問題——復用的視圖無法應用新的數據。
問題分析:當數據發生變化后,MTFlexbox的節點樹會對比新舊數據的變更,確定哪些結點需要更新並通知到具體的視圖節點,然后更新顯示內容(例如:新數據相比舊數據改變了Text,那么只有Text對應的節點會通知對應的視圖去更新內容)。Litho組件的Prop屬性是不允許更改的,而Litho組件中絕大多數屬性都是Prop屬性。
解決方案
方案一:使用State屬性全局替換所有組件的Prop屬性。這種方式的優點在於替換方式相對簡單直接,缺點是侵入性強,替換工作量巨大且不符合Litho的思想(盡可能少的去改變組件的狀態)。這種方案不是最優解,我們要降低侵入,簡單快捷地實現數據更新,於是就產生了方案二,具體如下圖所示。
方案二:封裝一套Updater組件,用於創建真正展示的組件。Updater組通過State屬性監聽對應節點的數據變更,當節點數據變化時,可以觸發對應節點的更新。
但在后來的實踐過程中,我們發現Litho整個組件樹中只要有一個組件有狀態更新,便會重新計算整個布局,而每次數據更新少說也會有幾十個節點發生變化。頻繁的重復計算反而導致性能變得很差。在經過了多種嘗試以后,我們找到了最優的解決方案:
如上圖所示,狀態更新控制器負責整個視圖所有節點的更新操作。在所有數據都更新完成以后,統一交由狀態更新控制器觸發一遍組件更新。
難點二:Litho不支持層疊布局問題
MTFlexbox並沒有完全嚴格的使用Flexbox布局規范,為了簡單實現層疊效果,MTFlexbox自定義了一種新布局規范——Layer布局。Layer布局具有以下兩個特點:
- 特點一:Layer的子視圖在z軸上依次層疊展示。
- 特點二:Layer的子視圖默認且只能充滿父布局。
原因分析: 由於Litho嚴格遵守Flexbox布局規范,所以沒有現成的Layer組件。
解決方案: 自己實現Layer組件,滿足第一個特點很容易,Flexbox本身就支持層疊展示,只需要把子視圖設為絕對布局就可以了。但是讓子視圖默認充滿父布局就沒有那么簡單了,Flexbox布局中沒有任何一個屬性可以達到這個效果。在經過了若干次組合多個屬性的嘗試以后,還是沒能找到解決方案。既然Layer並不是Flexbox布局的規范,那么我們局限在Flexbox的束縛下,怕是很難找到完美的解決方案。那么,能不能在Litho中繞過Flexbox的約束,自己實現Layer效果呢?想在Litho中突破Flexbox布局的束縛,就需要了解Litho是如何使用Flexbox的。
如上圖,Litho的Flexbox布局是由Yoga負責布局計算的。每一個Litho組件都會對應一個Yoga節點。但Yoga的布局計算過程是由根節點去統一觸發的,子節點沒有辦法知道自己對應的Yoga節點是何時開始計算,及何時計算結束。這樣以來,我們就沒有時機去感知到Layer組件的布局是否計算完成,也就沒有辦法在Layer組件計算完成后去控制Layer子節點的計算。為了解決這個問題,我們做了兩件事:
- 添加布局計算完成的回調,在布局計算完成后由根節點逐層通知子節點計算完成的消息。
- 拆分Yoga節點樹,由Layer自己來控制子節點的計算。
如上圖所示,把Layer組件偽造成葉子節點,不把Layer組件的子節點設置給Yoga,這樣一個Yoga中的布局樹就被Layer組件切割開了。當根節點計算完成以后,通知到Layer組件,Layer組件再依次去設置子節點的寬高和位置屬性,並觸發子節點去完成各自子節點的布局計算。這樣就完美地實現了Layer的布局效果。
難點三:Litho圖片組件不支持使用網絡圖片問題
原因分析: Litho的組件是一個屬性的集合,Litho期望我們在組件創建時便確定了所有屬性的值,所以Litho不支持網絡圖的展示。如果要支持從網絡下載圖片,就意味着圖片組件用來展示的內容會發生變化。所以Litho自帶的圖片組件並不支持使用網絡圖片。
解決方案
方案一:用State屬性解決網絡圖片下載帶來的展示內容變化問題。我們在實踐中發現,State屬性的更新會導致整個布局重新計算,其實替換圖片資源不會導致圖片組件的大小位置發生變化,根本不需要重新計算布局。為了減少使用State屬性導致布局計算頻繁的問題,就摒棄了這種方案。
方案二:Litho官方額外提供的異步下載圖片組件FrescoImage中使用的是圖片代理方式。FrescoImage使用DraweeDrawable來繪制視圖,而DraweeDrawable實際上並不具備圖片渲染的能力,只是在內部保存了一個真正的Drawable來負責渲染。所以,DraweeDrawable本質上是對真正要展示的圖片做了一層代理,當從網絡上下載下來真正要展示的圖片后,只需要通過替換代理圖片就可以完成視圖的更新。美團下載圖片使用的是Glide,只需要按照這個思路實現自己的GlideDrawable就好了。
難點四:自定義標簽擴展的接口不兼容問題
MTFlexbox支持自定義標簽的擴展,所以我們在完成基本視圖標簽的Litho實現以后,還需要支持自定義Tag的擴展,才算完成視圖引擎的替換工作。
原因分析: MTFlexbox在設計自定義標簽接口時,只提供了允許使用View完成視圖擴展的接口,但是Litho實現的視圖引擎是使用組件作為視圖單元和MTFlexbox對接的,所以接口不能兼容。
解決方案
方案一:重新提供使用Litho組件完成視圖擴展的接口。其缺點是,需要MTFlexbox的使用方重新實現已經支持了的自定義標簽,工作量較大,所以這種方案被拋棄了。
方案二:Litho中使用業務方已經擴展好的View。其優點是使用方對視圖引擎的替換無感知。那么,怎樣才能在Litho中使用業務方已經擴展好的View呢?可以先看下面這張圖。
我們可以簡單的理解成Litho對Android的View做了一個功能拆分,把屬性和布局計算的能力放在了組件里面,每一種組件對應一個繪制單元來專門負責繪制。那么對於使用方擴展的標簽,我們可以定義一個通用組件來統一承接。在掛載繪制單元時,再去調用使用方擴展的視圖去繪制。
優化效果
至此,視圖引擎的替換就完成了,整個視圖引擎的替換做到了使用方無感知。完美解決了MTFlexbox視圖層級深的問題,順帶還優化了部分性能。下面是布局層級優化效果的對比,可以看到相同樣式下,使用Litho引擎實現的視圖比使用MTFlexbox原生引擎的視圖層級要淺很多。
除此之外,還有我們的內存優化成果。下圖是美團首頁使用MTFlexbox時,內存占用隨滑動頁數(一頁為20條數據)增加而變化的趨勢圖。可以看到,使用Litho引擎實現的MTFlexbox比使用原生引擎的MTFlexbox在內存占用上能有30M以上的優化。
4.2 解決問題二:生成視圖耗時過長
上文提到導致生成視圖耗時過長的有兩個原因:
(1) MTFlexbox對布局模版的下載和解析耗時。 (2) MTFlexbox綁定時解析數據的耗時。
上文“自定義標簽擴展的接口不兼容問題”中介紹過Litho的組件能夠獨立完成布局計算。另外,Litho組件是輕量級的,所以我們直接把Litho組件作為RecyclerView適配器的數據源。這樣就需要在數據解析時提前完成組件的創建,而組件的創建需要用到MTFlexbox的整個解析過程,也就是說,我們把MTFlexbox導致視圖生成耗時過長的過程提前在數據層異步完成了。這樣就不需要等到視圖要展示時再去解析,從而規避了視圖生成耗時過長的問題。具體的原理,可以參見Litho的使用及原理剖析一文中的3.2節“異步布局”。
如上圖所示,在異步線程中提前完成MTFlexbox布局到Litho組件的轉換。當視圖真正要展示時,只需要把組件設置給LithoView就可以了。
優化效果
使用Litho引擎實現的滑動列表,在連續滑動過程中不會出現FPS波動問題,而使用MTFlexbox原生引擎實現的滑動列表則波動明顯。(數據采集自魅藍2手機,中低端手機優化效果明顯)
5. 總結
經過一段時間的實踐,Litho + MTFlexbox給美團App在性能指標上帶來了較大的提升。但是還有很多問題待完善,我們后續還會針對以下幾點進一步提升效果:
- 利用Litho組件屬性不可變的特點,將提前異步布局進一步擴展為提前渲染出位圖,在繪制時直接展示位圖,可以進一步提升繪制效率。
- 優化RecyclerView相關的API,降低侵入性。
- 解決有點擊事件、埋點事件等屬性的視圖需要降級成View才能使功能生效的問題,進一步優化視圖層級。
6.參考資料
7. 作者簡介
少寬、騰飛、葉梓,美團終端業務研發團隊開發工程師。
8. 招聘信息
美團終端業務研發團隊的職責是保障平台業務高效、穩定迭代的同時,持續優化用戶體驗和研發效率。團隊負責的業務主要包括美團首頁、美團搜索等千萬級DAU高頻業務以及分享、賬號、音/視頻等基礎業務,支撐和對接外賣、酒店等30多個業務方。
團隊通過動態化能力建設,加快業務上線速度,幫助產品團隊快速驗證業務選型,做出業務決策;通過架構/服務標准化體系建設,提升前后端以及平台與業務線的溝通、合作效率;業務監控和體驗優化,有效保障核心業務服務成功率的同時,提升用戶使用美團App過程中的穩定性和流暢性。團隊開發技術棧包括Android、iOS、ReactNative、Flexbox等。
美團終端業務研發團隊現誠聘Android、iOS工程師,歡迎有興趣的同學投簡歷至:tech@meituan.com(注明:美團終端業務研發團隊)。