Flutter Widget框架-渲染原理解析

Flutter Framework

视图树的创建与管理机制、布局、渲染核心框架

视图树

  • Widget => 为Element提供配置信息
  • Element => Flutter创建Element的可见树, 同时持有Widget和RenderObject
  • RenderObject => 渲染树中的一个对象

渲染机制

调用runApp(rootWidget),将rootWidget传给rootElement,做为rootElement的子节点,生成Element树,由Element树生成Render树

runApp(首次执行)

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}

runApp(rootWidget) => attachRootWidget(rootWidget) => attachToRenderTree() => element.mount() => _rebuild() => updateChild()

1. WidgetsFlutterBinding

WidgetsFlutterBinding混入了不少的其他的Binding

  • BindingBase 那些单一服务的混入类的基类
  • GestureBinding framework手势子系统的绑定,处理用户输入事件
  • ServicesBinding 接受平台的消息将他们转换成二进制消息,用于平台与flutter的通信
  • SchedulerBinding 调度系统,用于调用Transient callbacks(Window.onBeginFrame的回调)、Persistent callbacks(Window.onDrawFrame的回调)、Post-frame callbacks(在Frame结束时只会被调用一次,调用后会被系统移除,在Persistent callbacks后Window.onDrawFrame回调返回之前执行)
  • PaintingBinding 绘制库的绑定,主要处理图片缓存
  • SemanticsBinding 语义化层与Flutter engine的桥梁,主要是辅助功能的底层支持
  • RendererBinding 渲染树与Flutter engine的桥梁
  • WidgetsBinding Widget层与Flutter engine的桥梁

持有BuildOwner、PipelineOwner

  • BuildOwner

    BuildOwner是Widget framework的管理类, 该类跟踪哪些小部件需要重新构建,并处理应用于整个小部件树的其他任务,比如管理树的非活动元素列表

  • PipelineOwner

    管理真正需要绘制的View, 对RenderObjectTree中发生变化节点的进行flush操作, 最后交给底层引擎渲染

2. attachRootWidget

  • 1.attachRootWidget(app) 方法创建了Root[Widget](也就是 RenderObjectToWidgetAdapter)

    void attachRootWidget(Widget rootWidget) {
        _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
          container: renderView,
          debugShortDescription: '[root]',
          child: rootWidget
        ).attachToRenderTree(buildOwner, renderViewElement);
      }
    
  • 2.紧接着调用attachToRenderTree方法创建了 Root[Element]

    RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
        if (element == null) {
          owner.lockState(() {
            element = createElement();  //创建rootElement
            element.assignOwner(owner); //绑定BuildOwner
          });
          owner.buildScope(element, () { //子widget的初始化从这里开始
            element.mount(null, null);  // 初始化子Widget前,先执行rootElement的mount方法
          });
        } else {
          ...
        }
        return element;
      }
    
  • 3.Root[Element]尝试调用mount方法将自己挂载到父Element上,因为自己就是root了,所以没有父Element,挂空了

    owner.buildScope(element, () { //子widget的初始化从这里开始
        element.mount(null, null);  // 初始化子Widget前,先执行rootElement的mount方法
      });
    
  • 4.mount的过程中会调用Widget的createRenderObject,创建了 Root[RenderObject]

    void mount(Element parent, dynamic newSlot) {
        _parent = parent; //持有父Element的引用
        _slot = newSlot;
        _depth = _parent != null ? _parent.depth + 1 : 1;//当前节点的深度
        _active = true;
        if (parent != null) // Only assign ownership if the parent is non-null
          _owner = parent.owner; //每个Element的buildOwner,都来自父类的BuildOwner, 这样可以保证一个ElementTree,只由一个BuildOwner来维护
        ...
      }
      
    @override
      void mount(Element parent, dynamic newSlot) {
        super.mount(parent, newSlot);
        _renderObject = widget.createRenderObject(this);
        attachRenderObject(newSlot);
        _dirty = false;
      }
    
  • 5.我们将app作为参数传给了Root[Widget](也就是 RenderObjectToWidgetAdapter),app[Widget]也就成了为root[Widget]的child[Widget]

  • 6.调用owner.buildScope,开始执行子Tree的创建以及挂载(与更新流程一致, 见更新)

  • 7.调用createElement方法创建出Child[Element]

    Element inflateWidget(Widget newWidget, dynamic newSlot) {
        final Key key = newWidget.key;
    
        if (key is GlobalKey) {
          final Element newChild = _retakeInactiveElement(key, newWidget);
          if (newChild != null) {
    
            newChild._activateWithParent(this, newSlot);
            final Element updatedChild = updateChild(newChild, newWidget, newSlot);
            return updatedChild;
          }
        }
        final Element newChild = newWidget.createElement();
        newChild.mount(this, newSlot);
        return newChild;
      }
    
  • 8.调用Element的mount方法,将自己挂载到Root[Element]上,形成一棵树

  • 9.挂载的同时,调用widget.createRenderObject,创建Child[RenderObject]

  • 10.创建完成后,调用attachRenderObject,完成和Root[RenderObject]的链接

    @override
      void attachRenderObject(dynamic newSlot) {
        _slot = newSlot;
        
        _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
        _ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
        
        final ParentDataElement<RenderObjectWidget> parentDataElement = _findAncestorParentDataElement();
        if (parentDataElement != null)
          _updateParentData(parentDataElement.widget);
      }
    

    RenderObject与父RenderObject的挂载稍微复杂了点。每一个Widget都有一个对应的Element,但Element不一定会有对应的RenderObject。(因为有一些Element是不用来做页面显示的, 像StatelessWidget=>StatelessElement没有对应的RenderObject)所以你的父Element并不一有RenderObject,这个时候就需要向上查找。

    RenderObjectElement _findAncestorRenderObjectElement() {
        Element ancestor = _parent;
        while (ancestor != null && ancestor is! RenderObjectElement)
          ancestor = ancestor._parent;
        return ancestor;
      }
    

    find方法在向上遍历Element,直到找到RenderObjectElement,RenderObjectElement肯定是有对应的RenderObject了,这个时候在进行RenderObject子父间的挂载。

3. scheduleWarmUpFrame

安排一个帧尽快运行, 这在应用程序启动期间使用,以便第一个帧(可能非常昂贵)可以多运行几毫秒。(个人理解是为了实现第一次页面渲染可以调用到 => drawFrame)

setState(更新)

@protected
void setState(VoidCallback fn) {
    ...
    _element.markNeedsBuild();
}

1.Element标记自身为dirty,并通知buildOwner处理

void markNeedsBuild() {
    ...
    _dirty = true; // 标记自身为dirty
    owner.scheduleBuildFor(this); // 通知buildOwner处理
  }

2.buildOwner将element添加到集合_dirtyElements中,并通知ui.window安排新的一帧

buildOwner会将所有dirty的Element添加到_dirtyElements当中,等待下一帧绘制时集中处理。

还会调用ui.window.scheduleFrame();通知底层渲染引擎安排新的一帧处理。

void scheduleBuildFor(Element element) {
    ...
    _dirtyElements.add(element);
    element._inDirtyList = true;
    ...
  }

3.底层引擎最终回调到Dart层, 完成计算渲染回收

void drawFrame() {
    try {
      if (renderViewElement != null)
        buildOwner.buildScope(renderViewElement);
      super.drawFrame();
      buildOwner.finalizeTree();
    } finally {
    }
    ...
}

3.1 执行buildOwner的buildScope方法

void buildScope(Element context, [VoidCallback callback]) {
    ...
    try {
        ...
        //1.排序
      _dirtyElements.sort(Element._sort);
        ...
      int dirtyCount = _dirtyElements.length;
      int index = 0;
      while (index < dirtyCount) {
        try {
            //2.遍历rebuild
          _dirtyElements[index].rebuild();
        } catch (e, stack) {
        }
        index += 1;
      }
    } finally {
      for (Element element in _dirtyElements) {
        element._inDirtyList = false;
      }
      //3.清空
      _dirtyElements.clear();
        ...
    }
  }
3.1.1 按照Element的深度从小到大,对_dirtyElements进行排序
3.1.2 遍历执行_dirtyElements当中element的rebuild方法

遍历执行的过程中,也有可能会有新的element被加入到_dirtyElements集合中,此时会根据dirtyElements集合的长度判断是否有新的元素进来了,如果有,就重新排序。

void rebuild() {
   
    if (!_active || !_dirty)
      return;

    Element debugPreviousBuildTarget;

    performRebuild();
  }

element的rebuild方法最终会调用performRebuild(),而performRebuild()不同的Element有不同的实现

执行performRebuild()

performRebuild()不同的Element有不同的实现,我们暂时只看最常用的两个Element:

  • ComponentElement,是StatefulWidget和StatelessElement的父类

    void performRebuild() {
        Widget built;
        try {
          built = build();
        } 
        ...
        try {
          _child = updateChild(_child, built, slot);
        } 
        ...
      }
    

    build()执行我们复写的StatefulWidget的state的build方法, 拿到子Widget, 交给updateChild

    Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
        ...
            //1. 如果newWidget是null, 说明删除控件, Element被删除
        if (newWidget == null) {
          if (child != null)
            deactivateChild(child);
          return null;
        }
        
        if (child != null) {
            //2. 如果新旧控件相同, 说明Widget复用了, 判断位置是否相同, 不相同更新
          if (child.widget == newWidget) {
            if (child.slot != newSlot)
              updateSlotForChild(child, newSlot);
            return child;
          }
          //3. 判断key值和运行时类型(runtimeType)是否相等, 都相同才可以更新, 更新并返回Element(这个时候应该是Widget变了, 但是还是同类型的Widget)
          if (Widget.canUpdate(child.widget, newWidget)) {
            if (child.slot != newSlot)
              updateSlotForChild(child, newSlot);
            child.update(newWidget);// 
            return child;
          }
          deactivateChild(child);
        }
        //4. 如果上面的条件都不满足, 创建新的Element
        return inflateWidget(newWidget, newSlot);
      }
    
    • 1.如果newWidget是null, 说明删除控件, Element被删除

    • 2.如果新旧控件相同, 说明Widget复用了, 判断位置是否相同, 不相同更新

    • 3.判断key值和运行时类型(runtimeType)是否相等, 都相同才可以更新, 更新并返回Element(这个时候应该是Widget变了, 但是还是同类型的Widget)

      static bool canUpdate(Widget oldWidget, Widget newWidget) {
          return oldWidget.runtimeType == newWidget.runtimeType
              && oldWidget.key == newWidget.key;
        }
      

      child.update(newWidget);方法, 会根据newWidget的类型执行不同的update方法, 例如:

      • Column是MultiChildRenderObjectWidget类型的, 就会执行下面的方法:

        @override
          void update(MultiChildRenderObjectWidget newWidget) {
        
            super.update(newWidget);
            _children = updateChildren(_children, widget.children, forgottenChildren: _forgottenChildren);
            _forgottenChildren.clear();
          }
        

        由于Column里面的孩子是children类型(MultiChildRenderObjectWidget), 有多个, 所以对比算法采用updateChildren, 返回新的Element

      • Container是StatelessWidget类型的, 所以他执行StatelessWidget的update方法:

          @override
          void update(StatelessWidget newWidget) {
            super.update(newWidget);
            _dirty = true;
            rebuild();
          }
        
      • Scaffold是StatefulWidget类型的, 所以执行:

          @override
          void update(StatefulWidget newWidget) {
        
            super.update(newWidget);
        
            final StatefulWidget oldWidget = _state._widget;
         
            _dirty = true;
            _state._widget = widget;
            try {
              _debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
              final dynamic debugCheckForReturnedFuture = _state.didUpdateWidget(oldWidget) as dynamic;
            } finally {
              _debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
            }
            rebuild();
          }
        

      如果是StatelessWidget/StatefulWidget类型, 则继续执行下一级的对比, 以此类推.(child.update => rebuild => performRebuild(), rebuild就是上面那个方法)

      如果是MultiChildRenderObjectWidget类型, 则updateChildren里面会进行List对比算法, 每一个item也会调用updateChild()方法, 进行计算, 详细过程见最后
      * 4.如果上面的条件都不满足, 创建新的Element

      首先会尝试通过GlobalKey去查找可复用的Element,复用失败就调用Widget的方法创建新的Element,然后调用mount方法,将自己挂载到父Element上去,会在这个方法里创建新的RenderObject。

      Element inflateWidget(Widget newWidget, dynamic newSlot) {
          final Key key = newWidget.key;
          if (key is GlobalKey) {
            final Element newChild = _retakeInactiveElement(key, newWidget);
            if (newChild != null) {
              newChild._activateWithParent(this, newSlot);
              final Element updatedChild = updateChild(newChild, newWidget, newSlot);
              return updatedChild;
            }
          }
          final Element newChild = newWidget.createElement();
          newChild.mount(this, newSlot);
          return newChild;
        }
      
  • RenderObjectElement,是有渲染功能的Element的父类

    与ComponentElement的不同之处在于,没有去build,而是调用了updateRenderObject方法更新RenderObject。

      @override
      void performRebuild() {
        widget.updateRenderObject(this, renderObject);
        _dirty = false;
      }
    

    他代表的具有自己渲染功能的一类Widget(Text,没有child的)

