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 mBuilderPool = new Pools.SynchronizedPool(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 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 {
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 build() {
if (mRequired != null && mRequired.nextClearBit(0) < REQUIRED_PROPS_COUNT) {
List missingProps = new ArrayList();
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的使用还是一个比较初级的体验,文中如有错误的地方,烦请指出,非常感谢。
推荐阅读:
Android 组件化开发原理和配置
Android组件化开发实践