1. 什么是Litho?
Litho是Facebook推出的一套高效構建Android UI的聲明式框架,主要目的是提升RecyclerView復雜列表的滑動性能和降低內存占用。下面是Litho官網的介紹:
Litho is a declarative framework for building efficient user interfaces (UI) on Android. It allows you to write highly-optimized Android views through a simple functional API based on Java annotations. It was primarily built to implement complex scrollable UIs based on RecyclerView. With Litho, you build your UI in terms of components instead of interacting directly with traditional Android views. A component is essentially a function that takes immutable inputs, called props, and returns a component hierarchy describing your user interface.
Litho是高效構建Android UI的聲明式框架,通過注解API創建高優的Android視圖,非常適用於基於Recyclerview的復雜滾動列表。Litho使用一系列組件構建視圖,代替了Android傳統視圖交互方式。組件本質上是一個函數,它接受名為Props的不可變輸入,並返回描述用戶界面的組件層次結構。
Litho是一套完全不同於傳統Android的UI框架,它繼承了Facebook一向大膽創新的風格,突破性地在Android上實現了React風格的UI框架。架構圖如下:
應用層:上層Android應用接入層。
規范層(API):允許用戶使用聲明式的API(注解)來構建符合Flexbox規范的布局。
布局層:Litho使用可掛載組件、布局組件和Flexbox組件來構建布局,其中可掛載組件和布局組件允許用戶使用規范來定義,各個組件的具體用法下面的組件規范中會詳細介紹。在Litho中每一個組件都是一個獨立的功能模塊。Litho的組件和React的組件相類似,也具有屬性和狀態的概念,通過狀態的變更來控制組件的展示樣式。
布局測量:Litho使用Yoga來完成組件布局的異步或同步(可根據場景定制)測量和計算,實現了布局的扁平化。
布局渲染:Litho不僅支持使用View來渲染視圖,還可以使用更輕量的Drawable來渲染視圖。Litho實現了大量使用Drawable來渲染的基礎組件,可以進一步拍平布局。
除了上面提到的扁平化布局,Litho還實現了布局的細粒度復用和異步計算布局的能力,對於這些功能的實現在Litho的特性及原理剖析中詳細介紹。下面先介紹一下大家比較關心的Litho使用方法。
2. Litho的使用
Litho的使用方式相比於傳統的Android來說有些另類,它拋棄了通過XML定義布局的方式,采用聲明式的組件在Java中構建布局。
2.1 Litho和原生Android在使用上的區別
Android傳統布局:首先在資源文件res/layout目錄下定義布局文件xx.xml,然后在Activity或Fragment中引用布局文件生成視圖,示例如下:
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World" android:textAlignment="center" android:textColor="#666666" android:textSize="40dp" />
public class MainActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.helloworld); } }
Litho布局:Litho拋棄了Android原生的布局方式,通過組件方式構建布局生成視圖,示例如下:
public class MainActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ComponentContext context = new ComponentContext(this); final Text.Builder builder = Text.create(context); final Component = builder.text("Hello World") .textSizeDip(40) .textColor(Color.parseColor("#666666")) .textAlignment(Layout.Alignment.ALIGN_CENTER) .build(); LithoView view = LithoView.create(context, component); setContentView(view); } }
2.2 Litho自定義視圖
Litho中的視圖單元叫做Component,可以直觀的翻譯為“組件”,它的設計理念來自於React組件化的思想。每個組件持有描述一個視圖單元所必須的屬性和狀態,用於視圖布局的計算工作。視圖最終的繪制工作是由組件指定的繪制單元(View或者Drawable)來完成的。
Litho組件的創建方式也和原生View的創建方式有着很大的區別。Litho使用注解定義了一系列的規范,我們需要使用Litho的注解來定義自己的組件生成規則,最終由Litho在編譯期自動編譯生成真正的組件。
2.2.1 組件規范
Litho提供了兩種類型的組件規范,分別是Layout Spec規范和Mount Spec規范。下面分別介紹兩種規范的使用方式:
Layout Spec規范:用於生成布局類型組件的規范,布局組件在邏輯上等同於Android中的ViewGroup,用於組織其他組件構成一個布局。它要求我們必須使用@LayoutSpec注解來注明,並實現一個標注了@OnCreateLayout注解的方法。示例如下:
@LayoutSpec class HelloComponentSpec { @OnCreateLayout static Component onCreateLayout(ComponentContext c, @Prop String name) { return Column.create(c) .child(Text.create(c) .text("Hello, " + name) .textSizeRes(R.dimen.my_text_size) .textColor(Color.BLACK) .paddingDip(ALL, 10) .build()) .child(Image.create(c) .drawableRes(R.drawable.welcome) .scaleType(ImageView.ScaleType.CENTER_CROP) .build()) .build(); } }
最終Litho會在編譯時生成一個名為HelloComponent的組件。
public final class HelloComponent extends Component { @Prop(resType = ResType.NONE,optional = false) String name; private HelloComponent() { super(); } @Override protected Component onCreateLayout(ComponentContext c) { return (Component) HelloComponentSpec.onCreateLayout((ComponentContext) c, (String) name); } ... public static Builder create(ComponentContext context, int defStyleAttr, int defStyleRes) { Builder builder = sBuilderPool.acquire(); if (builder == null) { builder = new Builder(); } HelloComponent instance = new HelloComponent(); builder.init(context, defStyleAttr, defStyleRes, instance); return builder; } public static class Builder extends Component.Builder<Builder> { private static final String[] REQUIRED_PROPS_NAMES = new String[] {"name"}; private static final int REQUIRED_PROPS_COUNT = 1; HelloComponent mHelloComponent; ... public Builder name(String name) { this.mHelloComponent.name = name; mRequired.set(0); return this; } @Override public HelloComponent build() { checkArgs(REQUIRED_PROPS_COUNT, mRequired, REQUIRED_PROPS_NAMES); HelloComponent helloComponentRef = mHelloComponent; release(); return helloComponentRef; } } }
Mount Spec規范:用來生成可掛載類型組件的規范,用來生成渲染具體View或者Drawable的組件。同樣,它必須使用@MountSpec注解來標注,並至少實現一個標注了@onCreateMountContent的方法。Mount Spec相比於Layout Spec更復雜一些,它擁有自己的生命周期:
- @OnPrepare,准備階段,進行一些初始化操作。
- @OnMeasure,負責布局的計算。
- @OnBoundsDefined,在布局計算完成后掛載視圖前做一些操作。
- @OnCreateMountContent,創建需要掛載的視圖。
- @OnMount,掛載視圖,完成布局相關的設置。
- @OnBind,綁定視圖,完成數據和視圖的綁定。
- @OnUnBind,解綁視圖,主要用於重置視圖的數據相關的屬性,防止出現復用問題。
- @OnUnmount,卸載視圖,主要用於重置視圖的布局相關的屬性,防止出現復用問題。
除了上述兩種組件類型,Litho中還有一種特殊的組件——Layout,它不能使用規范來生成。Layout是Litho中的容器組件,類似於Android中的ViewGroup,但是只能使用Flexbox的規范。它可以包含子組件節點,是Litho各組件連接的紐帶。Layout組件只是Yoga在Litho中的代理,組件的所有布局相關的屬性都會直接設置給Yoga,並由Yoga完成布局的計算。Litho實現了兩個Layout組件Row和Column,分別對應Flexbox中的行和列。
2.2.2 Litho的屬性
在Litho中屬性分為兩種,不可變屬性稱為Props,可變屬性稱為State,下面分別介紹一下兩種屬性:
Props屬性:組件中使用@Prop注解標注的參數集合,具有單向性和不可變性。下面通過一個簡單的例子了解一下如何在組件中定義和使用Props屬性:
@MountSpec class MyComponentSpec { @OnPrepare static void onPrepare( ComponentContext c, @Prop(optional = true) String prop1) { ... } @OnMount static void onMount( ComponentContext c, SomeDrawable convertDrawable, @Prop(optional = true) String prop1, @Prop int prop2) { if (prop1 != null) { ... } } }
在上面的代碼中,共使用了三次Prop注解,分別標注prop1和prop2兩個變量,即定義了prop1和prop2兩個屬性。Litho會在自動編譯生成的MyComponent類的Builder類中生成這兩個屬性的同名方法。按照如下代碼,便可以去使用上面定義的屬性:
MyComponent.create(c)
.prop1("My prop 1") .prop2(256) .build();
State屬性:意為“狀態”屬性,State屬性雖然可變,但是其變化由組件內部控制,例如:輸入框、Checkbox等都是由組件內部去感知用戶行為,並更新組件的State屬性。所以一個組件一旦創建,我們便無法通過任何外部設置去更改它的屬性。組件的State屬性雖然不允許像Props屬性那樣去顯式設置,但是我們可以定義一個單獨的Props屬性來當做某個State屬性的初始值。
3. Litho的特性及原理剖析
Litho官網首頁通過4個段落重點介紹了Litho的4個特性。
3.1 聲明式組件
Litho采用聲明式的API來定義UI組件,組件通過一組不可變的屬性來描述UI。這種組件化的思想靈感來源於React,關於聲明式組件的用法上面已經詳細介紹過了。
傳統Android布局因為UI與邏輯分離,所以開發工具都有強大的預覽功能,方便開發者調整布局。而Litho采用React組件化的思想,通過組件連接了邏輯與布局UI,雖然Litho也提供了對Stetho的支持,借助於Chrome開發者工具對界面進行調試,不過使用起來並沒有那么方便。
3.2 異步布局
Android系統在繪制時為了防止頁面錯亂,頁面所有View的測量(Measure)、布局(Layout)以及繪制(Draw)都是在UI線程中完成的。當頁面UI非常復雜、視圖層級較深時,難免Measure和Layout的時間會過長,從而導致頁面渲染時候丟幀出現卡頓情況。Litho為解決該問題,提出了異步布局的思想,利用CPU的閑置時間提前在異步線程中完成Measure和Layout的過程,僅在UI線程中完成繪制工作。當然,Litho只是提供了異步布局的能力,它主要使用在RecyclerView等可以提前知道下一個視圖長什么樣子的場景。
3.2.1 異步布局原理剖析
針對RecyclerView等滑動列表,由於可以提前知道接下來要展示的一個甚至多個條目的視圖樣式,所以只要提前創建好下一個或多個條目的視圖,就可以提前完成視圖的布局工作。
那么Android原生為什么不支持異步布局呢?主要有以下兩個原因:
-
View的屬性是可變的,只要屬性發生變化就可能導致布局變化,因此需要重新計算布局,那么提前計算布局的意義就不大了。而Litho組件的屬性是不可變的,所以對於一個組件來說,它的布局計算結果是唯一且不變的。
-
提前異步布局就意味着要提前創建好接下來要用到的一個或者多個條目的視圖,而Android原生的View作為視圖單元,不僅包含一個視圖的所有屬性,而且還負責視圖的繪制工作。如果要在繪制前提前去計算布局,就需要預先去持有大量未展示的View實例,大大增加內存占用。反觀Litho的組件則沒有這個問題,Litho的組件只是視圖屬性的一個集合,僅負責計算布局,繪制工作由指定的繪制單元來完成,相比與傳統的View顯然Litho的組件要輕量的多。所以在Litho中,提前創建好接下來要用到的多個條目的組件,並不會帶來性能問題,甚至還可以直接把組件當成滑動列表的數據源。如下圖所示:
3.3 扁平化的視圖
使用Litho布局,我們可以得到一個極致扁平的視圖效果。它可以減少渲染時的遞歸調用,加快渲染速度。
下面是同一個視圖在Android和Litho實現下的視圖層級效果對比。可以看到,同樣的樣式,使用Litho實現的布局要比使用Android原生實現的布局更加扁平。
3.3.1 扁平化視圖原理剖析
Litho使用Flexbox來創建布局,最終生成帶有層級結構的組件樹。然后Litho對布局層級進行了兩次優化。
-
使用了Yoga來進行布局計算,Yoga會將Flexbox的相對布局轉成絕對布局。經過Yoga處理后的布局沒有了原來的布局層級,變成了只有一層。雖然不能解決過度繪制的問題,但是可以有效地減少渲染時的遞歸調用。
-
前面介紹過Litho的視圖渲染由繪制單元來完成,繪制單元可以是View或者更加輕量的Drawable,Litho自己實現了一系列掛載Drawable的基本視圖組件。通過使用Drawable可以減少內存占用,同時相比於View,Android無法檢查出Drawable的視圖層級,這樣可以使視圖效果看起來更加扁平。
原理如下圖所示,Litho會先把組件樹拍平成沒有層級的列表,然后使用Drawable來繪制對應的視圖單元。
Litho使用Drawable代替View能帶來多少好處呢?Drawable和View的區別在於前者不能和用戶交互,只能展示,因此Drawable不會像View那樣持有很多變量和引用,所以Drawable比View從內存上看要輕量很多。舉個例子:50個同樣展示“Hello world”的TextView和TextDrawable在內存占比上,前者幾乎是后者的8倍。對比圖如下,Shallow Size表示對象自身占用的內存大小。
3.3.2 繪制單元的降級策略
由於Drawable不具有交互能力,所以對於使用Drawable無法實現的交互場景,Litho會自動降級成View。主要有以下幾種場景:
- 有監聽點擊事件。
- 限制子視圖繪出父布局。
- 有監聽焦點變化。
- 有設置Tag。
- 有監聽觸摸事件。
- 有光影效果。
對於以上場景的使用請仔細考慮,過多的使用會導致Litho的層級優化效果變差。
3.3.3 對比Android的約束布局
為了解決布局嵌套問題,Android推出了約束布局(ConstraintLayout),使用約束布局也可以達到扁平化視圖的目的,那么使用Litho的好處是什么呢?
Litho可以更好地實現復雜布局。約束布局雖然可以實現扁平效果,但是它使用了大量的約束來固定視圖的位置。隨着布局復雜程度的增加,約束條件變得越來越多,可讀性也變得越來越差。而Litho則是對Flexbox布局進行的扁平化處理,所以實際使用的還是Flexbox布局,對於復雜的布局Flexbox布局可讀性更高。
3.4 細粒度的復用
Litho中的所有組件都可以被回收,並在任何位置進行復用。這種細粒度的復用方式可以極大地提高內存使用率,尤其適用於復雜滑動列表,內存優化非常明顯。
3.4.1 原生RecyclerView復用原理剖析
原生的RecyclerView視圖按模板類型進行存儲並復用,也就是說模板類型越多,所需存儲的模板種類也就越多,導致內存占用越來越大。原理如下圖。滑出屏幕的itemType1和itemType2都會在Recycler緩存池保存,等待后面滑進屏幕的條目的復用。
3.4.2 細粒度復用優化內存原理剖析
在Litho中,item在回收前,會把LithoView中掛載的各個繪制單元拆分出來(解綁),由Litho自己的緩存池去分類回收,在展示前由LithoView按照組件樹的樣式組裝(掛載)各個繪制單元,這樣就達到了細粒度復用的目的。原理如下圖。滑出屏幕的itemType1會被拆分成一個個的視圖單元。LithoView容器由Recycler緩存池回收,其他視圖單元由Litho的緩存池分類回收。
使用細粒度復用的RecyclerView的緩存池不再需要區分模板類型來緩存大量的視圖模板,只需要緩存LithoView容器。細粒度回收的視圖單元數量要遠遠小於原來緩存在各個視圖模板中的視圖單元數量。
4. 實踐
美團對Litho進行了二次開發,在美團的MTFlexbox動態化實現方案(簡稱動態布局)中把Litho作為底層UI渲染引擎來使用。通過動態布局的預覽工具,為Litho提供實時預覽能力,同時可以有效發揮Litho的性能優化效果。
目前Litho+動態布局的實現方案已經應用在了美團App中,給美團App帶來了不錯的性能提升。后續博主會詳細介紹Litho+動態布局在美團性能優化的實踐方案。
4.1 內存數據
由於Litho中使用了大量Drawable替換View,並且實現了視圖單元的細粒度復用,因此復雜列表滑動時內存優化比較明顯。美團首頁內存占用隨滑動頁數變化走勢圖如下。隨着一頁一頁地滑動,內存優化了30M以上。(數據采集自Vivo x20手機內存占用情況)
4.2 FPS數據
FPS的提升主要得益於Litho的異步布局能力,提前計算布局可以減少滑動時的幀率波動,所以滑動過程較平穩,不會有高低起伏的卡頓感。(數據采集自魅藍2手機一段時間內連續fps的波動情況)
5. 總結
Litho相對於傳統Android是顛覆式的,它采用了React的思路,使用聲明式的API來編寫UI。相比於傳統Android,確實在性能優化上有很大的進步,但是如果完全使用Litho開發一款應用,需要自己實現很多組件,而Litho的組件需要在編譯時生成,實時預覽方面也有所欠缺。相對於直接使用Litho的高成本,把Litho封裝成Flexbox布局的底層渲染引擎是個不錯的選擇。
6. 參考資料
7. 作者簡介
- 何少寬,美團Android開發工程師,2015年加入美團,負責美團平台終端業務研發工作。
- 張穎,美團Android開發工程師,2017年加入美團,負責美團平台終端業務研發工作。