3.1.3 遍历结束之后,清空dirtyElements集合

3.2 执行WidgetsBinding 的drawFrame (), PipelineOwner对RenderObject管理, 更新页面

@protected
  void drawFrame() {
    pipelineOwner.flushLayout();  //布局需要被布局的RenderObject
    pipelineOwner.flushCompositingBits(); // 判断layer是否变化
    pipelineOwner.flushPaint();  //绘制需要被绘制的RenderObject
    renderView.compositeFrame(); // this sends the bits to the GPU 将画好的layer传给engine绘制
    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. 一些语义场景需要
  }

3.3 执行了buildOwner.finalizeTree()清理

void finalizeTree() {
    Timeline.startSync('Finalize tree', arguments: timelineWhitelistArguments);
    try {
      lockState(() {
        _inactiveElements._unmountAll(); // this unregisters the GlobalKeys
      });
     ...
    } catch (e, stack) {
      _debugReportException('while finalizing the widget tree', e, stack);
    } finally {
      Timeline.finishSync();
    }
  }

所有没用的element都调用了deactivateChild方法进行回收

void deactivateChild(Element child) {
    child._parent = null;
    child.detachRenderObject();
    owner._inactiveElements.add(child); // this eventually calls child.deactivate()
  }

也就在这里将被废弃的element添加到了_inactiveElements当中。

另外在废弃element之后,调用inflateWidget创建新的element时,还调用了_retakeInactiveElement尝试通过GlobalKey复用element,此时的复用池也是在_inactiveElements当中。

如果你没有在一帧里通过GlobeKey完成Element的复用,_inactiveElements在最后将被清空,就没办法在复用了。

