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框架。javascript
Litho最初的目的是为了解决复杂列表的高效渲染和内存使用问题。以前我也写过相关的文章Android ListView中复杂数据流的高效渲染,Android复杂数据流的“高效”渲染。以前的思路是把列表中的逻辑Item拆分为可复用的更小单元,而后利用ListView或者RecyclerView自带的缓存策略达到节约内存的目的。Litho采用了更激进的方式,放弃使用原生的View,使用了自定义的View和布局,经过极高的View复用率节约了内存使用,同时采用了很是高效的布局策略,使得绘制更加迅速,滑动更加流畅。Litho的使用对于复杂数据流展现优化能够说是颠覆式的,很是佩服他们的思路和实现。固然我的认为Litho的目的不只仅是解决上述问题,做为一个UI渲染框架彻底能够代替目前Android中的渲染实现。可是就目前Litho的状况来看,离彻底替代还有很长的距离,以后我会说明本身的想法。css
先来看下官方上对于Litho高效渲染的介绍,主要介绍了4个特征:html
声名式组件
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,也很是棒。java
异步布局
Litho能够异步进行measure和layout,不须要在UI线程中。android
扁平化的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,这会让试图渲染更快速。如图:
git
细粒度的复用
全部组件包括text和image等能够被回收并在UI的全部位置进行复用。
Litho组件的全局复用,能够极大地提升内存使用率,在展现复杂列表时,内存使用会有明显的区别。github
看完Litho的四个特征,相信每一个Android开发者都是很是惊喜的。chrome
本文不会深刻到Litho的代码细节,主要介绍本身对于Litho的分析与想法。缓存
这里所说的组件化不是工程上的组件化,而是布局上的组件化。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中定义布局要强大不少。
咱们知道,Android中的View不止能够展现,还能够与用户进行交互,如点击、滑动等等。Litho使用yoga布局,能够节约内存占用和绘制时间,可是这种状况下不能与用户进行交互了。Litho单独对Event进行处理,能够处理点击、长按、碰触(touch)事件,与View元素对事件处理略有不一样,但能够知足基本的需求。
Android开发中咱们在xml中定义布局,Android studio有强大的预览功能,所见即所得的体验很棒。Litho提供了对于Stetho 对支持,能够利用chrome的开发者工具对界面进行调试:
其实相比xml,这种方式并不方便,在chrome只是辅助调试,最终仍是根据调试状况手动在代码中更新。
在写界面时,咱们要合理地对界面进行拆分,使用多个组件组合成为一个完整对界面。一个组件定义以下:
@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仍是更适用于对性能优化有强烈需求的应用。
Litho使用了相似React的设计思路,而React社区很是的活跃。若是Litho的将来发展的比较良好,能够支撑常规应用开发时,React社区的不少经验就能够借鉴过来,如Redux等工具的实现等。
对于Litho的使用仍是一个比较初级的体验,文中若有错误的地方,烦请指出,很是感谢。
推荐阅读: