說一說Facebook開源的Litho

Facebook總是能給業(yè)界帶來一些驚喜手报,最近開源的Litho是一個(gè)高效構(gòu)建Android UI的聲名式框架(declarative framework for building efficient UIs on Android)。Litho的出現(xiàn)可以追溯到Facebook去年的一篇博文Components for Android: A declarative framework for efficient UIs琼牧,中文譯文:Components for Android: 一個(gè)高效的聲明式UI框架验懊。

Litho最初的目的是為了解決復(fù)雜列表的高效渲染和內(nèi)存使用問題。之前我也寫過相關(guān)的文章Android ListView中復(fù)雜數(shù)據(jù)流的高效渲染Android復(fù)雜數(shù)據(jù)流的“高效”渲染攀痊。之前的思路是把列表中的邏輯Item拆分為可復(fù)用的更小單元,然后利用ListView或者RecyclerView自帶的緩存策略達(dá)到節(jié)約內(nèi)存的目的拄显。Litho采用了更激進(jìn)的方式苟径,放棄使用原生的View,使用了自定義的View和布局躬审,通過極高的View復(fù)用率節(jié)約了內(nèi)存使用棘街,同時(shí)采用了非常高效的布局策略,使得繪制更加迅速承边,滑動(dòng)更加流暢遭殉。Litho的使用對(duì)于復(fù)雜數(shù)據(jù)流展示優(yōu)化可以說是顛覆式的,非常佩服他們的思路和實(shí)現(xiàn)博助。當(dāng)然個(gè)人認(rèn)為Litho的目的不僅僅是解決上述問題恩沽,作為一個(gè)UI渲染框架完全可以代替目前Android中的渲染實(shí)現(xiàn)。但是就目前Litho的情況來看翔始,離完全替代還有很長的距離罗心,之后我會(huì)說明自己的想法里伯。

Litho 概述

先來看下官方上對(duì)于Litho高效渲染的介紹,主要介紹了4個(gè)特征:

  • 聲名式組件
    Litho采用聲名式的Api來定義UI組件渤闷,我們只需要基于一組不可變輸入( immutable inputs)描述UI的布局疾瓮,剩下的事情就可以交給Litho了。
    聲名式布局讓我們用一種描述式的方式構(gòu)建組件:
@LayoutSpec
public class FeedItemComponentSpec {

  @OnCreateLayout
  static ComponentLayout onCreateLayout(
      ComponentContext c,
      @Prop final Artist artist,
      @Prop final RecyclerBinder binder) {
    return Column.create(c)
        .child(
            Column.create(c)
                .child(artist.images.length == 1 ?
                    SingleImageComponent.create(c)
                        .image(artist.images[0])
                        .aspectRatio(2)
                        .withLayout() :
                    Recycler.create(c)
                        .binder(binder)
                        .withLayout().flexShrink(0)
                        .aspectRatio(2))
                .child(
                    TitleComponent.create(c)
                        .title(artist.name))
                .child(
                    ActionsComponent.create(c)))
        .child(
            FooterComponent.create(c)
                .text(artist.biography))
        .build();
  }
}

看代碼非常簡單易懂飒箭,而且Litho使用Flexbox 對(duì)組件進(jìn)行布局狼电,有前端經(jīng)驗(yàn)的同學(xué)知道Flexbox布局非常的方便。Litho提供的Image使用了fresco弦蹂,也非常棒肩碟。

  • ** 異步布局**
    Litho可以異步進(jìn)行measure和layout,不需要在UI線程中凸椿。

  • 扁平化的View
    Litho 使用了Yoga 來進(jìn)行布局削祈,可以減少UI中繪制ViewGroup的數(shù)量。
    在Android中脑漫,為了避免界面錯(cuò)亂髓抑,所有的UI繪制和操作都是在UI線程中,對(duì)于比較復(fù)雜的界面优幸,繪制過程過長就會(huì)引起界面卡頓吨拍,掉幀,之前的優(yōu)化基本都是通過減少布局層級(jí)网杆、避免過度繪制等手段進(jìn)行優(yōu)化羹饰。Litho使用異步布局就避免了在UI線程中執(zhí)行繁重的measure和layout過程。Litho使用Yoga可以進(jìn)一步優(yōu)化布局碳却,我們?cè)谏降腢I布局中只是指定了布局的樣子队秩,并不是實(shí)際的布局,Litho可以進(jìn)一步優(yōu)化追城,我們知道展示UI可以使用View或者更加輕量級(jí)的Drawable刹碾,Litho可以根據(jù)需要裝載View或者Drawable,相比Android原生的布局座柱,Litho使用了更多的drawable迷帜,這會(huì)讓試圖渲染更快速。如圖:


    當(dāng)我們使用開發(fā)者工具中的顯示布局時(shí)色洞,可以看到圖中的所有元素是渲染在一個(gè)View上的戏锹。

  • 細(xì)粒度的復(fù)用
    所有組件包括text和image等可以被回收并在UI的所有位置進(jìn)行復(fù)用。
    Litho組件的全局復(fù)用火诸,可以極大地提高內(nèi)存使用率锦针,在展示復(fù)雜列表時(shí),內(nèi)存使用會(huì)有明顯的區(qū)別。

看完Litho的四個(gè)特征奈搜,相信每個(gè)Android開發(fā)者都是非常驚喜的悉盆。

Litho的思路

本文不會(huì)深入到Litho的代碼細(xì)節(jié),主要介紹自己對(duì)于Litho的分析與想法馋吗。

1. 組件化

這里所說的組件化不是工程上的組件化焕盟,而是布局上的組件化。Litho的靈感應(yīng)該是來源于React宏粤,以組件的方式組織布局脚翘。

傳統(tǒng)的Android使用xml進(jìn)行布局,名義上是mvc中的view绍哎,但是在功能上非常弱来农,幾乎沒有邏輯處理,之后推出的data binding使得功能上稍有加強(qiáng)崇堰,但是功能依然比較弱沃于。當(dāng)然不可否認(rèn)這種界面布局與邏輯代碼分離的設(shè)計(jì)思路也是非常棒的。在傳統(tǒng)開發(fā)中赶袄,把界面布局和邏輯分離是最合理的方案揽涮,但是有些時(shí)候也稍顯笨重抠藕。litho的設(shè)計(jì)思路是放棄了xml布局饿肺,而是使用java代碼來構(gòu)建界面組件并進(jìn)行布局,使用組件的方式連接了邏輯和界面布局盾似,與React在前端上的設(shè)計(jì)有相同的思路敬辣。Litho包含兩種組件:

Mount spec: 可以獨(dú)立渲染一個(gè)view或者drawable,擁有自己的生命周期
Layout spec:可以組織其他組件構(gòu)成一個(gè)布局零院,類似于Android中的ViewGroup溉跃。

使用litho后每一個(gè)界面都是組件化的,合理設(shè)計(jì)組件告抄,可以增加組件的復(fù)用性撰茎,同時(shí)組件本身props、state的設(shè)計(jì)是的自身功能比較完整打洼,比傳統(tǒng)意義上的xml中定義布局要強(qiáng)大很多龄糊。

2. 扁平化與事件處理

我們知道,Android中的View不止可以展示募疮,還可以與用戶進(jìn)行交互炫惩,如點(diǎn)擊、滑動(dòng)等等阿浓。Litho使用yoga布局他嚷,可以節(jié)約內(nèi)存占用和繪制時(shí)間,但是這種情況下不能與用戶進(jìn)行交互了。Litho單獨(dú)對(duì)Event進(jìn)行處理筋蓖,可以處理點(diǎn)擊卸耘、長按、碰觸(touch)事件粘咖,與View元素對(duì)事件處理略有不同鹊奖,但可以滿足基本的需求。

關(guān)于Litho的一些想法

1. 關(guān)于界面調(diào)試

Android開發(fā)中我們?cè)趚ml中定義布局涂炎,Android studio有強(qiáng)大的預(yù)覽功能忠聚,所見即所得的體驗(yàn)很棒。Litho提供了對(duì)于Stetho 對(duì)支持唱捣,可以利用chrome的開發(fā)者工具對(duì)界面進(jìn)行調(diào)試:

其實(shí)相比xml两蟀,這種方式并不方便,在chrome只是輔助調(diào)試震缭,最終還是根據(jù)調(diào)試情況手動(dòng)在代碼中更新赂毯。

2. 開發(fā)體驗(yàn)

在寫界面時(shí),我們要合理地對(duì)界面進(jìn)行拆分拣宰,使用多個(gè)組件組合成為一個(gè)完整對(duì)界面党涕。一個(gè)組件定義如下:

@LayoutSpec
public class FeedItemComponentSpec {

  @OnCreateLayout
  static ComponentLayout onCreateLayout(
      ComponentContext c,
      @Prop final Artist artist,
      @Prop final RecyclerBinder binder) {
    return Column.create(c)
        .child(
            Column.create(c)
                .child(artist.images.length == 1 ?
                    SingleImageComponent.create(c)
                        .image(artist.images[0])
                        .aspectRatio(2)
                        .withLayout() :
                    Recycler.create(c)
                        .binder(binder)
                        .withLayout().flexShrink(0)
                        .aspectRatio(2))
                .child(
                    TitleComponent.create(c)
                        .title(artist.name))
                .child(
                    ActionsComponent.create(c)))
        .child(
            FooterComponent.create(c)
                .text(artist.biography))
        .build();
  }
}

