背靠大树好乘凉——Flutter 视图渲染机制探究之三棵树

Flutter 在维基百科中的介绍中第一句就是“Flutter is an open-source UI software development kit created by Google”,可见,它是一个以 UI 为主的开发环境,那 UI 就是它的重中之重。那么,做为一个 UI 开发套件,它到底是如何工做的呢?或者说,它到底是怎么把咱们的代码转化为一个个的界面,渲染在设备的方寸屏幕之上呢?node

三棵树——视图建立

Widget 和 Element

试想一下,若是公司的 UI 给了你一个界面,让你完成它,那么你会如何开始?是否是首先肯定这是一个静态的(stateless)仍是动态的(stateful)页面?若是是静态的,那么自定义一个 StatelessWidget 的子类,而后重写其 build() 方法,在 build() 方法中,咱们只需构造出咱们的页面(Widget 对象),而后将其返回方法外便可。若是是动态的,那么须要自定义 StatefulWidget 和 State 的子类,这里先按下不表,后面咱们再和它们有一个深刻的认识。express

那么可想而知,flutter 就是把咱们在 build() 方法中深耕细做的图形相关的代码拿去画出了形形色色的按钮、文字等这些真实可见的图形,那么它到底是如何在“神不知鬼不觉”的状况下完成这一切的呢?markdown

咱们以 StatelessWidget 的 build() 方法为源头出发,看看它创造出来的 widget 到底去了哪里,作了什么。app

/// StatelessElement 
@override
Widget build() => widget.build(this);
复制代码
/// ComponentElement(StatelessElement 的父类)
@override
void performRebuild() {
    // ...
    Widget built;
    try {
        // ...
        built = build();
        // ...
    } catch (e, stack) {
        _debugDoingBuild = false;
        built = ErrorWidget.builder(
            _debugReportException(
                ErrorDescription('building $this'),
                e,
                stack,
                informationCollector: () sync* {
                    yield DiagnosticsDebugCreator(DebugCreator(this));
                },
            ),
        );
    } finally {
        // ...
    }
    try {
        _child = updateChild(_child, built, slot);
        assert(_child != null);
    } catch (e, stack) {
        built = ErrorWidget.builder(
            _debugReportException(
                ErrorDescription('building $this'),
                e,
                stack,
                informationCollector: () sync* {
                    yield DiagnosticsDebugCreator(DebugCreator(this));
                },
            ),
        );
        _child = updateChild(null, built, slot);
    }

    // ...
}
复制代码

咱们建立的 widget 做为参数传递给 updateChild() 方法。框架

/// Element
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    if (newWidget == null) {
        if (child != null)
            deactivateChild(child);
        return null;
    }
    Element newChild;
    if (child != null) {
        bool hasSameSuperclass = true;
        // ...
        // Widget 和 Element 的类型(Stateless 或 Stateful 等)没有变化且 widget 没有变动
        if (hasSameSuperclass && child.widget == newWidget) {
            if (child.slot != newSlot)
                updateSlotForChild(child, newSlot);
            newChild = child;
        } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
            /// Widget 和 Element 的类型(Stateless 或 Stateful 等)没有变化
            /// 且新旧 widget 的运行时类型和 key 是相同的(canUpdate() 方法的内容)
            if (child.slot != newSlot)
                updateSlotForChild(child, newSlot);
            child.update(newWidget);
            assert(child.widget == newWidget);
            assert(() {
                child.owner._debugElementWasRebuilt(child);
                return true;
            }());
            newChild = child;
        } else {
            /// 对于刚建立的 widget,由于以前尚未挂载过,因此要走这里先进行挂载
            deactivateChild(child);
            assert(child._parent == null);
            newChild = inflateWidget(newWidget, newSlot);
        }
    } else {
        newChild = inflateWidget(newWidget, newSlot);
    }

    //...

    return newChild;
}
复制代码

上面的方法中我添加了一些注释,因此下面将要进行的就是将咱们建立的 widget 挂载到 flutter 已有的视图树上,看看具体操做了些什么:less

/// Element
@protected
Element inflateWidget(Widget newWidget, dynamic newSlot) {
    assert(newWidget != null);
    final Key key = newWidget.key;
    if (key is GlobalKey) {
        final Element newChild = _retakeInactiveElement(key, newWidget);
        if (newChild != null) {
            assert(newChild._parent == null);
            assert(() {
                _debugCheckForCycles(newChild);
                return true;
            }());
            newChild._activateWithParent(this, newSlot);
            final Element updatedChild = updateChild(newChild, newWidget, newSlot);
            assert(newChild == updatedChild);
            return updatedChild;
        }
    }
    /// newWidget 是咱们建立的 StatelessWidget 子类,经过参数传递过来,
    /// 这里经过 newWidget 建立了一个 Element类。
    final Element newChild = newWidget.createElement();
    assert(() {
        _debugCheckForCycles(newChild);
        return true;
    }());
    /// 这一步将新建立的 element 挂载到 Element 树上
    newChild.mount(this, newSlot);
    assert(newChild._debugLifecycleState == _ElementLifecycle.active);
    return newChild;
}
复制代码

mount() 方法是将建立的 element 挂载到 Element 树上,看方法:ide

/// Element
void mount(Element parent, dynamic newSlot) {
    // ...
    _parent = parent;
    _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;
    final Key key = widget.key;
    if (key is GlobalKey) {
        key._register(this);
    }
    _updateInheritance();
    // ...
}
复制代码

经过一种相似链式的赋值,将新的 element 连接到树的尾端。可是这里有一个问题,就是新建立的 element 将 _parent 变量指向了原 Element 树的末端节点,这仅仅是单向的链,父节点的 _child 变量并无指向新的 element,那么如何向下遍历呢?咱们将上面的方法倒着再走一遍,新建立的 element 被返回,那么它又被用来作什么呢?一路向上,看到 ComponentElement 的performRebuild() 方法,这里返回的 element 被赋值给父节点的 _child 变量,至此,双向连接完成。函数

还有一个变量须要在这里提早关注一下,就是这个“slot”,到目前为止,它好像一直在伴随着咱们走过各个方法,可是好像并无实际的做用。其实咱们后面主要研究的几乎全部方法都能看到它的身影,不过既然暂时没有发挥到它的做用,那咱们就先记住有这么个东西,并且赋值给了 Element 的_slot 成员,后面在它须要发挥做用的时候,咱们会继续介绍这个同行者。ui

一条完整的 Element 树有了,那这棵树是怎么渲染到屏幕上的呢?咱们暂时先无论,继续看看 StatefulWidget 的“种树”过程。this

StatefulWidget 和 StatelessWidget 有一点不同的地方在于,StatefulWidget 的 build() 方法是在 State 中的,StatefulWidget 经过 createState() 方法建立 State,咱们看看调用关系:

StatefulElement

能够看到,StatefulElement 持有 State,那明确了这一点以后,再看 State 的 build() 方法的调用关系:

/// StatefulElement
@override
Widget build() => _state.build(this);
复制代码

StatefulElement 和 StatelessElement 同样,同属于 ComponentElement 的子类。因此咱们上面看到的 ComponentElement 的 performRebuild() 方法调用 build() 方法的过程一样适用于它,后面的流程就和 StatelessWidget 同样了。

介绍到这里,咱们已经揭开了两棵树的神秘面纱——Widget 树和 Element 树,目前来看,它们是一一对应的。在 Element 类的成员变量中就有一个 Widget 实例 _widget,它会在 Element 建立时和调用 update() 时被赋值,这也侧面佐证了两者的一一对应关系。那么第三棵树到底是什么呢?

RenderObject

咱们在 Android studio 里打开 Widget 类的源码,而后打开 Hierarchy(能够双击 Shift 或者使用快捷键 Ctrl + Shift + A 搜索,也可使用你的快捷键打开),咱们看到 Widget 的直接子类有下面几个,咱们注意到,除了刚刚已经接触的 StatelessWidget 和 StatefulWidget,还有其余几个陌生的子类。

Widget Hierarchy

其中 PreferredSizeWidget 和 ProxyWidget 是对 Widget 某方面需求的简单封装,各拓展了一个属性,因此在必定程度上能够认为等同于 Widget,那还有一个 RenderObjectWidget,这个类是干什么的呢?咱们看看它的源码:

/// RenderObjectWidgets provide the configuration for [RenderObjectElement]s,
/// which wrap [RenderObject]s, which provide the actual rendering of the
/// application.
abstract class RenderObjectWidget extends Widget {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const RenderObjectWidget({ Key key }) : super(key: key);

  /// RenderObjectWidgets always inflate to a [RenderObjectElement] subclass.
  @override
  @factory
  RenderObjectElement createElement();

  /// Creates an instance of the [RenderObject] class that this
  /// [RenderObjectWidget] represents, using the configuration described by this
  /// [RenderObjectWidget].
  ///
  /// This method should not do anything with the children of the render object.
  /// That should instead be handled by the method that overrides
  /// [RenderObjectElement.mount] in the object rendered by this object's
  /// [createElement] method. See, for example,
  /// [SingleChildRenderObjectElement.mount].
  @protected
  @factory
  RenderObject createRenderObject(BuildContext context);

  /// Copies the configuration described by this [RenderObjectWidget] to the
  /// given [RenderObject], which will be of the same type as returned by this
  /// object's [createRenderObject].
  ///
  /// This method should not do anything to update the children of the render
  /// object. That should instead be handled by the method that overrides
  /// [RenderObjectElement.update] in the object rendered by this object's
  /// [createElement] method. See, for example,
  /// [SingleChildRenderObjectElement.update].
  @protected
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }

  /// A render object previously associated with this widget has been removed
  /// from the tree. The given [RenderObject] will be of the same type as
  /// returned by this object's [createRenderObject].
  @protected
  void didUnmountRenderObject(covariant RenderObject renderObject) { }
}
复制代码

能够看到,相比于 StatelessWidget 和 StatefulWidget,它多了三个方法,分别是 createRenderObject()updateRenderObject()didUnmountRenderObject(),咱们先研究建立的过程。

RenderObjectElement 的 mount() 方法调用了 createRenderObject() 方法(这里能够看到对于 RenderObjectWidget,一样有 RenderObjectElement 与之对应):

/// RenderObjectElement
@override
void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    // ...
    _renderObject = widget.createRenderObject(this);
    // ...
    attachRenderObject(newSlot);
    _dirty = false;
}
复制代码

建立的 RenderObject 对象赋值给 _renderObject 对象存储起来,紧接着执行 attachRenderObject() 方法:

/// RenderObjectElement
@override
void attachRenderObject(dynamic newSlot) {
    assert(_ancestorRenderObjectElement == null);
    _slot = newSlot;
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    _ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
    final ParentDataElement<ParentData> parentDataElement = _findAncestorParentDataElement();
    if (parentDataElement != null)
        _updateParentData(parentDataElement.widget);
}
复制代码

_findAncestorRenderObjectElement() 方法的做用是向上寻找第一个 RenderObjectElement 对象的实例:

/// RenderObjectElement
RenderObjectElement _findAncestorRenderObjectElement() {
    Element ancestor = _parent;
    while (ancestor != null && ancestor is! RenderObjectElement)
        ancestor = ancestor._parent;
    return ancestor as RenderObjectElement;
}
复制代码

这里咱们就能够知道一点,RenderObject 并不和 Element 树一一对应,也就不和 Widget 树对应了,咱们能够这样理解,咱们建立的 StatelessWidget 和 StatefulWidget 更大程度上是一个容器,它们并非实际的可见的视图元素,而只是对一组可见的视图元素的排列组合,规定了排列位置和大小等,因此它们并不须要实际渲染出来,而那些继承了 RenderObjectWidget 的 widget 才是须要渲染的,也才会在 RenderObject 树上有与之相对应的节点。

好了,言归正传,而后调用第一个 RenderObjectElement 祖先的 insertRenderObjectChild() 方法,该方法如今以重写的方式实现,那么咱们看两个比较经常使用的 RenderObjectElement 的子类——SingleChildRenderObjectElement 和 MultiChildRenderObjectElement 的实现。首先是 SingleChildRenderObjectElement:

