帝国的纷争-Flutter-UI绘制解析

栏目: Android · 发布时间: 6年前

内容简介:为避免传统的源码讲解方式的枯燥乏味,这一次,我尝试换一种方式,带着你以轻松的心态了解Flutter世界里的UI绘制流程,去探究Widget、Element、RenderObject的秘密。废话不多说,听故事!《帝国的纷争》十载干戈,移动端格局渐定,壁垒分明。

为避免传统的源码讲解方式的枯燥乏味,这一次,我尝试换一种方式,带着你以轻松的心态了解Flutter世界里的UI绘制流程,去探究Widget、Element、RenderObject的秘密。

废话不多说,听故事!《帝国的纷争》

故事

十载干戈,移动端格局渐定,壁垒分明。

北方草原金帐王朝Javascript虽内部纷争不断,但却一直窥视中原大陆,数年来袭扰不断,如今已夺得小片领土(ReactNative)。民间盛传:大前端融合之势已现!

2018年冬,Android边境小城Flutter突然宣布立国!并对两个移动端帝国正式宣战!!短短几日,已攻下数城。

帝国的纷争-Flutter-UI绘制解析

而今天我们要讲的故事,就发生在战火最严重的Android边陲重镇:View城。

某日,Android View 城军事会议:

镇边大将军对手下谋士道:“Flutter 最近对我们发起了数次进攻,已下数城,知己不知彼乃军家大忌!谁能给我说说这个Flutter和我们现在的View到底有什么区别?”

下方谋士面面相窥,不得已终于一个谋士站了出来:“我愿意替将军前去打探一番!”

数日后,谋士:“臣卧底归来,探明Flutter与我们View城的主要区别在于编程范式和视图逻辑单元不同”

将军:“先讲编程范式如何不同?”

Android/Flutter 编程范式

将军,我们Android现在视图开发是命令式的,我们的每一个View都直接听从将军(Developer)的指挥,例如:想要变更界面某个文案,便要指明具体TextView调用他的setText方法命令文字发生变更;

帝国的纷争-Flutter-UI绘制解析

而Flutter的视图开发是声明式的,对方的将军要做的是维护一套数据集,以及设定好一套布军计划(WidgetTree),并且为Widget“绑定”数据集中的某个数据,根据这个数据来渲染。

例如当需要变更文案时,便改变数据集中的数据,然后直接触发WidgetTree的重新渲染。这样Flutter的将军不再需要关注每一个士兵,大部分的精力都用来维护核心数据即可。

帝国的纷争-Flutter-UI绘制解析

如果每一次操作都消耗一点将军的精力值,又刚好有同一个数据“绑定”到了多个View或Widget上。命令式的编程需要做的事情是 命令N个View发生变更,消耗N点精力值;

声明式编程需要做的事情是 变更数据+触发WidgetTree重绘,消耗2点精力值;对精力的解放,也是Flutter可以快速招揽到那么多将军的原因之一。

将军:”但每次数据变更,都会触发WidgetTree的重绘,消耗的资源未免也太大了吧,我现在虽然多消耗些精力,但不会存在大量对象创建的情况“。

Widget、Element、RenderObject概念

谋士:这也是马上要讲的第二点不同。因为WidgetTree会大量的重绘,所以Widget必然是廉价的。

Flutter UI有三大元素: Widget、Element、RenderObject 。对应这三者也有三个owner负责管理他们,分别是 WidgetOwner(将军&Developer)、BuildOwner、PipelineOwner

  • Widget,Widget 并不是真正的士兵,它只是将军手中的棋子,是一些廉价的纯对象,持有一些渲染需要的配置信息,棋子在不断被替换着。

  • RenderObject,RenderObject 是真正和我们作战的士兵,在概念上和我们Android的View一样,渲染引擎会根据RenderObject来进行真正的绘制,它是相对稳定且昂贵的。

  • Element,使得不断变化Widget转变为相对稳定的RenderObject的功臣是Element。

WidgetOwner(Developer)在不断改变着布军计划,然后向 BuildOwner 发送着一张又一张 计划表(WidgetTree) ,首次的计划表(WidgetTree)会生成一个与之对应的ElementTree,并生成对应的RenderObjectTree。

帝国的纷争-Flutter-UI绘制解析

后续BuildOwner每次收到新的计划表就与上一次的进行对比,在ElementTree上只更新变化的部分,Element变化之后,与之对应的RenderObject也就更新了。

帝国的纷争-Flutter-UI绘制解析

可以看到WidgetTree全部被替换了,但ElementTree和RenderObjectTree只替换了变化的部分。

差点忘了讲 PipelineOwnerPipelineOwner 类似于Android中的ViewRootImpl,管理着真正需要绘制的View,

最后PipelineOwner会对RenderObjectTree中发生变化节点的进行layout、paint、合成等等操作,最后交给底层引擎渲染。

将军:“我大概明白了,看来保证声明式编程性能稳定的核心在于这个Element和BuildOwner。但我看这里还有两个问题,RenderObject好像少了一个节点?你画图画错了吗?还有能给我讲下他是怎么把Widget和RenderObject链接起来,以及发生变化时,BuildOwner是如何做到元素Diff的吗?”

Widget、Element、RenderObject之间的关系

首先,每一个Widget家族的老长辈Widget赋予了所有的Widget子类三个关键的能力:保证自身唯一以及定位的Key, 创建Element的 createElement, 和 canUpdate。 canUpdate 的作用后面讲。

Widget子类里还有一批特别优秀强壮的,是在纸面上代表着有渲染能力的RenderObjectWidget,它还有一个创建 RenderObject的 createRenderObject 方法。

帝国的纷争-Flutter-UI绘制解析

从这里你也看出来了,Widget、Element、RenderObject的创建关系并不是线性传递的, Element和RenderObject都是Widget创建出来的** ,也并不是每一个Widget都有与之对应的RenderObjectWidget**。这也解释上面图中RenderObjectTree看起来和前面的WidgetTree缺少了一些节点。

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

讲第一次创建,一定要从第一个被创建出来的士兵说起。我们都知道Android的ViewTree:

-PhoneWindow
	- DecorView
		- TitleView
		- ContentView

已经预先有这么多View了,相比Android的ViewTree,Flutter的WidgetTree则要简单的多,只有最底层的root widget。

- RenderObjectToWidgetAdapter<RenderBox>
	- MyApp (自定义)
	- MyMaterialApp (自定义)

简单介绍一下RenderObjectToWidgetAdapter,不要被他的adapter名字迷惑了,RenderObjectToWidgetAdapter其实是一个RenderObjectWidget,他就是第一个优秀且强壮的Widget。

这个时候就不得不搬出代码来看了,runApp源码:

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

WidgetsFlutterBinding ”迷信“了一系列的Binding,这些Binding持有了我们上面说的一些owner,比如BuildOwner,PipelineOwner,所以随着WidgetsFlutterBinding的初始化,其他的Binding也被初始化了,此时Flutter 的国家引擎开始转动了!

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

我们最需要关注的是 attachRootWidget(app) 这个方法,这个方法很神圣 ,很多的第一次就在这个方法里实现了!!(将军:“很神圣?你是不叛变了?”),app 是我们传入的自定义Widget,内部会创建RenderObjectToWidgetAdapter,并将app做为它的child的。

帝国的纷争-Flutter-UI绘制解析

Root的创建比较简单:

attachRootWidget(app)
attachToRenderTree
mount
createRenderObject

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

createElement
mount
widget.createRenderObject
attachRenderObject

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

通过上图我们可以看到,虽然createRenderObject方法的实现是在Widget当中,但持有RenderObject引用的却是Element。

这可也可以理解的嘛~ Widget在不停的变换,Element相对稳定并持有RenderObject,当Element都被换掉的时候,RenderObject也就该被下掉了。

另外在 attachToRenderTree 方法里我们也可以这段代码: element._newWidget = this; 。这里的this是Widget自身。element也持有了widget的引用。 element就是Widget 和RenderObject的中间商,它也确实在赚差价……

帝国的纷争-Flutter-UI绘制解析

这个时候Root Widget,Root Element,Root RenderObject都已经创建完成并且三者链接成功。将军您看还有什么问题吗?

将军:“Flutter内部还有中间商赚差价呢?真腐败!你刚才说第6不已经执行了一次刷新流程是什么意思?”

子Widget的首次初始化和后续发生变化时Element tree的变化流程是一样的,我们马上就讲!

Flutter的刷新流程:Element的复用

attachRootWidget -> attachToRenderTree

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
    if (element == null) {
      ...
    } else {
      element._newWidget = this;
      element.markNeedsBuild();
    }
    return element;
  }

我们会看到 attachToRenderTree 方法最后还调用了 element.markNeedsBuild(); 敲黑板!记住这句话,很重要!(将军:“你还给我敲黑板呢?”)

Flutter如果想要刷新界面,需要在StatefulWidget里调用 setState() 方法, setState() 干啥了呢?

@protected

void setState(VoidCallback fn) {

_element.markNeedsBuild();

}

哈哈哈~是不是和 attachToRenderTree 中最后调用的方法一样!

继续这样追着代码看,我怕将军您睡着了,所以我们后面我竟可能用少的源码去进行讲解!

将军我们实际演练一下,假设Flutter派出了这么一个WidgetTree:

帝国的纷争-Flutter-UI绘制解析

刷新第1步:Element标记自身为dirty,并通知buildOwner处理

当对方想改变下方Text Widget的文案时,会在MyStatefulWidget内部调用 setState((){_title="ttt"}) (点击事件没写,暂时先靠想象),之后该widget对应的element将自身标记为 dirty 状态,并调用 owner.scheduleBuildFor(this); 通知buildOwner进行处理。

帝国的纷争-Flutter-UI绘制解析

后续MyStatefulWidget的build方法一定会被执行,执行后,会创建新的子Widget出来,原来的子Widget便被抛弃掉了(将军:“好好的一个对象就这么被浪费了,哎……现在的年轻人~”)。

原来的子Widget肯定是没救了,但他们的Element大概率还是有救的。

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

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

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

帝国的纷争-Flutter-UI绘制解析

刷新第3步:底层引擎最终回到Dart层,并执行buildOwner的buildScope方法

这里很重要,所以用代码讲更清晰!

void buildScope(Element context, [VoidCallback callback]){...}
buildScope需要传入一个Element的参数,通过字面意思我们应该能理解,大概就是对这个Element以下(包含)的范围rebuild。

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步:按照Element的深度从小到大,对_dirtyElements进行排序

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

3.2步:遍历执行_dirtyElements当中element的rebuild方法

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

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

3.3步:遍历结束之后,清空dirtyElements集合

刷新第4步:执行performRebuild()

performRebuild()不同的Element有不同的实现,我们暂时只看最常用的两个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方法啦~

执行build方法build出来的是啥呢? 当然就是这个StatefulWidget的子Widget了。重点来了!敲黑板!!(将军:“又给我敲黑板??”)Element就是在这个地方赚差价的!

_child = updateChild(_child, built, slot);

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可以被删除了。

帝国的纷争-Flutter-UI绘制解析

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

帝国的纷争-Flutter-UI绘制解析

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

帝国的纷争-Flutter-UI绘制解析

中间商的差价哪来的呢?只要新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。

帝国的纷争-Flutter-UI绘制解析

RenderObjectElement的performRebuild()

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

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

不同Widget也有不同的updateRenderObject实现,我们看一下最常用的RichText,也就是Text。

void updateRenderObject(BuildContext context, RenderParagraph renderObject) {
    assert(textDirection != null || debugCheckHasDirectionality(context));
    renderObject
      ..text = text
      ..textAlign = textAlign
      ..textDirection = textDirection ?? Directionality.of(context)
      ..softWrap = softWrap
      ..overflow = overflow
      ..textScaleFactor = textScaleFactor
      ..maxLines = maxLines
      ..locale = locale ?? Localizations.localeOf(context, nullOk: true);
  }

一些看起来比较熟悉的赋值操作,像不像Android的view呀? 要不怎么说RenderObject实际相当于Android里的View呢。

到这里你基本就明白了Element是如何在中间应对Widget的多变,保障RenderObject的相对不变了吧~

Flutter的刷新流程:PipelineOwner对RenderObject的管理

在底层引擎最终回到Dart层,最终会执行 WidgetsBinding 的drawFrame ()

WidgetsBinding

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

buildOwner.buildScope(renderViewElement); 就是我们上面讲过的。

下面看一下 super.drawFrame(); 主要是PipelineOwner对RenderObject的管理我们暂时放在下期介绍。

Flutter的刷新流程:清理

drawFrame方法在最后执行了 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();
    }
  }

在做最后的清理工作。

将军:“_inactiveElements”又是个啥?之前咋没见过?

还记的前面讲Element赚差价的updateChild方法吗?所有没用的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在最后将被清空,就没办法在复用了。

结尾

将军,现在您对Flutter的绘制流程有了初步的了解了吗?

将军:“有些了解了,但你讲了这么多,对比起来我们Android,听起来Flutter这一套绘制流程没啥缺点? ”

当然有了,我们现在也只了解了Flutter的冰山一角,很多东西还没有发现。

但就只说动态向ViewTree中插入组件这一条,Flutter就没有我们灵活。比如Toast,现在的Toast组件都只能依赖methodChannel去调用我们原生的Toast。而Flutter提供的Scaffold组件,也是通过预先埋好的组件坑位,才实现了Material风格的Toast。

因为Flutter是声明式的,想要在运行中随时向WidgetTree插入一个Widget,目前还没有成熟接口。

但相信随着Flutter开发者对Flutter内部原理越来越熟悉,这种问题很快就会被解决的。


以上所述就是小编给大家介绍的《帝国的纷争-Flutter-UI绘制解析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Artificial Intelligence

Artificial Intelligence

Stuart Russell、Peter Norvig / Pearson / 2009-12-11 / USD 195.00

The long-anticipated revision of this #1 selling book offers the most comprehensive, state of the art introduction to the theory and practice of artificial intelligence for modern applications. Intell......一起来看看 《Artificial Intelligence》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具