基本功 | Litho的使用及原理剖析

美團技術團隊發表於2019-03-15

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框架。架構圖如下:

基本功 | Litho的使用及原理剖析

應用層:上層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的使用及原理剖析

除了上述兩種元件型別,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中,提前建立好接下來要用到的多個條目的元件,並不會帶來效能問題,甚至還可以直接把元件當成滑動列表的資料來源。如下圖所示:

基本功 | Litho的使用及原理剖析

3.3 扁平化的檢視

使用Litho佈局,我們可以得到一個極致扁平的檢視效果。它可以減少渲染時的遞迴呼叫,加快渲染速度。

下面是同一個檢視在Android和Litho實現下的檢視層級效果對比。可以看到,同樣的樣式,使用Litho實現的佈局要比使用Android原生實現的佈局更加扁平。

基本功 | Litho的使用及原理剖析

3.3.1 扁平化檢視原理剖析

Litho使用Flexbox來建立佈局,最終生成帶有層級結構的元件樹。然後Litho對佈局層級進行了兩次優化。

  • 使用了Yoga來進行佈局計算,Yoga會將Flexbox的相對佈局轉成絕對佈局。經過Yoga處理後的佈局沒有了原來的佈局層級,變成了只有一層。雖然不能解決過度繪製的問題,但是可以有效地減少渲染時的遞迴呼叫。

  • 前面介紹過Litho的檢視渲染由繪製單元來完成,繪製單元可以是View或者更加輕量的Drawable,Litho自己實現了一系列掛載Drawable的基本檢視元件。通過使用Drawable可以減少記憶體佔用,同時相比於View,Android無法檢查出Drawable的檢視層級,這樣可以使檢視效果看起來更加扁平。

原理如下圖所示,Litho會先把元件樹拍平成沒有層級的列表,然後使用Drawable來繪製對應的檢視單元。

基本功 | Litho的使用及原理剖析

Litho使用Drawable代替View能帶來多少好處呢?Drawable和View的區別在於前者不能和使用者互動,只能展示,因此Drawable不會像View那樣持有很多變數和引用,所以Drawable比View從記憶體上看要輕量很多。舉個例子:50個同樣展示“Hello world”的TextView和TextDrawable在記憶體佔比上,前者幾乎是後者的8倍。對比圖如下,Shallow Size表示物件自身佔用的記憶體大小。

基本功 | Litho的使用及原理剖析

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快取池儲存,等待後面滑進螢幕的條目的複用。

基本功 | Litho的使用及原理剖析

3.4.2 細粒度複用優化記憶體原理剖析

在Litho中,item在回收前,會把LithoView中掛載的各個繪製單元拆分出來(解綁),由Litho自己的快取池去分類回收,在展示前由LithoView按照元件樹的樣式組裝(掛載)各個繪製單元,這樣就達到了細粒度複用的目的。原理如下圖。滑出螢幕的itemType1會被拆分成一個個的檢視單元。LithoView容器由Recycler快取池回收,其他檢視單元由Litho的快取池分類回收。

基本功 | Litho的使用及原理剖析

使用細粒度複用的RecyclerView的快取池不再需要區分模板型別來快取大量的檢視模板,只需要快取LithoView容器。細粒度回收的檢視單元數量要遠遠小於原來快取在各個檢視模板中的檢視單元數量。

4. 實踐

美團對Litho進行了二次開發,在美團的MTFlexbox動態化實現方案(簡稱動態佈局)中把Litho作為底層UI渲染引擎來使用。通過動態佈局的預覽工具,為Litho提供實時預覽能力,同時可以有效發揮Litho的效能優化效果。

目前Litho+動態佈局的實現方案已經應用在了美團App中,給美團App帶來了不錯的效能提升。後續博主會詳細介紹Litho+動態佈局在美團效能優化的實踐方案。

使用Litho+動態佈局實現的部分卡片

4.1 記憶體資料

由於Litho中使用了大量Drawable替換View,並且實現了檢視單元的細粒度複用,因此複雜列表滑動時記憶體優化比較明顯。美團首頁記憶體佔用隨滑動頁數變化走勢圖如下。隨著一頁一頁地滑動,記憶體優化了30M以上。(資料採集自Vivo x20手機記憶體佔用情況)

基本功 | Litho的使用及原理剖析

4.2 FPS資料

FPS的提升主要得益於Litho的非同步佈局能力,提前計算佈局可以減少滑動時的幀率波動,所以滑動過程較平穩,不會有高低起伏的卡頓感。(資料採集自魅藍2手機一段時間內連續fps的波動情況)

基本功 | Litho的使用及原理剖析

5. 總結

Litho相對於傳統Android是顛覆式的,它採用了React的思路,使用宣告式的API來編寫UI。相比於傳統Android,確實在效能優化上有很大的進步,但是如果完全使用Litho開發一款應用,需要自己實現很多元件,而Litho的元件需要在編譯時生成,實時預覽方面也有所欠缺。相對於直接使用Litho的高成本,把Litho封裝成Flexbox佈局的底層渲染引擎是個不錯的選擇。

6. 參考資料

  1. Litho官網
  2. 說一說 Facebook 開源的 Litho
  3. React官網
  4. Yoga官網

7. 作者簡介

  • 何少寬,美團Android開發工程師,2015年加入美團,負責美團平臺終端業務研發工作。
  • 張穎,美團Android開發工程師,2017年加入美團,負責美團平臺終端業務研發工作。

基本功 | Litho的使用及原理剖析

相關文章