/// SingleChildRenderObjectElement
@override
void insertRenderObjectChild(RenderObject child, dynamic slot) {
    final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject as RenderObjectWithChildMixin<RenderObject>;
    assert(slot == null);
    assert(renderObject.debugValidateChild(child));
    renderObject.child = child;
    assert(renderObject == this.renderObject);
}
复制代码

须要注意的是,这里的“this”是第一个 RenderObjectElement 类型的祖先,因此 this.renderObject 也是祖先(父)RenderObject,child 则是咱们建立的 RenderObject 实例。经过给 RenderObject 的 child 成员赋值实现前面和 Element 同样的连接。

而在 MultiChildRenderObjectElement 的方法中,则经过 ContainerRenderObjectMixin 这个 mixin 来进行插入操做:

/// MultiChildRenderObjectElement
@override
void insertRenderObjectChild(RenderObject child, IndexedSlot<Element> slot) {
    final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject =
        this.renderObject as ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>>;
    assert(renderObject.debugValidateChild(child));
    renderObject.insert(child, after: slot?.value?.renderObject);
    assert(renderObject == this.renderObject);
}
 
/// ContainerRenderObjectMixin 实际上就是 RenderObject
mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType extends ContainerParentDataMixin<ChildType>> on RenderObject {
	/// ... 
}
复制代码

这里的关键代码是 renderObject.insert(child, after: slot?.value?.renderObject),可是在理解 insert() 方法的实现以前,咱们要先搞清楚传递的第二个参数 after: slot?.value?.renderObject 究竟是神么。Slot 咱们在上面也提醒读者要对它保留一点印象,由于它真的跟着咱们走了一路。一路走来,它神秘又低调,但如今,它再也不低调,由于它终于要发挥它的做用了,随着咱们对它的研究,它也终将再也不神秘。咱们首先看 Element 类对它的注释:

/// Information set by parent to define where this child fits in its parent's
/// child list.
///
/// Subclasses of Element that only have one child should use null for
/// the slot for that child.
dynamic get slot => _slot;
dynamic _slot;
复制代码

哦,孩子的父节点用来判断孩子的位置的,之因此强调“孩子的”,是为了代表它是存储在孩子的对象空间中的,用来找到相对于父节点来讲孩子的合适位置。那这么听来,父节点应该有不止一个孩子喽,否则一个位置哪有什么合适和不合适之说呢。确实如此,对于单孩子的节点来讲,这个变量通常为 null,对于多孩子的节点,这个变量才变得有意义。

上溯到 MultiChildRenderObjectElement 的 mount() 方法:

@override
void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _children = List<Element>(widget.children.length);
    Element previousChild;
    for (int i = 0; i < _children.length; i += 1) {
        final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element>(i, previousChild));
        _children[i] = newChild;
        previousChild = newChild;
    }
}

@protected
Element inflateWidget(Widget newWidget, dynamic newSlot) {
    // ...
    newChild.mount(this, newSlot);
    // ...
    return newChild;
}

@immutable
class IndexedSlot<T> {
  /// Creates an [IndexedSlot] with the provided [index] and slot [value].
  const IndexedSlot(this.index, this.value);

  /// Information to define where the child occupying this slot fits in its
  /// parent's child list.
  final T value;

  /// The index of this slot in the parent's child list.
  final int index;
 
  /// ...
}
复制代码

Slot 就是 IndexedSlot<Element>(i, previousChild) 了,它的 value 指向上一个兄弟节点。那么 after 就是 child 的上一个兄弟RenderObject,明确了这一点,咱们就能够继续往下看renderObject.insert() 方法的实现了:

/// ContainerRenderObjectMixin
ChildType? _firstChild;
ChildType? _lastChild;
/// [child] 新建立的 RenderObject 类,
/// [after] 新建立的 RenderObject 须要插入其后的父节点
void _insertIntoChildList(ChildType child, { ChildType? after }) {
    final ParentDataType childParentData = child.parentData as ParentDataType;
    // ...
    _childCount += 1;
    assert(_childCount > 0);
    if (after == null) {
        // insert at the start (_firstChild)
        childParentData.nextSibling = _firstChild;
        if (_firstChild != null) {
            final ParentDataType _firstChildParentData = _firstChild!.parentData as ParentDataType;
            _firstChildParentData.previousSibling = child;
        }
        _firstChild = child;
        _lastChild ??= child;
    } else {
        // ...
        final ParentDataType afterParentData = after.parentData as ParentDataType;
        if (afterParentData.nextSibling == null) {
            // insert at the end (_lastChild); we'll end up with two or more children
            assert(after == _lastChild);
            childParentData.previousSibling = after;
            afterParentData.nextSibling = child;
            _lastChild = child;
        } else {
            // insert in the middle; we'll end up with three or more children
            // set up links from child to siblings
            childParentData.nextSibling = afterParentData.nextSibling;
            childParentData.previousSibling = after;
            // set up links from siblings to child
            final ParentDataType childPreviousSiblingParentData = childParentData.previousSibling!.parentData as ParentDataType;
            final ParentDataType childNextSiblingParentData = childParentData.nextSibling!.parentData as ParentDataType;
            childPreviousSiblingParentData.nextSibling = child;
            childNextSiblingParentData.previousSibling = child;
            assert(afterParentData.nextSibling == child);
        }
    }
}
复制代码

这里的代码逻辑就很清晰了,将多个孩子以及兄弟关系都清楚地连接起来,RenderObject 的树就是在这样的开枝散叶中成长起来的。

欣欣向荣——视图更新

咱们上面探究了建立三棵树的节点并插入到树中的过程,正是在这种机制的保证下,咱们的三棵树才得以茁壮成长,咱们也才得以凭借咱们本身的代码创造出咱们本身的参天大树。可是为了保证大树的欣欣向荣,咱们就要修剪枯枝,将空间和营养留给新芽。因此对于那些已通过时的或者状态已经再也不正确的“树枝”和“树叶”,咱们要如何更新它们呢?

一般咱们在 State 中使用setState() 方法来进行 StatefulWidget 的更新,那么咱们就从这个方法入手,研究在咱们手动调用了这个方法以后,Flutter 到底作了那些事。

/// State
@protected
void setState(VoidCallback fn) {
    assert(fn != null);
    // ...
    final dynamic result = fn() as dynamic;
    // ...
    _element.markNeedsBuild();
}
复制代码

setState() 方法中传入的回调函数会首先被执行,接着会将 StatefulWidget 对应的 StatefulElement 标记为须要从新构建。

/// Element
void markNeedsBuild() {
    // ...
    if (dirty)
        return;
    _dirty = true;
    owner.scheduleBuildFor(this);
}
复制代码

接着,将 _dirty 属性修改成 true,这个变量用来标识此 element 是否须要从新构建,而后调用 BuildOwner 的 scheduleBuildFor() 方法。这里先简要地介绍一下 BuildOwner 这个类,老规矩仍是看一下官方的注释:

Manager class for the widgets framework.

This class tracks which widgets need rebuilding, and handles other tasks that apply to widget trees as a whole, such as managing the inactive element list for the tree and triggering the "reassemble" command when necessary during hot reload when debugging.

The main build owner is typically owned by the [WidgetsBinding], and is driven from the operating system along with the rest of the build/layout/paint pipeline.

Additional build owners can be built to manage off-screen widget trees.

To assign a build owner to a tree, use the [RootRenderObjectElement.assignOwner] method on the root element of the widget tree.

可见,这个类是用来管理视图框架的一些诸如 rebuild、inactive 状态等功能的。它会在 WidgetBinding 中建立并赋予给根节点,而后根据视图树向下传递给每个节点,因此在整棵视图树中,它的实例只有一个。更多相关的知识点你们能够本身研究。

回到 scheduleBuildFor() 方法:

/// BuildOwner

/// Adds an element to the dirty elements list so that it will be rebuilt
/// when [WidgetsBinding.drawFrame] calls [buildScope].
void scheduleBuildFor(Element element) {
    // ...
    if (element._inDirtyList) {
        // ...
        _dirtyElementsNeedsResorting = true;
        return;
    }
    // 当尚未安排刷新脏数据时就给安排上
    // 若是当前处于两个刷新帧之间,这个方法最终会调用 [window.scheduleFrame]方法,为下一帧作准备
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
        _scheduledFlushDirtyElements = true;
        onBuildScheduled();
    }
    _dirtyElements.add(element);
    element._inDirtyList = true;
    // ...
}
复制代码

经过 _dirtyElements.add(element) 将全部须要更新的节点登记在册以后,在接收到下一个 Vsync 信号进行视图刷新的时候,就会对这些脏节点按照在树的深度排序进行依次刷新。这些都在 WidgetsBinding.drawFrame() 方法中完成,但咱们暂时不进入这个方法详细研究,咱们看它的调用方法:

void buildScope(Element context, [ VoidCallback callback ]) {
    // ...
    Timeline.startSync('Build', arguments: timelineArgumentsIndicatingLandmarkEvent);
    try {
      _scheduledFlushDirtyElements = true;
      // ...
      _dirtyElements.sort(Element._sort);
      _dirtyElementsNeedsResorting = false;
      int dirtyCount = _dirtyElements.length;
      int index = 0;
      while (index < dirtyCount) {
        // ...
        try {
          _dirtyElements[index].rebuild();
        } catch (e, stack) {
          // ...
        }
        index += 1;
        if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) {
          _dirtyElements.sort(Element._sort);
          _dirtyElementsNeedsResorting = false;
          dirtyCount = _dirtyElements.length;
          while (index > 0 && _dirtyElements[index - 1].dirty) {
            // It is possible for previously dirty but inactive widgets to move right in the list.
            // We therefore have to move the index left in the list to account for this.
            // We don't know how many could have moved. However, we do know that the only possible
            // change to the list is that nodes that were previously to the left of the index have
            // now moved to be to the right of the right-most cleaned node, and we do know that
            // all the clean nodes were to the left of the index. So we move the index left
            // until just after the right-most clean node.
            index -= 1;
          }
        }
      }
      // ...
    } finally {
      for (final Element element in _dirtyElements) {
        assert(element._inDirtyList);
        element._inDirtyList = false;
      }
      _dirtyElements.clear();
      _scheduledFlushDirtyElements = false;
      _dirtyElementsNeedsResorting = null;
      Timeline.finishSync();
      // ...
    }
    // ...
  }
复制代码

对前面登记在册的脏节点进行遍历,而后依次调用它们的 rebuild() 方法,并且在执行完 build() 方法后,为了防止视图树产生变化,在必要时须要对视图树进行从新排序。在最后,再清空全部的登记的脏节点。

rebuild() 方法中调用的是 performRebuild() 方法,这里就又回到了文章开始的地方,会在这里经过调用 Widget 的 build() 方法建立 widget,这也就是为何咱们在手动调用 setState() 方法后,build() 方法会被从新执行的缘由。不过须要注意的是,在 updateChild() 方法中,由于此时的 element 可能已经对应了 widget,因此不会再从新挂载 widget 树,而只是对 widget 节点作更新操做。

上面咱们也提到,只有 RenderObjectElement 才会有对应的 RenderObject 对象,RenderObjectElement 的 performRebuild() 方法会对对应的脏的 renderObject 进行更新,具体更新逻辑由 RenderObjectWidget 的子类实现:

@override
void performRebuild() {
    // ..
    widget.updateRenderObject(this, renderObject);
    // ..
    _dirty = false;
}
复制代码

这样,三棵树的更新就全都覆盖到了,正是在这样的机制下,三棵树才能剪掉坏枝叶,健康茁壮成长。

写在最后(废话可跳过)

今天这篇文章主要从经常使用方法入手研究了 Widget 的三棵树以及三棵树的更新机制,篇幅所限,还有不少的问题暂时都尚未深刻到,好比咱们上面遗留的问题:RenderObject 树已经造成了,那么它是如何渲染成画面的?更多的问题,咱们会再以一个篇幅继续一块儿探究。上面的全部文章内容我事先也并非很了解,也是边写边看源码摸索,并且自知功力有限,因此有不少逻辑不严谨和疏漏的地方,那若是有误的地方,欢迎雅正,帮助我这个敲代码的小学生进步,感谢。

相关文章
相关标签/搜索