例子中我們定義了一個(gè)組件,但是我們?cè)谶壿嫶a中并不會(huì)引用到這段代碼巡社。Litho會(huì)根據(jù)componentSpec生的生成真正的component代碼:

public final class FeedItemComponent extends ComponentLifecycle {
  private static FeedItemComponent sInstance = null;

  private static final Pools.SynchronizedPool<Builder> mBuilderPool = new Pools.SynchronizedPool<Builder>(2);

  private FeedItemComponentSpec mSpec = new FeedItemComponentSpec();

  private FeedItemComponent() {
  }

  public static synchronized FeedItemComponent get() {
    if (sInstance == null) {
      sInstance = new FeedItemComponent();
    }
    return sInstance;
  }

  @Override
  protected ComponentLayout onCreateLayout(ComponentContext c, Component _abstractImpl) {
    FeedItemComponentImpl _impl = (FeedItemComponentImpl) _abstractImpl;
    ComponentLayout _result = (ComponentLayout) mSpec.onCreateLayout(
      (ComponentContext) c,
      (Artist) _impl.artist,
      (RecyclerBinder) _impl.binder);
    return _result;
  }

  private static Builder newBuilder(ComponentContext context, int defStyleAttr, int defStyleRes,
      FeedItemComponentImpl feedItemComponentImpl) {
    Builder builder = mBuilderPool.acquire();
    if (builder == null) {
      builder = new Builder();
    }
    builder.init(context, defStyleAttr, defStyleRes, feedItemComponentImpl);
    return builder;
  }

  public static Builder create(ComponentContext context, int defStyleAttr, int defStyleRes) {
    return newBuilder(context, defStyleAttr, defStyleRes, new FeedItemComponentImpl());
  }

  public static Builder create(ComponentContext context) {
    return create(context, 0, 0);
  }

  private static class FeedItemComponentImpl extends Component<FeedItemComponent> implements Cloneable {
    @Prop
    Artist artist;

    @Prop
    RecyclerBinder binder;

    private FeedItemComponentImpl() {
      super(get());
    }

    @Override
    public String getSimpleName() {
      return "FeedItemComponent";
    }

    @Override
    public boolean equals(Object other) {
      if (this == other) {
        return true;
      }
      if (other == null || getClass() != other.getClass()) {
        return false;
      }
      FeedItemComponentImpl feedItemComponentImpl = (FeedItemComponentImpl) other;
      if (this.getId() == feedItemComponentImpl.getId()) {
        return true;
      }
      if (artist != null ? !artist.equals(feedItemComponentImpl.artist) : feedItemComponentImpl.artist != null) {
        return false;
      }
      if (binder != null ? !binder.equals(feedItemComponentImpl.binder) : feedItemComponentImpl.binder != null) {
        return false;
      }
      return true;
    }
  }

  public static class Builder extends Component.Builder<FeedItemComponent> {
    private static final String[] REQUIRED_PROPS_NAMES = new String[] {"artist", "binder"};

    private static final int REQUIRED_PROPS_COUNT = 2;

    FeedItemComponentImpl mFeedItemComponentImpl;

    ComponentContext mContext;

    private BitSet mRequired = new BitSet(REQUIRED_PROPS_COUNT);

    private void init(ComponentContext context, int defStyleAttr, int defStyleRes,
        FeedItemComponentImpl feedItemComponentImpl) {
      super.init(context, defStyleAttr, defStyleRes, feedItemComponentImpl);
      mFeedItemComponentImpl = feedItemComponentImpl;
      mContext = context;
      mRequired.clear();
    }

    public Builder artist(Artist artist) {
      this.mFeedItemComponentImpl.artist = artist;
      mRequired.set(0);
      return this;
    }

    public Builder binder(RecyclerBinder binder) {
      this.mFeedItemComponentImpl.binder = binder;
      mRequired.set(1);
      return this;
    }

    public Builder key(String key) {
      super.setKey(key);
      return this;
    }

    @Override
    public Component<FeedItemComponent> build() {
      if (mRequired != null && mRequired.nextClearBit(0) < REQUIRED_PROPS_COUNT) {
        List<String> missingProps = new ArrayList<String>();
        for (int i = 0; i < REQUIRED_PROPS_COUNT; i++) {
          if (!mRequired.get(i)) {
            missingProps.add(REQUIRED_PROPS_NAMES[i]);
          }
        }
        throw new IllegalStateException("The following props are not marked as optional and were not supplied: " + Arrays.toString(missingProps.toArray()));
      }
      FeedItemComponentImpl feedItemComponentImpl = mFeedItemComponentImpl;
      release();
      return feedItemComponentImpl;
    }

    @Override
    protected void release() {
      super.release();
      mFeedItemComponentImpl = null;
      mContext = null;
      mBuilderPool.release(this);
    }
  }
}

所以有個(gè)弊端是我們每次修改一個(gè)component文件都需要build一次生成可用的代碼膛堤。對(duì)于開發(fā)來說體驗(yàn)并不友好。

另外我們可以看下Litho提供的可用組件:

所以如果完全使用Litho來開發(fā)一款應(yīng)用晌该,需要自己實(shí)現(xiàn)的控件會(huì)非常多肥荔。個(gè)人認(rèn)為雖然Litho有諸多好處,對(duì)于一般的應(yīng)用來講朝群,常規(guī)的優(yōu)化手段已經(jīng)完全可以滿足需求燕耿。Litho還是更適用于對(duì)性能優(yōu)化有強(qiáng)烈需求的應(yīng)用。

3. Litho組件化的思考

Litho使用了類似React的設(shè)計(jì)思路姜胖,而React社區(qū)非常的活躍誉帅。如果Litho的未來發(fā)展的比較良好,可以支撐常規(guī)應(yīng)用開發(fā)時(shí)右莱,React社區(qū)的很多經(jīng)驗(yàn)就可以借鑒過來蚜锨,如Redux等工具的實(shí)現(xiàn)等。

最后

對(duì)于Litho的使用還是一個(gè)比較初級(jí)的體驗(yàn)隧出,文中如有錯(cuò)誤的地方踏志,煩請(qǐng)指出,非常感謝胀瞪。

推薦閱讀:

Android 組件化開發(fā)原理和配置

Android組件化開發(fā)實(shí)踐

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末针余,一起剝皮案震驚了整個(gè)濱河市饲鄙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌圆雁,老刑警劉巖忍级,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異伪朽,居然都是意外死亡轴咱,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門烈涮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來朴肺,“玉大人,你說我怎么就攤上這事坚洽「旮澹” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵讶舰,是天一觀的道長鞍盗。 經(jīng)常有香客問我,道長跳昼,這世上最難降的妖魔是什么般甲? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮鹅颊,結(jié)果婚禮上敷存,老公的妹妹穿的比我還像新娘。我一直安慰自己挪略,他們只是感情好历帚,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布滔岳。 她就那樣靜靜地躺著杠娱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪谱煤。 梳的紋絲不亂的頭發(fā)上摊求,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音刘离,去河邊找鬼室叉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛硫惕,可吹牛的內(nèi)容都是我干的茧痕。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼恼除,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼踪旷!你這毒婦竟也來了曼氛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤令野,失蹤者是張志新(化名)和其女友劉穎舀患,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體气破,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡聊浅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了现使。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片低匙。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖碳锈,靈堂內(nèi)的尸體忽然破棺而出努咐,到底是詐尸還是另有隱情,我是刑警寧澤殴胧,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布渗稍,位于F島的核電站,受9級(jí)特大地震影響团滥,放射性物質(zhì)發(fā)生泄漏竿屹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一灸姊、第九天 我趴在偏房一處隱蔽的房頂上張望拱燃。 院中可真熱鬧,春花似錦力惯、人聲如沸碗誉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽哮缺。三九已至,卻和暖如春甲喝,著一層夾襖步出監(jiān)牢的瞬間尝苇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國打工埠胖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留糠溜,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓直撤,卻偏偏與公主長得像非竿,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子谋竖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,498評(píng)論 25 707
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點(diǎn)贊按鈕進(jìn)度條TabLayout圖標(biāo)下拉刷新...
    皇小弟閱讀 46,708評(píng)論 22 664
  • 從小到大發(fā)現(xiàn)進(jìn)步最快的就是剛開始學(xué)某樣?xùn)|西的時(shí)候悄雅,因?yàn)檫@個(gè)時(shí)候我們對(duì)任何事情都保持著好奇心,每天都有十萬個(gè)問什么等...
    環(huán)盈閱讀 241評(píng)論 0 1
  • 一 從前有一只小兔子 名字叫兔醬 今天天氣很晴朗 暖暖的陽光君透過葉片勻勻地灑在兔醬小屋前的草坪上 于是兔醬決定出...
    梵鶴閱讀 355評(píng)論 0 0
  • Junit的所有測試方法都是以@Test修飾铁蹈,以public void 開頭宽闲。如下: @BeforeClass &...
    wangmin閱讀 634評(píng)論 0 1