原文地址在这里。算法
本文主要说了Flutter内部使用了怎样的算法和优化让Flutter如此强大。某些内容对比了Flutter和其余开发工具一致性算法的优劣,不过我的感受仍是太过简短,后面我会花更多的时间来研究这方面的内容,后续补上。最后还讲述了Flutter在API设计上是如何达到开发者的预期的。因为译者水平有限,疏漏之处还请见谅。
本文描述了Flutter的内部工做原理。Flutter的widget是用激进组合的方式工做的,因此用户在构建UI的时候会用到不少的widget。为了支持这个工做量,Flutter使用了亚线性算法来处理布局、构建组件以及树数据结构。还包括了其余的一些常量及的优化。综合考虑其余的一些细节,这样的设计也会让开发者更加的容易的使用回调来建立无限滚动列表中对用户可见的部分。数组
Flutter的特色之一就是激进组合(aggressive composability)。通常组件(widget)都是由其余的组件组成的,这样的组件都是由更加基本的组件组成的。好比Padding
就是一个组件,而不是一个组件的某个属性。总之,用户的UI是有不少,不少的组件组成的。安全
组件的最末尾的节点都是RenderObjectWidget
,这些组件都会被用来建立绘制到屏幕上的节点。一颗绘制树就是一个保存了用户界面几何信息(大小,位置等)的数据结构。这些几何信息是在layout阶段计算出来的,并在绘制(painting)和碰撞检测(hit test)被用到。基本上Flutter的开发人员不会直接去建立绘制对象(render object),而是经过组件(widget)来操做绘制树。数据结构
为了在组件层支持激进组合,Flutter对组件层和绘制树层使用了不少的算法和优化。这些都会在随后的章节中介绍。app
如何应对大量的组件和绘制对象(render object),如何获取好的性能?关键就是有效的算法!这其中最关键的就是layout算法,这个算法决定了绘制对象的几何信息,好比大小和位置。某些工具的布局算法的时间复杂度到了O(N²)甚至更差(好比,在默写约束下作固定点的迭代)。Flutter的目标是首次布局计算达到线性性能,在随后的更新已存在布局的时候达到亚线性性能,除了某些特殊状况以外。尤为是,相对于大量增长的绘制对象,布局消耗的时间只会缓慢增长。框架
Flutter每一帧只执行一次布局,每次布局的计算都是以单通道(single pass)的方式工做。约束会经过父对象调用每一个子对象的layout方法传递下去。子对象会递归地执行本身的layout方法并在返回的时候把几何信息向上返回。一旦一个绘制对象从它的layout方法返回了,那么这个绘制对象不会再被访问,一直到下一帧(frame)的布局计算。这样的方式把原本会分红两步:测量(measure)和布局的计算集合成了一个单通道的方式。这样,每一个render object再聚聚的时候最多被访问两次:一次是沿着树向下的访问,一次是沿着树向上的访问。ide
Flutter包含了一个协议(protocol)的多个具象(specialization)。最多见的具象就是RenderBox
,做用于一个二维笛卡尔坐标系。在盒布局中,它的约束是一个最大、最小宽度和一个最大、最小高度。在布局的时候,child
就是经过在边界中选择一个值做为它的几何信息。当child从布局中返回,父对象决定了child在父对象坐标系的位置。注意:子对象的布局和它的位置无关,由于直到子对象从layout返回才能直到它的位置。所以,父对象能够能够任意给子对象定位,而不须要再次计算布局。函数
总的来讲,在布局期间,惟一从父对象流向子对象的就是约束(constraints)。惟一从子对象流向父对象的数据就是几何信息。这些不变量能够大幅度减小布局计算所需的工做量。工具
这么多优化的结果是:当绘制对象树(render object tree)里包含了脏(dirt)节点的时候,只有这些脏节点和他们周围有限的子树须要在layout的被访问到。布局
和layout算法相似,Flutter的组件构建算法也是亚线性的。在构建以后,全部组件都被element树持有,这个树也保持了UI的逻辑结构。Element树很是必要,由于组件自己是不可修改的(immutable)。也就是说若是有多个wiget的话,他们是不会记得他们之中的父子节点关系的。Element树也持有StatefuleWidget
的state对象。
在用户数据或者其余操做以后,一个element能够变为脏(dirty),好比一个开发者对state对象调用了setState()
。那么Flutter会保留一个“脏”element的列表,并在构建阶段直接跳过去而忽略掉其余干净(clean)的element。在构建阶段,数据单向的由上向下从element树流动,也就是说在构建阶段,element只会被访问一次。一旦element变成干净的(clean)就不会变成脏的,由于它的祖先element都是干净的了。
因为组件是不可修改(immutable)的,若是父对象使用同一个组件从新构建,并且组件对应的element没有把本身标记为脏,那么这个element能够在构建阶段当即返回。并且,element只须要比较两个widget引用的ID来肯定两个widget是否为同一个。这个优化叫作二次投影模式,具体来讲就是一个组件包含了一个构建前的子组件,在构建的时候把它保存为了一个成员变量。
在构建的时候,Flutter也会避免使用InheritedWidget
来访问父链。若是全部组件都访问父链,好比获取当前的主题颜色,那么根据树的深度,构建将变成O(N²)。这样的耗时就回很是之多。为了不这样的状况发生,Flutter在每一个element上都有一个InheritedWidget
的哈希表。不少的element只会引用同一个哈希表,只有element引用了新的InheritedWidget
才会发生改变。
与广泛认为的不一样,Flutter不会进树级别的找不一样。并且使用了一个O(N)算法:独立检测每一个element的子element列表来决定是否要重用这个element。子列表一致性算法的优化分为如下几种状况:
一般的方法就是从头至尾的对比两个列表里的每一个组件的运行时类型(runtime type)和key,极有可能会在每一个列表里发现一段包含了全部不匹配的组件,以及他们的范围(range) 。Flutter会把旧的列表里的组件根据他们的key,放进一个哈希表里。接下来,Flutter遍历新的列表的范围(range),并从哈希表中查找匹配的key。不匹配的会被抛弃,匹配的则使用新的组件从新构建。
重用element对于性能来讲很是之重要,由于element持有两种很重要的数据:状态组件的状态和底层的绘制对象。当Flutter能够重用一个element的时候,UI某个逻辑部分的状态得以保留,而且以前计算出来的layout数据也能够重用,基本能够避免整个子树的遍历。事实上,Flutter支持保留了状态和布局的非本地(non-local)树修改。
开发者能够经过使一个widget和一个GlobalKey
关联的方式来执行非本地树修改。每个全局键(global key)在整个app里都是惟一的,而且注册在了一个线程相关的哈希表里。在构建阶段,开发者能够把一个有全局键的组件移动到element树的任意位置。而不是在那个位置上再建一个全新的组件。Flutter会检查哈希表,而后把组件挂在新的父组件下,并保留整个子树。
在子树里的绘制对象能够保留他们的布局信息,由于布局的约束是惟一从树的父对象流向子对象的数据。新的父对象会被标记为脏(dirty)由于它的子对象列表已经发生了改变。可是若是新的父对象传过来的layout数据和旧的parent传过来的是同样的,那么这个子对象会马上返回,中止遍历。
全局键和非本地树修改普遍用于英雄转化(hero transition)和导航(navigation)。
在这些算法优化以外,要达到及机组和还须要几个常量因素优化。这些优化对于上面提到的算法也相当重要。
RenderBox
类有一个抽象的visitChildren()
方法而不是实际的firstChild和nextSibling接口。许多子类都支持一个单一的child,直接作为一个类成员变量,而不是一列子节点。好比,RenderPadding
支持一个惟一的child。这样只会有一个耗时更短的,更简单的layout方法会被执行。RenderParagraph
。这是一个绘制树的叶子节点。文本的处理不须要继承的方式,而是用组合的方式。如此一来就能够避免RenderParagraph
从新计算它所持有的文本在父节点传递了一样的约束的条件下再次计算布局数据。着很常见,即便是在树分解中也同样。鉴于一般都是硕大的组件数来讲,这样的优化带来的性能提高很是显著。
绘制对象树(绘制树)和Element树是同构的(严格的说,绘制树是element树的一个子集)。一个明显的简化是把这两个数组合成一个。然而,在实践中把这两个数分开有不少的益处。
无限滚动列表的实现对于各类工具来讲都是很是之难。Flutter在构建的时候提供了一个很是简单的接口来实现这个功能。一个列表,当用户滚动的时候,使用了一个回调来实现可视的时候显示一个组件。支持这个功能须要用到viewport-aware布局和按需构建组件。
和Flutter多数的东西同样,可滚动组件也是用组合的方式组成的。在可滚动组件的外面是一个Viewport
,的子组件它能够扩展到可视窗口外面的部分,还能够滚动到视图内。然而,一个viewport有一个RenderSilver
类型的子组件,而不是RenderBox
类型的子组件。RenderSilver
类型有一个视图感知接口。
这个silver布局协议和盒式布局的协议结构上是一致的,也会给子节点传递约束并返回几何信息。然而,约束和几何信息在二者之间却不一样。在silver协议里,子节点收到的是viewport数据,包括剩余的可视空间。他们返回的几何数据让不少种和滚动相关的效果成为可能,包括可折叠的header和视差效果。
不一样的silver填充viewport剩余空间的方式是不一样的。好比,一个silver能够生成一列子组件,一个挨着一个排列,直到这个silver显示了所有的子组件或者用光了全部的空间。相似地,一个silver生成一个二维的grid,子组件只填满这个grid可视的部分。由于他们能够感知到剩余的空间还有多少。silver还能够生成有限的子组件,虽然他们也能够生成无限的子组件。
silver能够经过组合生成不一样的可滚动布局和效果。好比,一个单独的viewport能够有一个可折叠的heaer,下面跟着一个线性列表和一个grid。全部的三个silver都会根据silver协议来互动,从而生成在viewport里可视的子组件,无论他们是属于header,list仍是grid的。
若是Flutter有一个严格的先构建再布局再绘制的管道(pipeline),前述内容在构建无限滚动列表的时候就很是的低效了,由于viewport里剩余多少空间可使用的数据只有在layout的阶段才能够知道。不采用其余手法的前提下,布局阶段对于构建填充剩余空间的组件来讲太迟了。Flutter把管道中的构建和布局两个阶段互相交叉,解决了这个问题。在布局阶段的任什么时候刻,Flutter均可以按需构建新的组件,只要这些组件是当前执行布局的绘制对象的子组件。
交叉构建和布局能够实现,彻底是由于构建和布局算法中严格的数据传递控制。尤为是,在构建阶段,数据只能够向下传递。当一个绘制对象在计算布局的时候,布局遍历尚未访问到这个绘制对戏的子树,也就是说子树生成的写入还不能改写当前布局的计算结果。相似的,一旦布局从一个绘制对象返回了,在此次布局计算中这个绘制对象讲不会再被访问到,也就是说任何后一步布局计算生成的写入都不能影响当前绘制对象用于构建子树的数据。
另外,线性一致性和树分解对于滚动中高效的更新element都很是的有必要。当element滚动进或出可视区域的时候,对修改绘制树来讲也一样的重要。
只有在框架能够被正确使用的时候,快才有意义。为了达到API友好的效果,Flutter和开发者进行了普遍的体验研究。这些有时确定了以前的某些决定,有时会帮助肯定某些功能的优先级,有时又改变了API设计的方向。好比,Flutter的API都有丰富的文档。UX研究确定了这些文档的价值,可是也明确了示例代码和图标的做用。
这一节讨论Flutter的API的设计以备急用。
Flutter的Widget
, Element
和RenderObject
树节点的基类没有子模型(child model)。这也让某个节点能够成为它要适用的某个节点的子模型。
多数的组件对象都只有一个子组件,所以只暴露了一个child
参数。某些组件支持不少的子组件,因此暴露了一个叫作children
的列表参数。某些组件没有任何的子组件,因此也不会暴露任何的参数。相似的,RenderObjects
暴露特定的子模型API。RenderImage
是一个叶子节点,没有子对象的概念。RenderPadding
接受一个单一的child,因此它只有一个单独的引用指向一个child。RenderFlex
接受未知数量的child并使用一个链表管理他们。
在某些特殊的状况下,须要更复杂的子模型。RenderTable
绘制对象接收的是一个二维数组,这个类对应的getter和setter来控制行和列的数量,还有必定数量的方法能够替换某个x,y下标的child。
Chip
组件和InputDecoration
对象的属性也和相关控件相符合。一刀切的子模型会强制语义至于子模型分层的顶端,好比,定义第一个child为前缀值,第二个child为后缀值,那么这个特定的子模型(child model)能够被用于特定的命名属性。
这种灵活性让这些树的每一个节点按照它的角色来操做。不多会把一个cell插入到一个table里,由于全部其余的cell都会变形,移位。相似地,也不多会使用下标而不是引用从一个flex行删除某个元素。
RenderParagraph
对象是组极端的例子:它有一个彻底不一样的child,TextSpan
。在RenderParagraph
范围内,RenderObject
树会变成一个TextSpan
树。
整体上,让API的设计符合开发者预期,不仅是子模型上的努力。
某些简单的组件的存在就是为了让开发者在解决的某个问题的时候能够找他他们。给一个行或列添加空间,一旦你知道方法就回变得很是简单:使用Expanded
组件和一个零大小的SiezedBox
子组件,不过其实这还不是最好的方法。若是你搜索space的话你会找到Spacer
组件,这个组件内部就使用了Expanded
和SizedBox
。
相似的还有隐藏一个组件的子树也很容易,只要不要在构建的时候包含这个组件的子树。开发者但愿有一个组件能够达到这个效果,那么就有了Visibility
组件。
UI框架都会有不少的参数,通常来讲开发者不多会记得构造函数里的每一个参数的语义。Flutter使用响应模式,因此在构建的时候会用到不少的构造函数。有了Dart语言的命名参数,Flutter的API就能够保证每一个build方法都清晰,容易理解。
这个模式能够扩展到每一个用了不少参数的方法上。尤为是每一个bool类型的参数,因此方法里的每一个true
和false
都是自带文档属性的。
一个在Flutter里广泛使用的技术是定义一种错误条件不存在的API。这样避免了对于错误的过多关注。
好比,一个插值方法容许一端或者插值的两端都为null,Flutter没有把它定义为错误。而是:插值的两端都为null则返回null,若是有一端为null,那么就至关因而给某个特定类型的0值插入值。也就是说,开发者若是意外给插值方法传入了null值,那么不会发生错误,而是会输出一个合理的值。
在Flex
布局算法里有一个更加细微的例子。这个布局的概念是flex的绘制对象的空间会有它的一个或者多个子组件分割,因此flex布局的大小应该是占满可用空间。在最初的设计中,提供一个无线的空间会出错:这样隐式的代表flex布局是无限大小,一个没有用的布局。然而,API作了修改,这样当一个无限大小的空间赋值给flex绘制对象的时候,它会变成这些子组件须要的大小,减小了可能的错误状况。
不是全部的错误均可以经过设计避免的。对于那些在debug的时候依旧存在的问题,Flutter一般都会今早的捕获异常,而且及时报告。断言(assert)的使用很是广泛。构造函数的参数也都检查到了细节。生命周期也都有监控,一旦发生错误就会抛出异常。
在某些状况下,这些发挥到了极致:好比,当运行单元测试的时候,无论测试的是什么,每一个RenderBox
子类都会检查固有size方法是否知足固有size的契约。这能够帮助发现那些API中不容易暴露的问题。
抛出的异常也包含了竟可能丰富的信息。
基于可变树的API都要经历一种混乱:建立树的最初状态的操做集合和后续的更新的操做结合存在很大的不一样。Flutter的绘制层使用了这个模型,这是一种维护一个持久树的有效方法,也是使布局和绘制高效的关键所在。然而,直接操做绘制层会显得很是奇怪,更糟糕的是还可能引入bug。
Flutter的组件层使用了响应式的模式来组合组件,并以此来操做底层的绘制树。这一API把树的建立和修改多个步骤合成为一个树的描述(build)步骤,每当APP的状态(state)发生了改变,那么UI就会生出新的配置,这个配置是由开发者控制的。以后Flutter对树的修改作必要的计算来反映出新的修改。
Flutter鼓励开发者根据当前APP状态的,做出相应的配置。也就是说,App状态变了,那么对应的组件也发生了变化。这个时候须要一种机制能够保证这些变化是有动画效果来过渡的。
好比,在状态1的时候有S1,界面上包含了一个圆圈。可是在下一个状态S2,它变成了一个方框。没有任何动画机制的话,这个显得很突兀。一个隐式的动画会让圆圈通过几帧以后再变成方框,体验会更好。
Flutter的口号是:“everything is a widget”,也就是使用基本的组件来构建复杂的组件。积极组合的结果会致使开发者使用大量的组件,这就须要仔细地设计算法和数据结构,这样才能高效的处理组件。再加上另外的设计,这些数据结构也让开发者能够很容易的建立无限滚动列表组件。