weex新版Layout引擎以及渲染逻辑探究

简介: # weex新版Layout引擎以及渲染逻辑探究 ## 一、背景 原来weex sdk使用Facebook yoga进行基础css布局,但是由于开源协议问题选择基于Google的FlexboxLayout了自研,此处按下不表。 一言以蔽之,Layout引擎目的是通过递归的方式将节点的css属性约束析构,然后计算出节点正确的位置等基础属性。也就是说需要先明确一点,Layout引擎只

weex新版Layout引擎以及渲染逻辑探究

一、背景

原来weex sdk使用Facebook yoga进行基础css布局,但是由于开源协议问题选择基于Google的FlexboxLayout了自研,此处按下不表。

一言以蔽之,Layout引擎目的是通过递归的方式将节点的css属性约束析构,然后计算出节点正确的位置等基础属性。也就是说需要先明确一点,Layout引擎只负责计算外部传进来一棵节点树,仅此而已。

而想要研究整体的渲染机制,单是Layout引擎远远不够,其中最起码还包括:脏节点染色机制、更新机制、特殊节点处理、多属性共同决定同一属性的优先级等等。本文只是我近两周在解决上百个Layout引擎内核bug时管中窥豹有所得,部分细节处有感兴趣的同学可以一同讨论。

二、渲染机制

如果是要表述整套渲染逻辑,涉及的细节处就未免太多太琐碎了,因此,在这里我只选取部分重要的实现机制来讲讲这些机制是如何运作,又是如何配合将渲染工作处理完成的,同时也带出我自己的一些思考,便于后续研究和探讨。

1、基于节点的flex layout渲染引擎

渲染引擎包括布局的核心计算,主要任务是计算某个节点树。输入某个节点A,输出整棵以节点A作为根节点的节点树。

抽象来看,计算某节点的伪代码如下:

void calculateLayout(node){
    initBFCContext();                        //将node节点下子节点划分为BFC列表和非BFC列表
    measureNonBFCsIfDirty();                //如果node被染色需要重新计算,则计算子节点中所有非BFC的布局
    layout(left,top,right,bottom);        //设置自己的大小并布局非BFC子节点的位置
    for(childNode in BFCList){
        calculateLayout(childNode);        //递归计算node节点下划分为BFC的子节点
    }
}

简单来说,BFC是一个不受父亲flex属性影响的块结构 ,详细概念可见这篇文章

由此可见,节点的布局是通过计算NonBFC列表中子节点属性以及自己本身的属性约束决定的。而BFC因为不影响父亲的布局,也不受父亲flex属性影响,因此,递归可得BFC下子树的布局。

所以计算自身节点的布局主要就看measureNonBFCsIfDirty这个方法中怎么处理的。

但是因为css中属性本身就不是单一属性决定布局的,而是由自己的属性、父亲的属性、孩子的属性等共同决定的,因此measureNonBFCsIfDirty只是靠一次遍历递归计算很难得到正确的布局。因此,measure方法的结构思路如下:

void measure{
    //初步根据节点的大小约束属性递归计算各个子节点的大小,此时获得子节点因为自身属性得到的布局。
    recursiveMeasureDirtySubNodes(width,height);
    
    if(dirty){ 
        //如果在上一步由于子节点计算使得父布局发生变化被染色,则需要计算布局内子节点是否收到新布局的约束
        recursiveCheckChildConstrainSize();
    }
    
    //根据子节点计算当前节点维持的flexLine列表的主轴和侧轴长度最终长度,并根据flexLine列表判断子节点是否需要进行布局扩展。
    determineFlexLinesSizeAndExpandChild();
    
    //递归计算被染色的子节点。
    recursiveMeasureDirtySubNodes(width,height);
}

其中flexLine就是flexbox布局中的主轴概念,如下图所示:

父亲div1中有4个子节点,布局方式是横向布局,虚线部分是flexline,可以理解为容纳子节点主轴上的单排虚拟容器,此时,因为子节点横向布局时受到父布局的宽度限制,因此会自动产生新的flexline以容纳超出的子节点。

因此,measure方法主要包括了两次计算。第一次递归计算了子节点布局,并从叶子节点往上计算得到父节点的flexline列表。然后根据计算中父节点的统计的flexline列表得到父节点自己的布局并检查子节点是否需要扩展布局,需要则染色。然后第二次递归计算所有被染色的子节点得到最终布局。