updateChildren详细过程

  @protected
  List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element> forgottenChildren }) {

    Element replaceWithNullIfForgotten(Element child) {
      return forgottenChildren != null && forgottenChildren.contains(child) ? null : child;
    }

    int newChildrenTop = 0;
    int oldChildrenTop = 0;
    int newChildrenBottom = newWidgets.length - 1;
    int oldChildrenBottom = oldChildren.length - 1;

    final List<Element> newChildren = oldChildren.length == newWidgets.length ?
        oldChildren : List<Element>(newWidgets.length);

    Element previousChild;

    // Update the top of the list.
    while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
      final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
      final Widget newWidget = newWidgets[newChildrenTop];
     
      if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
        break;
      final Element newChild = updateChild(oldChild, newWidget, previousChild);
      
      newChildren[newChildrenTop] = newChild;
      previousChild = newChild;
      newChildrenTop += 1;
      oldChildrenTop += 1;
    }

    // Scan the bottom of the list.
    while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
      final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);
      final Widget newWidget = newWidgets[newChildrenBottom];
      
      if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
        break;
      oldChildrenBottom -= 1;
      newChildrenBottom -= 1;
    }

    // Scan the old children in the middle of the list.
    final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
    Map<Key, Element> oldKeyedChildren;
    if (haveOldChildren) {
      oldKeyedChildren = <Key, Element>{};
      while (oldChildrenTop <= oldChildrenBottom) {
        final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
        
        if (oldChild != null) {
          if (oldChild.widget.key != null)
            oldKeyedChildren[oldChild.widget.key] = oldChild;
          else
            deactivateChild(oldChild);
        }
        oldChildrenTop += 1;
      }
    }

    // Update the middle of the list.
    while (newChildrenTop <= newChildrenBottom) {
      Element oldChild;
      final Widget newWidget = newWidgets[newChildrenTop];
      if (haveOldChildren) {
        final Key key = newWidget.key;
        if (key != null) {
          oldChild = oldKeyedChildren[key];
          if (oldChild != null) {
            if (Widget.canUpdate(oldChild.widget, newWidget)) {
              // we found a match!
              // remove it from oldKeyedChildren so we don't unsync it later
              oldKeyedChildren.remove(key);
            } else {
              // Not a match, let's pretend we didn't see it for now.
              oldChild = null;
            }
          }
        }
      }
      
      final Element newChild = updateChild(oldChild, newWidget, previousChild);
      
      newChildren[newChildrenTop] = newChild;
      previousChild = newChild;
      newChildrenTop += 1;
    }

    // We've scanned the whole list.
    newChildrenBottom = newWidgets.length - 1;
    oldChildrenBottom = oldChildren.length - 1;

    // Update the bottom of the list.
    while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
      final Element oldChild = oldChildren[oldChildrenTop];
      
      final Widget newWidget = newWidgets[newChildrenTop];
      
      final Element newChild = updateChild(oldChild, newWidget, previousChild);
      
      newChildren[newChildrenTop] = newChild;
      previousChild = newChild;
      newChildrenTop += 1;
      oldChildrenTop += 1;
    }

    // Clean up any of the remaining middle nodes from the old list.
    if (haveOldChildren && oldKeyedChildren.isNotEmpty) {
      for (Element oldChild in oldKeyedChildren.values) {
        if (forgottenChildren == null || !forgottenChildren.contains(oldChild))
          deactivateChild(oldChild);
      }
    }

    return newChildren;
  }
  1. 从前往后, 依次判断oldChild是否存在(如果是GlobalKey, 则当做不存在处理), 并且新旧节点是否相同, 皆是则执行updateChild对比里面的节点, 返回Element, 赋值给newChildren, 记录下一个没有对比的新旧节点序号; 如果不同, 则跳出循环.
  2. 从后往前, 依次判断oldChild是否存在, 并且新旧节点是否相同, 皆是则记录下一个没有对比的新旧节点序号; 如果不同, 则跳出循环.
  3. 判断是否还有未比较的oldChildren, 如果有, 则获取到所有含有key的节点, 存入oldKeyedChildren, 不包括GlobalKey.
  4. 循环未比较的newChildren, 是否存在key, 存在则对比key是否在oldKeyedChildren中存在, 存在则移除oldKeyedChildren中对于的key, 最后调用updateChild返回Element, 赋值给newChildren.
  5. 再次循环, 把后面具有相同key的数据赋值给newChildren.
  6. 删除多余的oldChildren.
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容