MTFlexbox是美團內部應用的很是成熟的一種跨平臺動態化解決方案,它遵循了CSS3中提出的Flexbox規範來抹平多平臺的差別。MTFlexbox適用於重展現、輕交互的業務場景,與現有HTML、React Native、Weex等跨平臺方案相比,MTFlexbox具有着性能高、渲染速度快、兼容性高、原生功能支持度高等優點。但其缺點在於不支持複雜的交互邏輯,不適合複雜交互的業務場景。目前,MTFlexbox已經普遍應用在美團首頁、搜索、外賣等重要業務場景。本文主要介紹在MTFlexbox中使用Litho優化性能的實踐經驗。css
MTFlexbox首先定義一份跨平臺統一的DSL佈局描述文件,前端經過「所見即所得」的編輯器編輯產生布局,客戶端下載佈局文件後,根據佈局中的描述綁定JSON數據,並最終完成視圖的渲染。MTFlexbox框架圖以下圖所示:html
圖中分爲五層,分別是:前端
鑑於本篇博客主要涉及渲染相關的內容,下面將着重介紹MTFlexbox從模版解析到渲染的過程。以下圖所示,MTFlexbox首先會把XML模版解析成Java中的標籤樹,而後和JSON數據綁定結合成一顆具備完整數據信息的節點樹。至此,模版解析工做就完成了。解析完成的節點樹會交給視圖引擎進行Native視圖樹的建立和渲染。git
隨着MTFlexbox在美團內部被普遍使用,咱們遇到了兩個問題:github
MTFlexbox使用的是Flexbox佈局,Flexbox佈局能夠理解成Android LinearLayout佈局的一種擴展。Flexbox在佈局過程當中使用到大量的佈局嵌套,若是佈局酷炫複雜,無疑會出現佈局層級過深、視圖樹遍歷耗時、繪製耗時等問題,最終引起滑動卡頓。下圖是美團正在使用的一個模版的視圖層級狀況(佈局最深處有8層):後端
佈局層級過深在佈局的計算和渲染過程當中會致使過多的遞歸調用,影響視圖的繪製效率,引起頁面滑動FPS降低問題,這會直接影響到用戶體驗。緩存
視圖生成耗時緣由以下圖所示:RecyclerView在使用MTFlexbox佈局條目時,須要對條目模版進行下載並解析生成節點樹,這樣會致使生成視圖的過程耗時過長。爲了提升視圖生成速度,咱們增長了複用機制,可是滑動過程當中,若是遇到新的佈局樣式仍然須要從新下載和解析。另外,MTFlexbox綁定的數據是未經解析的JSON字符串,因此也要比正常狀況下的數據綁定更耗時一些。 正是上面兩個緣由,致使了MTFlexbox生成視圖耗時過長的問題,這也會致使滑動時FPS出現忽然降低的現象,產生卡頓感。網絡
因爲視圖的建立會阻塞主線程,建立視圖耗時過長會致使RecyclerView列表滑動時卡頓感明顯,也嚴重影響到了用戶體驗。架構
Litho是一套聲明式UI框架,或者說是一個渲染引擎,它主要優化複雜RecyclerView列表的滑動性能問題。Litho實現了視圖的細粒度複用、異步計算佈局和扁平化視圖,能夠顯著提高滑動性能,減小RecyclerView滑動時的內存佔用。詳細介紹能夠參考美團技術團隊以前發佈的另外一篇博客:Litho的使用及原理剖析。框架
經過對Litho原理的瞭解,咱們能夠看到Litho主要針對RecyclerView複雜滑動列表作了如下幾點優化:
扁平化視圖恰好能夠優化MTFlexbox遇到的視圖層級過深的問題。異步計算佈局雖然不能直接解決MTFlexbox生成視圖耗時過長問題,可是給問題的解決提供了新的思路——異步提早完成視圖建立。並且使用Litho還能帶來必定程度的內存優化。因此如何將Litho應用到MTFlexbox中,進而來解決MTFlexbox現存的問題,是咱們解下來要討論的重點。
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佈局具備如下兩個特色:
緣由分析: 因爲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子節點的計算。爲了解決這個問題,咱們作了兩件事:
如上圖所示,把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以上的優化。
上文提到致使生成視圖耗時過長的有兩個緣由:
(1) MTFlexbox對佈局模版的下載和解析耗時。 (2) MTFlexbox綁定時解析數據的耗時。
上文「自定義標籤擴展的接口不兼容問題」中介紹過Litho的組件可以獨立完成佈局計算。另外,Litho組件是輕量級的,因此咱們直接把Litho組件做爲RecyclerView適配器的數據源。這樣就須要在數據解析時提早完成組件的建立,而組件的建立須要用到MTFlexbox的整個解析過程,也就是說,咱們把MTFlexbox致使視圖生成耗時過長的過程提早在數據層異步完成了。這樣就不須要等到視圖要展現時再去解析,從而規避了視圖生成耗時過長的問題。具體的原理,能夠參見Litho的使用及原理剖析一文中的3.2節「異步佈局」。
如上圖所示,在異步線程中提早完成MTFlexbox佈局到Litho組件的轉換。當視圖真正要展現時,只須要把組件設置給LithoView就能夠了。
優化效果
使用Litho引擎實現的滑動列表,在連續滑動過程當中不會出現FPS波動問題,而使用MTFlexbox原生引擎實現的滑動列表則波動明顯。(數據採集自魅藍2手機,中低端手機優化效果明顯)
通過一段時間的實踐,Litho + MTFlexbox給美團App在性能指標上帶來了較大的提高。可是還有不少問題待完善,咱們後續還會針對如下幾點進一步提高效果:
少寬、騰飛、葉梓,美團終端業務研發團隊開發工程師。
美團終端業務研發團隊的職責是保障平臺業務高效、穩定迭代的同時,持續優化用戶體驗和研發效率。團隊負責的業務主要包括美團首頁、美團搜索等千萬級DAU高頻業務以及分享、帳號、音/視頻等基礎業務,支撐和對接外賣、酒店等30多個業務方。
團隊經過動態化能力建設,加快業務上線速度,幫助產品團隊快速驗證業務選型,作出業務決策;經過架構/服務標準化體系建設,提高先後端以及平臺與業務線的溝通、合做效率;業務監控和體驗優化,有效保障核心業務服務成功率的同時,提高用戶使用美團App過程當中的穩定性和流暢性。團隊開發技術棧包括Android、iOS、ReactNative、Flexbox等。
美團終端業務研發團隊現誠聘Android、iOS工程師,歡迎有興趣的同窗投簡歷至:tech@meituan.com(註明:美團終端業務研發團隊)。