乐趣区

基本功 | Litho的使用及原理剖析

1. 什么是 Litho?
Litho 是 Facebook 推出的一套高效构建 Android UI 的声明式框架,主要目的是提升 RecyclerView 复杂列表的滑动性能和降低内存占用。下面是 Litho 官网的介绍:
Litho is a declarative framework for building efficient user interfaces (UI) on Android. It allows you to write highly-optimized Android views through a simple functional API based on Java annotations. It was primarily built to implement complex scrollable UIs based on RecyclerView.With Litho, you build your UI in terms of components instead of interacting directly with traditional Android views. A component is essentially a function that takes immutable inputs, called props, and returns a component hierarchy describing your user interface.Litho 是高效构建 Android UI 的声明式框架,通过注解 API 创建高优的 Android 视图,非常适用于基于 Recyclerview 的复杂滚动列表。Litho 使用一系列组件构建视图,代替了 Android 传统视图交互方式。组件本质上是一个函数,它接受名为 Props 的不可变输入,并返回描述用户界面的组件层次结构。

Litho 是一套完全不同于传统 Android 的 UI 框架,它继承了 Facebook 一向大胆创新的风格,突破性地在 Android 上实现了 React 风格的 UI 框架。架构图如下:

应用层:上层 Android 应用接入层。
规范层(API):允许用户使用声明式的 API(注解)来构建符合 Flexbox 规范的布局。
布局层:Litho 使用可挂载组件、布局组件和 Flexbox 组件来构建布局,其中可挂载组件和布局组件允许用户使用规范来定义,各个组件的具体用法下面的组件规范中会详细介绍。在 Litho 中每一个组件都是一个独立的功能模块。Litho 的组件和 React 的组件相类似,也具有属性和状态的概念,通过状态的变更来控制组件的展示样式。
布局测量:Litho 使用 Yoga 来完成组件布局的异步或同步(可根据场景定制)测量和计算,实现了布局的扁平化。
布局渲染:Litho 不仅支持使用 View 来渲染视图,还可以使用更轻量的 Drawable 来渲染视图。Litho 实现了大量使用 Drawable 来渲染的基础组件,可以进一步拍平布局。
除了上面提到的扁平化布局,Litho 还实现了布局的细粒度复用和异步计算布局的能力,对于这些功能的实现在 Litho 的特性及原理剖析中详细介绍。下面先介绍一下大家比较关心的 Litho 使用方法。
2. Litho 的使用
Litho 的使用方式相比于传统的 Android 来说有些另类,它抛弃了通过 XML 定义布局的方式,采用声明式的组件在 Java 中构建布局。
2.1 Litho 和原生 Android 在使用上的区别
Android 传统布局:首先在资源文件 res/layout 目录下定义布局文件 xx.xml,然后在 Activity 或 Fragment 中引用布局文件生成视图,示例如下:
<?xml version=”1.0″ encoding=”utf-8″?>
<TextView xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”Hello World”
android:textAlignment=”center”
android:textColor=”#666666″
android:textSize=”40dp” />
public class MainActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.helloworld);
}
}
Litho 布局:Litho 抛弃了 Android 原生的布局方式,通过组件方式构建布局生成视图,示例如下:
public class MainActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ComponentContext context = new ComponentContext(this);
final Text.Builder builder = Text.create(context);
final Component = builder.text(“Hello World”)
.textSizeDip(40)
.textColor(Color.parseColor(“#666666”))
.textAlignment(Layout.Alignment.ALIGN_CENTER)
.build();
LithoView view = LithoView.create(context, component);
setContentView(view);
}
}
2.2 Litho 自定义视图
Litho 中的视图单元叫做 Component,可以直观的翻译为“组件”,它的设计理念来自于 React 组件化的思想。每个组件持有描述一个视图单元所必须的属性和状态,用于视图布局的计算工作。视图最终的绘制工作是由组件指定的绘制单元(View 或者 Drawable)来完成的。
Litho 组件的创建方式也和原生 View 的创建方式有着很大的区别。Litho 使用注解定义了一系列的规范,我们需要使用 Litho 的注解来定义自己的组件生成规则,最终由 Litho 在编译期自动编译生成真正的组件。
2.2.1 组件规范
Litho 提供了两种类型的组件规范,分别是 Layout Spec 规范和 Mount Spec 规范。下面分别介绍两种规范的使用方式:
Layout Spec 规范:用于生成布局类型组件的规范,布局组件在逻辑上等同于 Android 中的 ViewGroup,用于组织其他组件构成一个布局。它要求我们必须使用 @LayoutSpec 注解来注明,并实现一个标注了 @OnCreateLayout 注解的方法。示例如下:
@LayoutSpec
class HelloComponentSpec {
@OnCreateLayout
static Component onCreateLayout(ComponentContext c, @Prop String name) {
return Column.create(c)
.child(Text.create(c)
.text(“Hello, ” + name)
.textSizeRes(R.dimen.my_text_size)
.textColor(Color.BLACK)
.paddingDip(ALL, 10)
.build())
.child(Image.create(c)
.drawableRes(R.drawable.welcome)
.scaleType(ImageView.ScaleType.CENTER_CROP)
.build())
.build();
}
}
最终 Litho 会在编译时生成一个名为 HelloComponent 的组件。
public final class HelloComponent extends Component {

@Prop(resType = ResType.NONE,optional = false) String name;

private HelloComponent() {
super();
}

@Override
protected Component onCreateLayout(ComponentContext c) {
return (Component) HelloComponentSpec.onCreateLayout((ComponentContext) c, (String) name);
}

public static Builder create(ComponentContext context, int defStyleAttr, int defStyleRes) {
Builder builder = sBuilderPool.acquire();
if (builder == null) {
builder = new Builder();
}
HelloComponent instance = new HelloComponent();
builder.init(context, defStyleAttr, defStyleRes, instance);
return builder;
}

public static class Builder extends Component.Builder<Builder> {
private static final String[] REQUIRED_PROPS_NAMES = new String[] {“name”};
private static final int REQUIRED_PROPS_COUNT = 1;
HelloComponent mHelloComponent;

public Builder name(String name) {
this.mHelloComponent.name = name;
mRequired.set(0);
return this;
}

@Override
public HelloComponent build() {
checkArgs(REQUIRED_PROPS_COUNT, mRequired, REQUIRED_PROPS_NAMES);
HelloComponent helloComponentRef = mHelloComponent;
release();
return helloComponentRef;
}
}
}
Mount Spec 规范:用来生成可挂载类型组件的规范,用来生成渲染具体 View 或者 Drawable 的组件。同样,它必须使用 @MountSpec 注解来标注,并至少实现一个标注了 @onCreateMountContent 的方法。Mount Spec 相比于 Layout Spec 更复杂一些,它拥有自己的生命周期:

@OnPrepare,准备阶段,进行一些初始化操作。
@OnMeasure,负责布局的计算。
@OnBoundsDefined,在布局计算完成后挂载视图前做一些操作。
@OnCreateMountContent,创建需要挂载的视图。
@OnMount,挂载视图,完成布局相关的设置。
@OnBind,绑定视图,完成数据和视图的绑定。
@OnUnBind,解绑视图,主要用于重置视图的数据相关的属性,防止出现复用问题。
@OnUnmount,卸载视图,主要用于重置视图的布局相关的属性,防止出现复用问题。

除了上述两种组件类型,Litho 中还有一种特殊的组件——Layout,它不能使用规范来生成。Layout 是 Litho 中的容器组件,类似于 Android 中的 ViewGroup,但是只能使用 Flexbox 的规范。它可以包含子组件节点,是 Litho 各组件连接的纽带。Layout 组件只是 Yoga 在 Litho 中的代理,组件的所有布局相关的属性都会直接设置给 Yoga,并由 Yoga 完成布局的计算。Litho 实现了两个 Layout 组件 Row 和 Column,分别对应 Flexbox 中的行和列。
2.2.2 Litho 的属性
在 Litho 中属性分为两种,不可变属性称为 Props,可变属性称为 State,下面分别介绍一下两种属性:
Props 属性:组件中使用 @Prop 注解标注的参数集合,具有单向性和不可变性。下面通过一个简单的例子了解一下如何在组件中定义和使用 Props 属性:
@MountSpec
class MyComponentSpec {

@OnPrepare
static void onPrepare(
ComponentContext c,
@Prop(optional = true) String prop1) {

}

@OnMount
static void onMount(
ComponentContext c,
SomeDrawable convertDrawable,
@Prop(optional = true) String prop1,
@Prop int prop2) {
if (prop1 != null) {

}
}
}
在上面的代码中,共使用了三次 Prop 注解,分别标注 prop1 和 prop2 两个变量,即定义了 prop1 和 prop2 两个属性。Litho 会在自动编译生成的 MyComponent 类的 Builder 类中生成这两个属性的同名方法。按照如下代码,便可以去使用上面定义的属性:
MyComponent.create(c)
.prop1(“My prop 1”)
.prop2(256)
.build();
State 属性:意为“状态”属性,State 属性虽然可变,但是其变化由组件内部控制,例如:输入框、Checkbox 等都是由组件内部去感知用户行为,并更新组件的 State 属性。所以一个组件一旦创建,我们便无法通过任何外部设置去更改它的属性。组件的 State 属性虽然不允许像 Props 属性那样去显式设置,但是我们可以定义一个单独的 Props 属性来当做某个 State 属性的初始值。
3. Litho 的特性及原理剖析
Litho 官网首页通过 4 个段落重点介绍了 Litho 的 4 个特性。
3.1 声明式组件
Litho 采用声明式的 API 来定义 UI 组件,组件通过一组不可变的属性来描述 UI。这种组件化的思想灵感来源于 React,关于声明式组件的用法上面已经详细介绍过了。
传统 Android 布局因为 UI 与逻辑分离,所以开发工具都有强大的预览功能,方便开发者调整布局。而 Litho 采用 React 组件化的思想,通过组件连接了逻辑与布局 UI,虽然 Litho 也提供了对 Stetho 的支持,借助于 Chrome 开发者工具对界面进行调试,不过使用起来并没有那么方便。
3.2 异步布局
Android 系统在绘制时为了防止页面错乱,页面所有 View 的测量 (Measure)、布局(Layout) 以及绘制 (Draw) 都是在 UI 线程中完成的。当页面 UI 非常复杂、视图层级较深时,难免 Measure 和 Layout 的时间会过长,从而导致页面渲染时候丢帧出现卡顿情况。Litho 为解决该问题,提出了异步布局的思想,利用 CPU 的闲置时间提前在异步线程中完成 Measure 和 Layout 的过程,仅在 UI 线程中完成绘制工作。当然,Litho 只是提供了异步布局的能力,它主要使用在 RecyclerView 等可以提前知道下一个视图长什么样子的场景。
3.2.1 异步布局原理剖析
针对 RecyclerView 等滑动列表,由于可以提前知道接下来要展示的一个甚至多个条目的视图样式,所以只要提前创建好下一个或多个条目的视图,就可以提前完成视图的布局工作。
那么 Android 原生为什么不支持异步布局呢?主要有以下两个原因:

View 的属性是可变的,只要属性发生变化就可能导致布局变化,因此需要重新计算布局,那么提前计算布局的意义就不大了。而 Litho 组件的属性是不可变的,所以对于一个组件来说,它的布局计算结果是唯一且不变的。
提前异步布局就意味着要提前创建好接下来要用到的一个或者多个条目的视图,而 Android 原生的 View 作为视图单元,不仅包含一个视图的所有属性,而且还负责视图的绘制工作。如果要在绘制前提前去计算布局,就需要预先去持有大量未展示的 View 实例,大大增加内存占用。反观 Litho 的组件则没有这个问题,Litho 的组件只是视图属性的一个集合,仅负责计算布局,绘制工作由指定的绘制单元来完成,相比与传统的 View 显然 Litho 的组件要轻量的多。所以在 Litho 中,提前创建好接下来要用到的多个条目的组件,并不会带来性能问题,甚至还可以直接把组件当成滑动列表的数据源。如下图所示:

3.3 扁平化的视图
使用 Litho 布局,我们可以得到一个极致扁平的视图效果。它可以减少渲染时的递归调用,加快渲染速度。
下面是同一个视图在 Android 和 Litho 实现下的视图层级效果对比。可以看到,同样的样式,使用 Litho 实现的布局要比使用 Android 原生实现的布局更加扁平。

3.3.1 扁平化视图原理剖析
Litho 使用 Flexbox 来创建布局,最终生成带有层级结构的组件树。然后 Litho 对布局层级进行了两次优化。

使用了 Yoga 来进行布局计算,Yoga 会将 Flexbox 的相对布局转成绝对布局。经过 Yoga 处理后的布局没有了原来的布局层级,变成了只有一层。虽然不能解决过度绘制的问题,但是可以有效地减少渲染时的递归调用。
前面介绍过 Litho 的视图渲染由绘制单元来完成,绘制单元可以是 View 或者更加轻量的 Drawable,Litho 自己实现了一系列挂载 Drawable 的基本视图组件。通过使用 Drawable 可以减少内存占用,同时相比于 View,Android 无法检查出 Drawable 的视图层级,这样可以使视图效果看起来更加扁平。

原理如下图所示,Litho 会先把组件树拍平成没有层级的列表,然后使用 Drawable 来绘制对应的视图单元。

Litho 使用 Drawable 代替 View 能带来多少好处呢?Drawable 和 View 的区别在于前者不能和用户交互,只能展示,因此 Drawable 不会像 View 那样持有很多变量和引用,所以 Drawable 比 View 从内存上看要轻量很多。举个例子:50 个同样展示“Hello world”的 TextView 和 TextDrawable 在内存占比上,前者几乎是后者的 8 倍。对比图如下,Shallow Size 表示对象自身占用的内存大小。

3.3.2 绘制单元的降级策略
由于 Drawable 不具有交互能力,所以对于使用 Drawable 无法实现的交互场景,Litho 会自动降级成 View。主要有以下几种场景:

有监听点击事件。
限制子视图绘出父布局。
有监听焦点变化。
有设置 Tag。
有监听触摸事件。
有光影效果。

对于以上场景的使用请仔细考虑,过多的使用会导致 Litho 的层级优化效果变差。
3.3.3 对比 Android 的约束布局
为了解决布局嵌套问题,Android 推出了约束布局(ConstraintLayout),使用约束布局也可以达到扁平化视图的目的,那么使用 Litho 的好处是什么呢?
Litho 可以更好地实现复杂布局。约束布局虽然可以实现扁平效果,但是它使用了大量的约束来固定视图的位置。随着布局复杂程度的增加,约束条件变得越来越多,可读性也变得越来越差。而 Litho 则是对 Flexbox 布局进行的扁平化处理,所以实际使用的还是 Flexbox 布局,对于复杂的布局 Flexbox 布局可读性更高。
3.4 细粒度的复用
Litho 中的所有组件都可以被回收,并在任何位置进行复用。这种细粒度的复用方式可以极大地提高内存使用率,尤其适用于复杂滑动列表,内存优化非常明显。
3.4.1 原生 RecyclerView 复用原理剖析
原生的 RecyclerView 视图按模板类型进行存储并复用,也就是说模板类型越多,所需存储的模板种类也就越多,导致内存占用越来越大。原理如下图。滑出屏幕的 itemType1 和 itemType2 都会在 Recycler 缓存池保存,等待后面滑进屏幕的条目的复用。

3.4.2 细粒度复用优化内存原理剖析
在 Litho 中,item 在回收前,会把 LithoView 中挂载的各个绘制单元拆分出来(解绑),由 Litho 自己的缓存池去分类回收,在展示前由 LithoView 按照组件树的样式组装(挂载)各个绘制单元,这样就达到了细粒度复用的目的。原理如下图。滑出屏幕的 itemType1 会被拆分成一个个的视图单元。LithoView 容器由 Recycler 缓存池回收,其他视图单元由 Litho 的缓存池分类回收。

使用细粒度复用的 RecyclerView 的缓存池不再需要区分模板类型来缓存大量的视图模板,只需要缓存 LithoView 容器。细粒度回收的视图单元数量要远远小于原来缓存在各个视图模板中的视图单元数量。
4. 实践
美团对 Litho 进行了二次开发,在美团的 MTFlexbox 动态化实现方案(简称动态布局)中把 Litho 作为底层 UI 渲染引擎来使用。通过动态布局的预览工具,为 Litho 提供实时预览能力,同时可以有效发挥 Litho 的性能优化效果。
目前 Litho+ 动态布局的实现方案已经应用在了美团 App 中,给美团 App 带来了不错的性能提升。后续博主会详细介绍 Litho+ 动态布局在美团性能优化的实践方案。

4.1 内存数据
由于 Litho 中使用了大量 Drawable 替换 View,并且实现了视图单元的细粒度复用,因此复杂列表滑动时内存优化比较明显。美团首页内存占用随滑动页数变化走势图如下。随着一页一页地滑动,内存优化了 30M 以上。(数据采集自 Vivo x20 手机内存占用情况)

4.2 FPS 数据
FPS 的提升主要得益于 Litho 的异步布局能力,提前计算布局可以减少滑动时的帧率波动,所以滑动过程较平稳,不会有高低起伏的卡顿感。(数据采集自魅蓝 2 手机一段时间内连续 fps 的波动情况)

5. 总结
Litho 相对于传统 Android 是颠覆式的,它采用了 React 的思路,使用声明式的 API 来编写 UI。相比于传统 Android,确实在性能优化上有很大的进步,但是如果完全使用 Litho 开发一款应用,需要自己实现很多组件,而 Litho 的组件需要在编译时生成,实时预览方面也有所欠缺。相对于直接使用 Litho 的高成本,把 Litho 封装成 Flexbox 布局的底层渲染引擎是个不错的选择。
6. 参考资料

Litho 官网
说一说 Facebook 开源的 Litho
React 官网
Yoga 官网

7. 作者简介

何少宽,美团 Android 开发工程师,2015 年加入美团,负责美团平台终端业务研发工作。
张颖,美团 Android 开发工程师,2017 年加入美团,负责美团平台终端业务研发工作。

退出移动版