至此,基本上整体递归的框架就出来了,所需的就是将flex中的属性布局规则按照优先级填充进框架中,完善recursiveMeasureDirtySubNodes的计算逻辑,在此就不多赘述,对flex属性感兴趣的可以参考w3c标准。

2、特殊节点处理

对于div而言,本身的布局就是由自身style、父亲的flex限制和孩子的大小综合得到的,但是很多特殊节点的计算方式有些许不同,这种特殊节点的计算方式并不会记入Layout引擎中,因此Layout引擎提供了外部传入measureFunc方法参与到节点计算的形式。下面挑几个大致讲一下:

text

text组件一般作为叶子节点,但是绝大多数时候,它是由attributes中value的值和style中的lines等属性决定布局的,因此,textComponent中需要将attributeString的计算方式的方法指针传入Layout引擎中,先根据传入的value得到文字布局,然后再根据自身style和父亲约束决定最终布局。

list

在iOS中,list是一个非常特殊的组件。因为list组件的底层实现是UITableView,对于tableView而言,系统提供了一整套复用机制和cell管理机制,因此上层不能简单的手动控制每个cell的布局,而是必须通过tableview的delegate委托事件进行控制。

因此,如果将list产生的子节点树根据component树逻辑加入到根节点下,由Layout引擎控制布局,则会与系统原生tableView布局机制冲突,失去复用甚至布局错乱。但是,list下面的cell或者cell内部元素同时也是使用css属性布局的,也应该满足Layout引擎的逻辑规则。

那么,两者需要怎么兼容呢?需求上是ListComponent既要利用Layout引擎计算整体子树的布局,但是又不需要从页面的根节点递归控制到cell的大小位置,因此,思路如下图:

原有逻辑是将WXComponentManager管理下的node节点(也就是weexInstance下根节点)作为输入填进Layout引擎,从而计算出整张图的布局,并由WXComponentManager管理的Component树递归布局好。

而新逻辑则是设置标识位,让listComponent原来持有的node节点变成root节点下的叶子节点,原来的cell节点不加入到root节点树下,布局不会受到WXComponentManager的直接控制。ListComponent会新建一个新节点作为根节点,传入Layout引擎中获取了list下整个node树的布局(普通的component只维护一个node,ListComponent会产生两个node)。然后ListComponent将计算完毕的list node树委托给系统的UITableView的回调事件进行处理,由系统维护cell的初始化、复用、销毁。

这样做有两个好处:

  • 使用原生的机制,tableView不会一次性将所有cell渲染出来,节省开销。
  • 使用原生tableView,不需要重复实现tableView本身的各种复杂的机制。

但是这种实现方式还有局限:

  • cell的使用还只是往上堆叠WXView,并没有真正发挥cell的复用性。
  • 因为分割了node树,因此component树和node树并不是完全对应的,逻辑性有割裂,可能导致后续更新Layout引擎布局逻辑时,组件需要因此不断打补丁。

但是,还是不得不说,现有的方案,在不需要重新实现一套tableView的情况下用最小的代价完成了整体的布局逻辑,还是比较巧妙的。

embed

WXEmbedComponent是一个异类。它是weex sdk提供的组件中唯一一个自身持有了一个WXSDKInstance的组件,这就意味着它本身即是dom树下的一个子节点,同时也是一个weex容器。它在设计之初就是为了实现weex页面的嵌套。因为在现有业务中大量使用到tab形式实现会场页面,这种页面最大的特点就是tab作为导航,tab下有完整的整套页面逻辑,因此最适合使用页面嵌套的形式处理。

因此传入一个页面的url,embed组件就能够通过内部的WXSDKInstance将子页面渲染出来并加载到自身节点上。

3、脏节点染色机制

随着界面的复杂度不断增加,整个节点树的复杂度会不断增加,因此刷新一次节点树的布局耗费的时间成本就会不断上升,此时,如果因为某个子节点或者叶子节点属性发生变化,触发刷新的时候重新遍历计算了整一棵节点树明显不合适,会产生很多兄弟节点的重复计算。因此,必须有标志标示需要更新的节点,这就是脏节点染色机制。

现有的染色规则下,当节点A产生以下行为时会将节点A染色并递归染色此分支上节点A的所有直系祖先节点:

  • 初始化节点style
  • 移除或者增加节点下的子节点
  • 更新了margin、padding、border、left、right、top、bottom、width、height等布局属性
  • 更新了position、flexDirection、flexWrap、alignItems、flex等布局方式

