Facebook總是能給業界帶來一些驚喜,最近開源的Litho是一個高效構建Android UI的聲名式框架(declarative framework for building efficient UIs on Android)。Litho的出現可以追溯到Facebook去年的一篇博文Components for Android: A declarative framework for efficient UIs,中文譯文:Components for Android: 一個高效的宣告式UI框架。
Litho最初的目的是為了解決複雜列表的高效渲染和記憶體使用問題。之前我也寫過相關的文章Android ListView中複雜資料流的高效渲染,Android複雜資料流的“高效”渲染。之前的思路是把列表中的邏輯Item拆分為可複用的更小單元,然後利用ListView或者RecyclerView自帶的快取策略達到節約記憶體的目的。Litho採用了更激進的方式,放棄使用原生的View,使用了自定義的View和佈局,通過極高的View複用率節約了記憶體使用,同時採用了非常高效的佈局策略,使得繪製更加迅速,滑動更加流暢。Litho的使用對於複雜資料流展示優化可以說是顛覆式的,非常佩服他們的思路和實現。當然個人認為Litho的目的不僅僅是解決上述問題,作為一個UI渲染框架完全可以代替目前Android中的渲染實現。但是就目前Litho的情況來看,離完全替代還有很長的距離,之後我會說明自己的想法。
Litho 概述
先來看下官方上對於Litho高效渲染的介紹,主要介紹了4個特徵:
聲名式元件
Litho採用聲名式的Api來定義UI元件,我們只需要基於一組不可變輸入( immutable inputs)描述UI的佈局,剩下的事情就可以交給Litho了。
聲名式佈局讓我們用一種描述式的方式構建元件:@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 對元件進行佈局,有前端經驗的同學知道Flexbox佈局非常的方便。Litho提供的Image使用了fresco,也非常棒。
非同步佈局
Litho可以非同步進行measure和layout,不需要在UI執行緒中。扁平化的View
Litho 使用了Yoga 來進行佈局,可以減少UI中繪製ViewGroup的數量。
在Android中,為了避免介面錯亂,所有的UI繪製和操作都是在UI執行緒中,對於比較複雜的介面,繪製過程過長就會引起介面卡頓,掉幀,之前的優化基本都是通過減少佈局層級、避免過度繪製等手段進行優化。Litho使用非同步佈局就避免了在UI執行緒中執行繁重的measure和layout過程。Litho使用Yoga可以進一步優化佈局,我們在生命式的UI佈局中只是指定了佈局的樣子,並不是實際的佈局,Litho可以進一步優化,我們知道展示UI可以使用View或者更加輕量級的Drawable,Litho可以根據需要裝載View或者Drawable,相比Android原生的佈局,Litho使用了更多的drawable,這會讓試圖渲染更快速。如圖:
當我們使用開發者工具中的顯示佈局時,可以看到圖中的所有元素是渲染在一個View上的。細粒度的複用
所有元件包括text和image等可以被回收並在UI的所有位置進行復用。
Litho元件的全域性複用,可以極大地提高記憶體使用率,在展示覆雜列表時,記憶體使用會有明顯的區別。
看完Litho的四個特徵,相信每個Android開發者都是非常驚喜的。
Litho的思路
本文不會深入到Litho的程式碼細節,主要介紹自己對於Litho的分析與想法。
1. 元件化
這裡所說的元件化不是工程上的元件化,而是佈局上的元件化。Litho的靈感應該是來源於React,以元件的方式組織布局。
傳統的Android使用xml進行佈局,名義上是mvc中的view,但是在功能上非常弱,幾乎沒有邏輯處理,之後推出的data binding使得功能上稍有加強,但是功能依然比較弱。當然不可否認這種介面佈局與邏輯程式碼分離的設計思路也是非常棒的。在傳統開發中,把介面佈局和邏輯分離是最合理的方案,但是有些時候也稍顯笨重。litho的設計思路是放棄了xml佈局,而是使用java程式碼來構建介面元件並進行佈局,使用元件的方式連線了邏輯和介面佈局,與React在前端上的設計有相同的思路。Litho包含兩種元件:
Mount spec: 可以獨立渲染一個view或者drawable,擁有自己的生命週期
Layout spec:可以組織其他元件構成一個佈局,類似於Android中的ViewGroup。
使用litho後每一個介面都是元件化的,合理設計元件,可以增加元件的複用性,同時元件本身props、state的設計是的自身功能比較完整,比傳統意義上的xml中定義佈局要強大很多。
2. 扁平化與事件處理
我們知道,Android中的View不止可以展示,還可以與使用者進行互動,如點選、滑動等等。Litho使用yoga佈局,可以節約記憶體佔用和繪製時間,但是這種情況下不能與使用者進行互動了。Litho單獨對Event進行處理,可以處理點選、長按、碰觸(touch)事件,與View元素對事件處理略有不同,但可以滿足基本的需求。
關於Litho的一些想法
1. 關於介面除錯
Android開發中我們在xml中定義佈局,Android studio有強大的預覽功能,所見即所得的體驗很棒。Litho提供了對於Stetho 對支援,可以利用chrome的開發者工具對介面進行除錯:
其實相比xml,這種方式並不方便,在chrome只是輔助除錯,最終還是根據除錯情況手動在程式碼中更新。
2. 開發體驗
在寫介面時,我們要合理地對介面進行拆分,使用多個元件組合成為一個完整對介面。一個元件定義如下:
@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會根據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);
}
}
}複製程式碼
所以有個弊端是我們每次修改一個component檔案都需要build一次生成可用的程式碼。對於開發來說體驗並不友好。
另外我們可以看下Litho提供的可用元件:
所以如果完全使用Litho來開發一款應用,需要自己實現的控制元件會非常多。個人認為雖然Litho有諸多好處,對於一般的應用來講,常規的優化手段已經完全可以滿足需求。Litho還是更適用於對效能優化有強烈需求的應用。
3. Litho元件化的思考
Litho使用了類似React的設計思路,而React社群非常的活躍。如果Litho的未來發展的比較良好,可以支撐常規應用開發時,React社群的很多經驗就可以借鑑過來,如Redux等工具的實現等。
最後
對於Litho的使用還是一個比較初級的體驗,文中如有錯誤的地方,煩請指出,非常感謝。
推薦閱讀: