Components for Android: 一个高效的声明式UI框架

英文原文:Components for Android: A declarative framework for efficient UIs 。

滚动界面是移动端最常见的模式了。如果你做过app,那么你很可能使用过RecyclerView来实现滚动列表。

在安卓上建立列表界面是相当简单的:只需创建item的布局,把它挂载到RecyclerView 的adapter上就完成了。虽然大多数app要比这稍微复杂点。

如果你要为列表添加无限滚动,你就需要考虑内存使用的问题了。如果你adapter的 view type比较多,你需要考虑如何高效的回收视图。如果列表的item很复杂,你还很可能需要优化布局以避免滚动时丢帧。

在Facebook,我们要处理RecyclerView在各种信息流中的极端使用情况。拿新闻信息流来说,它的item非常复杂,有各种各样的内容包括分享,富文本,视频,广告,图片等等。而且每天都有许多来自不同产品团队的安卓工程师在做新闻信息流的工作。

14858155_653574974809597_1903845520690905088_n.png

这就带来了一些有趣的技术挑战。如何才能在内容如此复杂,团队合作人数如此众多的情况下做到让RecyclerView具有流畅的滚动性能呢?如何才能在一个内容变化近乎无限的信息流中提供一个高效的内存回收模式呢?

在这种规模之下,依靠一个团队为自己的产品提出一次性的解决方案是不可持续的。我们需要一个封装了实现这些信息流复杂度的可扩展的公共架构,这样产品团队就能专注于为社区输出惊叹的功能。

我们从ReactComponentKit得到灵感创建了一个强大的框架。它可以让安卓开发者只需通过一个简单的声明式API就能实现复杂且高性能的RecyclerView。我们把它叫做Components for Android (C4A)。

C4A采用了单向数据流的响应式编程模型。你只需利用给定的不可变输入声明UI的不同状态然后框架就会为你做剩余的事情。

也许最好的描述方式是代码:

 @LayoutSpec
 public class HeaderComponentSpec {
      
   @OnCreateLayout
   static ComponentLayout onCreateLayout(
     ComponentContext c,
     @Prop Uri imageUri,
     @Prop(resType = STRING) String title,
     @Prop(resType = STRING, optional = true) String subtitle) {
      
     return Container.create(c)
         .direction(ROW)
         .alignItems(CENTER)
         .backgroundRes(R.drawable.header_bg)
         .paddingDip(ALL, 16)
         .child(
             FrescoImage.create(c)
                 .uri(imageUri)
                 .placeholderRes(R.drawable.avatar_placeholder)
                 .withLayout()
                 .widthRes(R.dimen.avatar_size)
                 .heightRes(R.dimen.avatar_size)
                 .marginDip(END, 16))
         .child(
             Container.create(c)
                 .direction(COLUMN)
                 .flexGrow(1)
                 .child(
                     Text.create(c)
                         .text(title)
                         .textColor(Color.DKGRAY)
                         .textSizeSp(16)
                         .textColorRes(R.color.user_name))
                 .child(
                     Text.create(c, R.style.SubtitleText)
                         .text(subtitle)
                         .withLayout()
                         .marginRes(TOP, R.dimen.subtitle_margin)))
         .build();
      }
    }

HeaderComponentSpec我们称之为component spec。它只是一个具有特殊注解的Java类。在编译时,注解处理器将生成一个HeaderComponent类以及一个builder,builder带有匹配了component spec中使用的变量的方法。你可以这样使用HeaderComponent:

HeaderComponent.create(c)
    .imageUri(Uri.parse("http://example.com/image"))
    .titleRes(R.string.title)
    .subtitle("And this is my subtitle.")
    .build();

C4A使用Flexbox,一个web上广泛存在的强大的布局系统,由我们开源跨平台的实现css-layout支持。HeaderComponent在屏幕上的效果如下:

14844398_309898026061233_1645817805536231424_n.jpg

HeaderComponentSpec用起来感觉非常简单,只需处理纯函数。你只要传入一些参数然后就返回一个布局树。这和安卓UI上状态化,命令式的代码形成鲜明的对比。

在我们探索这个framework的特性之前,让我们先快速浏览一遍它是如何在屏幕上显示控件的。

在C4A中,布局和渲染是分为两个独立的步骤实现的:布局(layout)和装载(mount)。

14859616_309394219447120_9062725472411975680_n.png

布局这一步和安卓视图系统是完全无关的,因为C4A有自己的布局系统(css-layout)。框架的使用者只需创建一个布局树( @OnCreateLayout方法),其余的事情都由框架来处理。布局的结果是一个控件列表以及它们各自的大小和位置。

然后布局的结果被用到装载这一步,以创建实际的view结构,一旦控件可见就立即渲染到屏幕上。

我们利用这个架构去实现一些激动人心的功能:异步布局,平铺视图结构,逐步装载以及精细的回收。

异步布局

Component是带不可变输入的纯函数因此它们在设计上是线程安全的。框架不受安卓视图系统约束,因此可以在后台线程执行布局,免受复杂的多线程的困扰。

比如, C4A可以提前计算好RecyclerView中item的布局,而不要阻塞UI线程。等到用户能看到item的时候,绝大多数繁重的工作都已经完成了。

平铺视图结构

 @OnCreateLayout布局返回的布局结构只是一个UI的轮廓,和安卓视图没有直接的关系。这允许框架在component被装载之前对布局树做性能优化。我们通过两种方法来做这件事情。

首先,C4A在完成布局的计算之后可以完全跳过容器,因为它们对于装载步骤无关紧要。在前面的例子中,当HeaderComponent被装载的时候,不会出现ViewGroup包裹标题和副标题的情况。

其次,component可以装载一个View或者一个drawable。实际上这个框架的绝大多数核心部件都是装载drawables的,而不是View,比如Text 和 Image。

经过这些优化之后,HeaderComponent就可以作为一个单独的、完全扁平的视图被渲染。你可以从下面启用了开发者选项的Show layout bound的截图中看出来。

14858167_389440938112528_5021182094647230464_n.png

虽然平铺视图对于内存使用和绘制时间都有很大益处,但是它们并不是银弹。当我们想学习安卓视图中的重要功能比如触摸事件处理,冲突限制时,C4A具有一个非常通用的系统可以自动让mounted components的视图结构“去扁平化”。比如,如果HeaderComponent的标题文字设置了点击事件处理,框架会自动把它包裹在一个View中。

逐步装载

因为RecyclerView中布局提前完成,我们就知道每个控件在显示之前的尺寸与边界。控件利用这点,一旦它们在屏幕上就可见逐步装载各自的内部内容。

14849298_1696838943968064_2453756679251034112_n.png

Incremental mount distributes the work of rendering components transparently across multiple frames making them less likely to cause jank while scrolling. This is especially relevant forRecyclerViews with complex items that would be very challenging to implement efficiently without a lot of custom optimizations.

精细的回收

RecyclerView提供了一种基于View type概念的回收机制,不同类型的item从不同的池子(pool)回收。绝大多数情况下这都能很好的工作,但是对于view类型非常多的列表就不理想了,因为RecyclerView需要不断的为每种类型inflating新的view,导致内存过度开销和滚动性能问题。

在C4A中,所有叶子控件如Text和Image在底层都是单独回收的。这可以让我们为RecyclerView带来更精细的回收方案。一旦一个组合控件的内部部分移出屏幕,该框架立即让它们可以被adapter中的其它item复用。

14850018_889415767855982_7034163546078314496_n.png

This means C4A dynamically cross-pollinates things like text and images across all composite items in a RecyclerView and doesn't require the use of multiple view types.

更好的性能

C4A让开发者可以在后台提前执行布局,渲染更平铺的视图结构,使用更精细的回收。所有这些都通过更简单的编程模式获得。

我们从使用了C4A的安卓app中看到了滚动性能的提升。现在Facebook Lite(我们为新兴市场做的app)的最新版本在Jelly Bean及以上的设备上就是完全基于Components。即便在低端手机上,滚动性能也获得了很大的提升。

同时,我们也正在把Facebook for Android应用转换成使用Components,并且把传统安卓视图转换成新框架之后也看到了类似的性能提升。