但是,上述四点只是通用的节点染色机制,对于特殊节点,例如Slider更新数据、text更新value等内容发生变化也会导致节点被染色。

4、更新机制

首先得先说明一下整个weex大致的运作,weex页面是由WXSDKInstance的实例控制整个生命周期的,而管理页面下整个component树以及node树的是WXSDKInstance实例中WXComponentManager对象。

WXComponentManager实例中持有了一个CADisplayLink对象,本质上就是持有了一个计时器,这个计时器与屏幕的绘制刷新保持同步,即正常流畅情况下1/60s的频次。在WXComponentManager对象实例被创建的时候它就会将CADisplayLink对象创建出来并加入到当前的RunLoop中,以每秒60次的频率检测当前component列表中是否有节点被染色。如果有,将根节点传入Layout引擎中,重新计算当前布局中被染脏的节点以及对应的影响,然后视图树重新布局作为事务传入UI事件队列中排队处理。关于weex事件队列,下一节会有讲述。如果UI事件队列中1s内没有任何事务传入,CADisplayLink将会暂停,直到下次触发ComponentTask才会被唤醒。

如下图:

这套机制保证了被染色的脏节点能够及时被处理,同时又不会因为加入了CADisplayLink导致当前RunLoop一直在空转中。

5、异步与时序

在weex中,计算和触发更新是一个频繁进行的操作,如果这个时候在主线程中进行操作,很有可能使得主线程卡顿。同时,如果此时主线程已经被占用,事件的更新就会被阻塞,因此需要异步处理JsContext传递的更新事件。

但是仅仅使用GCD异步处理容易造成时序混乱的问题,因此,需要底层自身维护一个子线程用与事件的异步线性化处理。

weex本身维护了一个component线程,计算相关都在此线程完成,包括递归生成component树和node树、计算节点树布局等。可以说,主体任务都是由component线程做处理的,当conponent线程处理完毕后得到整个布局树后会切到UI线程进行布局设置。

三、现有机制的问题以及一些思考

只向上对父亲以及祖先染色合适么

现在的染色逻辑是对上的,假设发生更新的是节点A,在染色自身后A会逐级向父亲节点染色,然后再由根节点逐级向下计算被染色节点布局更新。但是问题在于,此时的计算会涉及到发生更新的节点A,A的直系祖先节点,或者A的父亲更新时影响到A的兄弟节点的布局,但是,对于A的子孙节点就无法被更新到了。如果此时A里面嵌套了多层,某个子孙节点受到A的布局影响,A更新了,但是无法使得此节点得到更新,因为A的子节点判断没有被影响,A的更新逻辑就截止了。

这个问题的情况出现在我的几个bug排查记录中,有兴趣的可以看看,也可以找我讨论下。这个问题暂时没有找到好的解法,前端同学最好还是要注意不要写太多层级嵌套,并且约束是跨越好几层的这种情况,避免踩坑。

如何节约运算

现有的机制下,因为flex属性,所以必须使得产生两次节点树的运算,咋一看,这种计算好像是无法避免的,因为需要统计flex约束,就必须先统计各个子节点的布局,然后根据flex属性定义更新子节点布局约束,然后再重新计算节点布局。

但是换个角度思考,这种无法避免的原因是在于基础属性和flex属性不是在一个维度的东西,或者说,基础属性是定值,而flex属性更像是一种“约束”。约束依靠定值才能产生作用。

但是,能不能将这种“约束”换成某种规则的组合,然后在第一次计算的时候就使得约束也能够计算进去呢?甚至是不是在这种规则下,在组件更新时候依照这种规则的组合可以直接更新到对应组件,不需要计算没有被“约束住”的组件呢?这块暂时只有个想法,具体还没有实现。有兴趣的可以找我讨论下。

node树和component树分离带来的问题

虽然因为一些特殊节点定制化的原因(例见上文list组件),node树和component树出现了不一致的情况。这种情况现在由于定制化所以单独处理了,但是这种分离助长了组件的特异性,出现了子Component单独管理一棵节点树的情况,而Manager此时并不能获取到整棵节点树,给后来属性扩充埋下了兼容性的隐患。

我觉得此时是不是可以利用代理模式,将系统原生的代理传递给cell这种形式,用以维持node树的完整性呢。毕竟node树只是布局树,不应该因为视图的使用方式不同将node树截断或扭曲。想来可行性应该挺高的。

