Flutter UI渲染分析

Posted 渡口一艘船

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 自绘,组件之间可以任意组合

4、视图树

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

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

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

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也就更新了,如下图所示
可以看到WidgetTree全部被替换了,但是ElmentTree和RenderObjectTree只替换了变化的部分
其中 PipelineOwner类似于Android中的ViewRootImpl,管理着真正需要绘制的View,
最后PipelineOwner会对RenderObjectTree中发生变化节点的进行layout、paint、合成等等操作,最后交给底层引擎渲染。

Widget、Element、RenderObject之间的关系

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

可以看出 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;
  

我们解释一下上面的图片,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 Element:

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
    ...
  

我们先看一下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、参考

以上是关于Flutter UI渲染分析的主要内容,如果未能解决你的问题,请参考以下文章

Flutter 核心渲染流程分析

Flutter 核心渲染流程分析 [完结篇]

Flutter UI 渲染原理概览

Flutter UI 渲染原理概览

Flutter Dart Framework原理简解

Flutter Android 发布 UI 渲染失败