今天看啥  ›  专栏  ›  龙湫

Flutter UI渲染分析

龙湫  · 掘金  ·  · 2020-03-04 09:25
阅读 88

Flutter UI渲染分析

1、前言

本篇文章主要介绍Flutter 渲染框架及其渲染过程

Flutter是谷歌的移动UI框架,在此之前也有类似ReactNative、Weex等跨端方案,Flutter在一定程度上借鉴了ReactNative的思想,采用三棵树 其中element tree diff管理,来触发renderTree的刷新,并且不同于android这种命令式视图开发,采用了声明式,下面将一一介绍。

2、编程范式的改变

在Android视图开发中是命令式的,view大多数都是在xml声明,开发者然后通过id找出view,数据更新时,仍需要开发者关注需要变化的view,再调用方法比如 setText之类的使其发生改变;
但是在Flutter中视图的开发是声明式的,开发者需要维护好一套数据集合以及绑定好widgetTree,这样后面数据变化时候widget会根据数据来渲染,开发者就不再关注每个组件,关心核心数据即可。

3、Flutter 渲染框架介绍

Flutter的渲染框架分为Framework和Engine两层,应用是基于Framework层开发,其中

  • Framework层负责渲染中的Build、Layout、Paint、生成Layer等环节,使用Dart语言
  • Engine层是C++实现的渲染引擎,负责把Framework生成的Layer组合,生成纹理,然后通过OpenGL接口向GPU提交渲染数据

该跨平台应用框架没有使用webview或者平台自带的组件,使用自身的高性能渲染引擎Skia 自绘,组件之间可以任意组合

image.png

4、视图树

flutter中通过各种各样的widget组合使用,视图树中包含了以下三种树 Widget、Element、RenderObject,对应关系如下

image.png

  • Widget:存放渲染内容、视图布局信息,widget的属性最好都是immutable
  • Element:存放上下文,通过Element遍历视图树,Element同时持有Widget和RenderObject(BuilderOwner)
  • RenderObject:根据Widget的布局属性进行layout,paint Widget传人的内容(PipeLineOwner)

通常 我们创建widget树,然后调用runApp(rootWidget),将rootWidget传给rootElement,作为rootElement的子节点,生成Element树,由Element树生成Render树

image.png

widget是immutable,数据变化会重绘,如何避免资源消耗

Flutter界面开发是一种响应式的编程,当数据发生变化时通知到可变更的节点(statefullWidget或者rootwidget),但是每次数据变更,都会触发widgetTree的重绘,由于widget只是持有一些渲染的配置信息而已,不是真正触发渲染的对象,非常轻量级,flutter团队对widget的创建、销毁做了优化,不用担心整个widget树重新创建带来的性能问题。RenderObject才是真正渲染时使用,涉及到layout、paint等复杂操作,是一个真正渲染的view,二者被Element Tree持有,ElementTree通过Diff 算法来将不断变化的widget转变为相对稳定的RenderObject。
当我们不断改变widget时,BuilderOwner收到widgetTree会与之前的widgetTree作对比,在ElementTree上只更新变化的部分,当Elment变化之后 与之对应的RenderObject也就更新了,如下图所示

image.png
可以看到WidgetTree全部被替换了,但是ElmentTree和RenderObjectTree只替换了变化的部分
image.png

其中 PipelineOwner类似于Android中的ViewRootImpl,管理着真正需要绘制的View,
最后PipelineOwner会对RenderObjectTree中发生变化节点的进行layout、paint、合成等等操作,最后交给底层引擎渲染。

Widget、Element、RenderObject之间的关系

在介绍Elment Tree的Diff规则之前,先介绍下,这三者之前的关系,之前也大致提到 Elment Tree持有了Element同时持有Widget和RenderObject(BuilderOwner),我们先从代码入手

image.png

可以看出 Widget抽象类有3个关键能力

  • 保证自身唯一性的key
  • 创建Element的create
  • canUpdate

从上面类图也可以看出,**Element和RenderObject都是由Widget创建出来,**也并不是每一个Widget都有与之对应的RenderObject

Widget、Element、RenderObject 的第一次创建与关联


在Android中ViewTree

-PhoneWindow
	- DecorView
		- TitleView
		- ContentView

复制代码

而在Flutter中则比较简单,只有底层的root widget

- RenderObjectToWidgetAdapter<RenderBox>
	- MyApp (自定义)
	- MyMaterialApp (自定义)
复制代码

其中RenderObjectToWidgetAdapter 也是一个renderObjectWidget,通过注释可以发现它是runApp启动时“A bridge from a [RenderObject] to an [Element] tree.”
runApp代码

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}
复制代码

WidgetsFlutterBinding 初始化了一系列的Binding,这些Binding持有了我们上面说的一些owner,比如BuildOwner,PipelineOwner,所以随着WidgetsFlutterBinding的初始化,其他的Binding也被初始化了,

GestureBinding 提供了 window.onPointerDataPacket 回调,绑定 Framework 手势子系统,是 Framework 事件模型与底层事件的绑定入口
ServicesBinding 提供了 window.onPlatformMessage 回调, 用于绑定平台消息通道(message channel),主要处理原生和 Flutter 通信
SchedulerBinding 提供了 window.onBeginFrame 和 window.onDrawFrame 回调,监听刷新事件,绑定 Framework 绘制调度子系统
PaintingBinding 绑定绘制库,主要用于处理图片缓存
SemanticsBinding 语义化层与 Flutter engine 的桥梁,主要是辅助功能的底层支持
RendererBinding 提供了 window.onMetricsChanged 、window.onTextScaleFactorChanged 等回调。它是渲染树与 Flutter engine 的桥梁
WidgetsBinding 提供了 window.onLocaleChanged、onBuildScheduled 等回调。它是 Flutter widget 层与 engine 的桥梁

继续跟进下attachRootWidget(app)

void attachRootWidget(Widget rootWidget) {
    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]',
      child: rootWidget
    ).attachToRenderTree(buildOwner, renderViewElement);
  }
复制代码

内部创建了 RenderObjectToWidgetAdapter 并将我们传入的app 自定义widget做了child,接着执行attachToRenderTree这个方法,创建了第一个Element和RenderObject

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;
  }
复制代码

image.png

我们解释一下上面的图片,Root的创建比较简单:

  • 1.attachRootWidget(app) 方法创建了Root[Widget](也就是 RenderObjectToWidgetAdapter)
  • 2.紧接着调用attachToRenderTree方法创建了 Root[Element]
  • 3.Root[Element]尝试调用mount方法将自己挂载到父Element上,因为自己就是root了,所以没有父Element,挂空了
  • 4.mount的过程中会调用Widget的createRenderObject,创建了 Root[RenderObject]

它的child,也就是我们传入的app是怎么挂载父控件上的呢?

  • 5.我们将app作为Root[Widget](也就是 RenderObjectToWidgetAdapter),appp[Widget]也就成了为root[Widget]的child[Widget]
  • 6.调用owner.buildScope,开始执行子Tree的创建以及挂载,敲黑板!!!这中间的流程和WidgetTree的刷新流程是一模一样的,详细流程我们后面讲!
  • 7.调用createElement方法创建出Child[Element]
  • 8.调用Element的mount方法,将自己挂载到Root[Element]上,形成一棵树
  • 9.挂载的同时,调用widget.createRenderObject,创建Child[RenderObject]
  • 10.创建完成后,调用attachRenderObject,完成和Root[RenderObject]的链接

就这样,WidgetTree、ElementTree、RenderObject创建完成,并有各自的链接关系。

这里有两个操作需要注意下,

mount

abstract class Elementvoid 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
    ...
  }
复制代码

我们先看一下Element的挂载,就是让_parent持有父Element的引用,因为RootElement 是没有父Element的,所以参数传了null:element.mount(null, null);
还有两个值得注意的地方:

  • 节点的深度_depth 也是在这个时候计算的,深度对刷新很重要
  • 每个Element的buildOwner,都来自父类的BuildOwner,这样可以保证一个ElementTree,只由一个BuildOwner来维护。

RenderObjectElement

abstract class RenderObjectElement:

@override
  void attachRenderObject(dynamic newSlot) {
    ...
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    _ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
    ...
  }
复制代码

RenderObject与父RenderObject的挂载稍微复杂了点。通过代码我们可以看到需要先查询一下自己的AncestorRenderObject,这是为什么呢?
还记得之前我们讲过,每一个Widget都有一个对应的Element,但Element不一定会有对应的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子父间的挂载。

5、渲染过程

当需要更新UI的时候,Framework通知Engine,Engine会等到下个Vsync信号到达的时候,会通知Framework,然后Framework会进行animations, build,layout,compositing,paint,最后生成layer提交给Engine。Engine会把layer进行组合,生成纹理,最后通过Open Gl接口提交数据给GPU, GPU经过处理后在显示器上面显示。整个流程如下图:

6、渲染触发 (setState)

setState背后发生了什么

在Flutter开发应用的时候,当需要更新的UI的时候,需要调用一下setState方法,然后就可以实现了UI的更新,我们接下来分析一下该方法做哪些事情。

void setState(VoidCallback fn) {
   ...
    _element.markNeedsBuild(); //通过相应的element来实现更新,关于element,widget,renderOjbect这里不展开讨论
  }
复制代码

继续追踪

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

widget对应的element将自身标记为dirty状态,并调用owner.scheduleBuildFor(this);通知buildOwner进行处理

	void scheduleBuildFor(Element element) {
    ...
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      onBuildScheduled(); //这是一个callback,调用的方法是下面的_handleBuildScheduled
    }
    _dirtyElements.add(element); //把当前element添加到_dirtyElements数组里面,后面重新build会遍历这个数组
    element._inDirtyList = true;
    
  }
复制代码

后续MyStatefulWidget的build方法一定会被执行,执行后,会创建新的子Widget出来,原来的子Widget便被抛弃掉了,原来的子Widget肯定是没救了,但他们的Element大概率还是有救的,此时 buildOwner会将所有dirty的Element添加到_dirtyElements当中
经过Framework一连串的调用后,最终调用scheduleFrame来通知Engine需要更新UI,Engine就会在下个vSync到达的时候通过调用_drawFrame来通知Framework,然后Framework就会通过BuildOwner进行Build和PipelineOwner进行Layout,Paint,最后把生成Layer,组合成Scene提交给Engine。

底层引擎最终回到Dart层,并执行buildOwner的buildScope方法,首先从Engine回调Framework的入口开始。

	void _drawFrame() { //Engine回调Framework入口 
  _invoke(window.onDrawFrame, window._onDrawFrameZone);
	}

	//初始化的时候把onDrawFrame设置为_handleDrawFrame
  void initInstances() {
    super.initInstances();
    _instance = this;
    ui.window.onBeginFrame = _handleBeginFrame;
    ui.window.onDrawFrame = _handleDrawFrame;
    SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
  }
  
  void _handleDrawFrame() {
    if (_ignoreNextEngineDrawFrame) {
      _ignoreNextEngineDrawFrame = false;
      return;
    }
    handleDrawFrame();
  }
  void handleDrawFrame() {
      _schedulerPhase = SchedulerPhase.persistentCallbacks;//记录当前更新UI的状态
      for (FrameCallback callback in _persistentCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);
    }
  }

  void initInstances() {
    ....
    addPersistentFrameCallback(_handlePersistentFrameCallback);
  }

 	void _handlePersistentFrameCallback(Duration timeStamp) {
    drawFrame();
  }

  void drawFrame() {
    ...
     if (renderViewElement != null)
        buildOwner.buildScope(renderViewElement); //先重新build widget
      super.drawFrame();
      buildOwner.finalizeTree();
      
  }

复制代码


核心方法 buildScope

void buildScope(Element context, [VoidCallback callback]){
	...
}
复制代码

需要传入一个Element的参数,这个方法通过字面意思应该理解就是对这个Element以下范围rebuild

void buildScope(Element context, [VoidCallback callback]) {
    ...
    try {
		...
      _dirtyElements.sort(Element._sort); //1.排序
     	...
      int dirtyCount = _dirtyElements.length;
      int index = 0;
      while (index < dirtyCount) {
        try {
          _dirtyElements[index].rebuild(); //2.遍历rebuild
        } catch (e, stack) {
        }
        index += 1;
      }
    } finally {
      for (Element element in _dirtyElements) {
        element._inDirtyList = false;
      }
      _dirtyElements.clear();  //3.清空
		...
    }
  }
复制代码

这里对上面方法做下解释

  • 第1步:按照Element的深度从小到大,对_dirtyElements进行排序

由于父Widget的build方法必然会触发子Widget的build,如果先build了子Widget,后面再build父Widget时,子Widget又要被build一次。所以这样排序之后,可以避免子Widget的重复build。

  • 第2步:遍历执行_dirtyElements当中element的rebuild方法

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

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

  • 第3步:遍历结束之后,清空dirtyElements集合

因此setState()过程主要工作是记录所有的脏元素,添加到BuildOwner对象的_dirtyElements成员变量,然后调用scheduleFrame来注册Vsync回调。 当下一次vsync信号的到来时会执行handleBeginFrame()和handleDrawFrame()来更新UI。

Element的Diff

在上面的第二步会遍历执行element的build方法
  _dirtyElements[index].rebuild(); //2.遍历rebuild
element的rebuild方法最终会调用performRebuild(),而performRebuild()不同的Element有不同的实现,以下面两个为例

  • ComponentElement,是StatefulWidget和StatelessElement的父类
  • RenderObjectElement, 是有渲染功能的Element的父类
ComponentElement的performRebuild()
void performRebuild() {
    Widget built;
    try {
      built = build();
    } 
    ...
    try {
      _child = updateChild(_child, built, slot);
    } 
    ...
  }
复制代码

执行element的build();,以StatefulElement的build方法为例:Widget build() => state.build(this);。 就是执行了我们复写的StatefulWidget的state的build方法,此时创建出来的当然就是这个StatefulWidget的子Widget了

下面看下核心方法 Element updateChild(Element child, Widget newWidget, dynamic newSlot)

Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
	...
		//1
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    
    if (child != null) {
    	//2
      if (child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        return child;
      }
      //3
      if (Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        return child;
      }
      deactivateChild(child);
    }
    //4
    return inflateWidget(newWidget, newSlot);
  }
复制代码

参数child 是上一次Element挂载的child Element, newWidget 是刚刚build出来的。updateChild有四种可能的情况

  • 1.如果刚build出来的widget等于null,说明这个控件被删除了,child Element可以被删除了。

  • 2.如果child的widget和新build出来的一样(Widget复用了),就看下位置一样不,不一样就更新下,一样就直接return了。Element还是旧的Element

  • 3.看下Widget是否可以update,Widget.canUpdate的逻辑是判断key值和运行时类型是否相等。如果满足条件的话,就更新,并返回。


中间商的差价哪来的呢?只要新build出来的Widget和上一次的类型和Key值相同,Element就会被复用!由此也就保证了虽然Widget在不停的新建,但只要不发生大的变化,那Element是相对稳定的,也就保证了RenderObject是稳定的!

  • 4.如果上述三个条件都没有满足的话,就调用 inflateWidget() 创建新的Element

这里再看下inflateWidget()方法:

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;
  }
复制代码

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

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

与ComponentElement的不同之处在于,没有去build,而是调用了updateRenderObject方法更新RenderObject。到这里我们基本就明白了Element是如何在中间应对Widget的多变,保障RenderObject的相对不变了

7、参考




原文地址:访问原文地址
快照地址: 访问文章快照