逻辑的碎片化与规则的可插拔性

现有w3c的css属性规则太复杂了,尤其是组合逻辑,造成Layout引擎的逻辑非常难以维护,并且牵一发动全身,应该用规则插件化的思想结构化这些规则,或者大部分规则,这样可读性和维护性应该都能提高,甚至单规则或者组合规则的单测都好做很多,可作为后期Layout架构优化的方向。

四、结尾

weex还在不断更新中,网络上类似的跨平台布局解决方案也在不断丰富中,这块探究其实还缺少个各个平台的对比,后续再开系列吧。

相关文章
|
移动开发 IDE weex
Weex 实时渲染插件
强大的[Weex](https://github.com/alibaba/weex)已开源, 截止现在 Star 的数量已经达到了3576, 简直赞到不行呀~~~~ 为了方便大家更加便捷的开发 Weex 页面, 小弟写了一款 Intellij 的插件, 目前亲测可用的几款IDE 有 `Intellij Idea`, `Android Studio`, `Web Storm`, 下面简单的为大
4126 0
|
移动开发 JavaScript weex
浅析weex之vdom渲染
# 前言 前段时间进行了weex页面尝试, 页面滚动加载渲染得非常流畅, 让H5页面拥有了native般的体验。 如此之利器,让人非常想探一究竟,因此最近进行了js-framwork源码学习(weex开源地址:),希望能进一步了解其dom渲染机制。 # 一. 文件结构 weex代码结构如下,重点关注其js-framework实现。 ``` ├── weex-de
11006 0
|
7月前
|
移动开发 前端开发 JavaScript
探究移动端混合开发技术:React Native、Weex、Flutter的比较与选择
移动端混合开发技术在移动应用开发领域日益流行,为开发者提供了更高效的跨平台开发方案。本文将比较三种主流混合开发技术:React Native、Weex和Flutter,从性能、生态系统和开发体验等方面进行评估,以帮助开发者在选择适合自己项目的技术时做出明智的决策。
417 2
|
7月前
|
移动开发 前端开发 weex
移动端混合开发技术:React Native、Weex、Flutter 之争
在移动应用开发领域,React Native、Weex 和 Flutter 是备受关注的混合开发技术。本文将对它们进行全面比较与评估,以帮助开发者做出明智的选择。我们将从开发生态、性能、跨平台能力和易用性等方面进行比较,为读者提供全面的参考和指导。
|
7月前
|
移动开发 Dart 前端开发
移动端混合开发技术:React Native、Weex、Flutter的比较与选择
移动应用的开发已经成为现代社会中的重要一环。本文将比较并评估三种主流的移动端混合开发技术:React Native、Weex和Flutter。通过对它们的特点、优势和劣势的分析,帮助开发者在选择适合自己项目的技术方案时做出明智的决策。
|
7月前
|
移动开发 开发框架 前端开发
移动端混合开发技术探析:React Native、Weex、Flutter的比较与选择
随着移动应用开发的高速发展,混合开发技术成为了一种备受关注的选择。本文将对移动端混合开发技术中的React Native、Weex和Flutter进行比较与探讨,分析它们在性能、开发体验、生态系统和跨平台支持等方面的差异,以及如何根据项目需求进行选择。
221 1
|
移动开发 JSON JavaScript
weex开发 - VS Code解除格式警告
weex开发 - VS Code解除格式警告
119 0
weex开发 - VS Code解除格式警告
|
移动开发 JavaScript weex
weex开发- 无法找到模块“weex-vue-render”的声明文件。引入vue报错,无法找到引入的vue模块
weex开发- 无法找到模块“weex-vue-render”的声明文件。引入vue报错,无法找到引入的vue模块
310 0
weex开发- 无法找到模块“weex-vue-render”的声明文件。引入vue报错,无法找到引入的vue模块
|
weex-ui 移动开发 JavaScript
weex开发-使用weex-ui绑定事件源注意事项
weex开发-使用weex-ui绑定事件源注意事项
150 0
|
移动开发 JavaScript weex
weex开发 - 方法的映射,在weex调用fetch方法,实际调用同名的原生方法,在回调中把数据传递回js
weex开发 - 方法的映射,在weex调用fetch方法,实际调用同名的原生方法,在回调中把数据传递回js
210 0