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)指出,非常感謝胀瞪。
推薦閱讀: