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

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+動態佈局在美團性能優化的實踐方案。

使用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. 參考資料

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

7. 作者簡介

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章