美文网首页FlutterAndroid开发Flutter圈子
从UI系统的设计角度认识flutter三棵树

从UI系统的设计角度认识flutter三棵树

作者: 低情商的大仙 | 来源:发表于2020-04-05 16:11 被阅读0次

    前言

    刚接触flutter开发的同学肯定对一个概念不陌生:三棵树,分别是widget树,element树和renderobject树。然后大家就开始搜三棵树的各种原理,创建流程了。但是,今天我想从一个不同的角度来谈谈这三棵树的认知,那就是设计思想角度。为什么是三颗树呢?一棵树不行吗,四颗不行吗?

    常见UI设计思路

    在现在的可视化操作系统中,基本上UI系统的设计思路是大同小异的,所谓的UI系统要解决的问题就是三个(这里我们将一块要显示的区域称为view):

    • view有多大?
    • view放在哪里?
    • view长什么样?

    继续抽象下,就对应三个概念:测量,布局,绘制。其中测量比较特殊,我们想下,一个要展示的区域大小由什么决定?其实有两点决定:上一级view的约束以及下一级view的大小。首先是上一级view的约束,比如我不希望我的子view大小超过我的大小,或者我希望子view只占据我大小的二分之一等等,之所以还收到子view大小的影响,我们考虑这样一个场景:一个view的大小随它的子view大小改变而改变,最常见的例子就是文本展示view,当文本太长换行时,我们自然希望文本view高度自动变高,从而能完美展示所有文本。基于这些考虑,我们发现view可以很容易的组织成下面的结构:


    p1.png

    可以看到,在这样的UI设计体系中,View2的是受到上一级view和下一级view共同影响的,这其实是实际需求决定的。

    树状UI体系的缺点

    上面我们从实际需求出发,探讨了现代UI设计为什么是这种树形结构,但事实上这种结构有它的弊端,那就是更新效率不高!我们设想下,如果上图中的view2需要更新,该如何做?那就是必须遍历整颗view树,将所有的测量,布局,绘制流程重新走一遍。因为view2的大小改变了,它会同时影响上级view和下级所有的view。

    这个时候,我们再设想一个很普通的场景,一次网络请求回来,我们要给界面设置数据:

    textView1.setText("text1");
    textView2.setText("text2");
    textView3.setText("text3");
    ……
    

    可能大家没有意识到,事实上,每一次setText都会触发整棵view树的遍历过程,主要是为了计算哪些区域需要重新绘制,便于在下一次垂直同步信号来临时重新绘制整棵view树(包含测量,布局,绘制三个过程)。这里三次调用setText,就意味着整棵树被遍历了三遍,现实开发中可能会有更多的setText,那被遍历的次数就更多了,这就是树状UI体系的缺点了。

    一棵树到两棵树

    仔细分析上面的重复遍历整颗view数的过程,如果要优化该怎么做呢?最简单的方式就是等所有的setText方法结束后,统一来一次遍历更新就好了。要实现这个需求就意味着我们的setText()方法将不再触发ui的刷新逻辑。
    其实仔细分析下,TextView中的text只是一个文本属性,这个属性改变的时候确实会有界面的改变,但是在连续多个TextView的text改变时,我们不希望立即刷新界面,只希望最后一次刷新就好了。这样的话,我们完全可以将TextView的职责做个拆分:


    p2.png

    这样,我们将同时负责text记录和界面刷新的TextView拆分成了两个,Widget负责记录text,RenderObject负责真正的展示UI和刷新。用这种思路,我们大可以尽情随意设置Widget中的text,代价非常低,最后只需要执行一次界面刷新就可以了。

    总结下这种思想,就是职责分离,负责界面展示的的就负责界面展示,负责记录控件配置的就记录控件配置,配置的改变并不一定需要改变界面的展示。按照这种思想,我们原有的树状结构一棵view树就演变成了下面这个样子:


    p3.png

    两棵树到三棵树

    有了两棵树的结构,我们确实解决了UI刷新时重复遍历的问题,那这样就够了嘛?我们把目光聚焦在Widget树上,以Widget2为例,它有可能会发生以下变化:

    • Widget2中某个内容变化
      这种情况我们需要直接更新RenderObject2就好(可能需要遍历整颗RenderObject树)
    • Widget2的子Widget3被移除
      这时我们需要对应的移除RenderObject3
    • Widget2的子Widget3被Widget5替换
      这个时候我们不需要移除RenderObject3然后补上一个RenderObject5,我们可以直接复用RenderObject3,这样代价最小。

    上面是随意列出的一些情况,我们发现,两棵树的情况下,Widget树的变化如何对应到RenderObject树上的变化是一个比较特殊的过程,它不能直接映射过去,而是可以做各种优化。那么,问题来了,这个特殊的映射过程谁来负责?

    我们首先考虑由Wiget来控制自己对应的RenderObject可不可以。我们假设Widget2要被从Widget树中移除,这个时候Widget2应该找到对应的RenderObject2,然后遍历所有RenderObject2的子RenderObject,分别移除它们。怎么样,发现问题没有?我们将一棵树拆成了两棵树,就是为了避免在设置一些控件的配置时引起整颗view树的遍历,但这里又开始进行遍历RenderObject树了,如果使用这种方式的话,拆成两棵树就没有意义了。

    为了解决这个尴尬的情况,我们希望能有一个新的角色出现,它负责监控整个Widget树的变化,然后通过一些列的计算,以最小的代价去改动RenderObject树,这样,我们依然可以随意的以极低的成本去修改控件的某些配置了。我们将这个角色称为Element,由于要监控整个Widget树的变化,那么最简单就是每个Widget都有自己的Element。因此,两棵树的结构就演变成了三棵树的结构:


    p4.png

    至此,我们一步步推导出了flutter三棵树的意义:

    • Widget树 控件的配置信息,不涉及渲染,更新代价极低。
    • RenderObject 树 真正的UI控件树,负责渲染UI,更新代价极大。
    • Element树 Widget树和RenderObject树之间的粘合剂,负责将Widget树的变更以最低的代价映射到RenderObject树上。

    结语

    事实上,三棵树的存在并不是这里描述的这么简单,这里我只是提供了一种自己理解的思路来诠释三棵树存在的意义,让大家对flutter的UI架构有一个大致的了解。比如我们知道了RenderObject树存在就是为了渲染界面,那么我们是不是可以抛弃Widget树和Element树,自己基于RenderObject树开创一套全新的UI编写方式呢?当然,限于本人水平有限,这里的一些理解可能不一定对,这里欢迎大家指出来,

    相关文章

      网友评论

        本文标题:从UI系统的设计角度认识flutter三棵树

        本文链接:https://www.haomeiwen.com/subject/sjiuphtx.html