Flutter框架分析(四)-- Flutter框架的运行

前言

前面几篇文章介绍了Flutter框架的渲染流水线,window,初始化以及WidgetElementRenderObject体系。其中对WidgetElementRenderObject的介绍主要是一些静态的说明,了解了以上这些技术点之后,在这篇文章里我们会通过动态运行的方式来介绍一下Flutter框架是如何运行的。
从之前介绍的渲染流水线可以知道,这个过程大致可以分为两段操作。第一段是从State.setState()到去engine那里请求一帧,第二段就是Vsync信号到来以后渲染流水线开始重建新的一帧最后送入engine去显示。我们先来看第一段Flutter框架都做了什么。

调度之前

先看一下State.setState()

void setState(VoidCallback fn) {
    
    final dynamic result = fn() as dynamic;
  
    _element.markNeedsBuild();
  }

这里会调用到ElementmarkNeedsBuild()函数。

void markNeedsBuild() {
    if (!_active)
      return;
    if (dirty)
      return;
    _dirty = true;
    owner.scheduleBuildFor(this);
  }

Element首先看自己是不是active的状态,不是的话就直接返回了,如果是“脏”(dirty)的状态也直接返回,不是的话会置上这个状态然后调用BuildOwnerscheduleBuildFor()函数,这个BuildOwner我们之前介绍过,它的实例是在WidgetsBinding初始化的时候构建的。每个Element的都会持有BuildOwner的引用。由其父Elementmount的时候设置。

  void scheduleBuildFor(Element element) {
    if (element._inDirtyList) {
      _dirtyElementsNeedsResorting = true;
      return;
    }
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      onBuildScheduled();
    }
    _dirtyElements.add(element);
    element._inDirtyList = true;
  }

BuildOwner会维护一个_dirtyElements列表,所有被标记为“脏”(dirty)的element都会被添加进去。在此之前会调用onBuildScheduled()。这个函数是WidgetsBinding初始化的时候设置给BuildOwner的,对应的是WidgetsBinding._handleBuildScheduled()

void _handleBuildScheduled() {
    ensureVisualUpdate();
  }

这里会调用到ensureVisualUpdate()。这个函数定义在SchedulerBinding里的

void ensureVisualUpdate() {
    switch (schedulerPhase) {
      case SchedulerPhase.idle:
      case SchedulerPhase.postFrameCallbacks:
        scheduleFrame();
        return;
      case SchedulerPhase.transientCallbacks:
      case SchedulerPhase.midFrameMicrotasks:
      case SchedulerPhase.persistentCallbacks:
        return;
    }
  }

函数ensureVisualUpdate()会判断当前调度所处的状态,如果是在idle(空闲)或者postFrameCallbacks运行状态则调用scheduleFrame()。其他状态则直接返回。下面这三个状态正是渲染流水线运行的时候。

void scheduleFrame() {
    if (_hasScheduledFrame || !_framesEnabled)
      return;
    window.scheduleFrame();
    _hasScheduledFrame = true;
  }

在函数scheduleFrame()里我们看到了熟悉的window。这里就是通知engine去调度一帧的地方了。调度之后会置上_hasScheduledFrame标志位,避免重复请求。另外一个标志位_framesEnabled是代表当前app的状态,或者说其所处的生命周期是否允许刷新界面。这个状态有四种:resumedinactivepausedsuspending

  • resumed:app可见且可以响应用户输入。
  • inactive:app不能响应用户输入,例如在Android上弹出系统对话框。
  • paused:app对用户不可见。
  • suspending:app挂起??这个状态貌似Android和iOS都没有上报。

_framesEnabled只有在resumedinactive状态下才为true。也就是说,只有在这两个状态下Flutter框架才会刷新页面。

至此第一阶段,也就是调度之前的工作做完了。看起来比较简单,主要就是把需要重建的Element放入_dirtyElements列表。接下来Flutter框架会等待Vsync信号到来以后engine回调框架,这就是第二段要做的事情了。

Vsync到来之后

我们之前说过Vsync信号到来之后,engin会按顺序回调window的两个回调函数:onBeginFrame()onDrawFrame()。这两个回调是SchedulerBinding初始化的时候设置给window的。对应的是SchedulerBinding.handleBeginFrame()SchedulerBinding.handleDrawFrame()

onBeginFrame

这个回调会直接走到SchedulerBinding.handleBeginFrame()

  void handleBeginFrame(Duration rawTimeStamp) {
   ...
    _hasScheduledFrame = false;
    try {
      // TRANSIENT FRAME CALLBACKS
      _schedulerPhase = SchedulerPhase.transientCallbacks;
      final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
      _transientCallbacks = <int, _FrameCallbackEntry>{};
      callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
        if (!_removedIds.contains(id))
          _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);
      });
      _removedIds.clear();
    } finally {
      _schedulerPhase = SchedulerPhase.midFrameMicrotasks;
    }
  }

这个函数主要是在依次回调“Transient”回调函数,这些回调函数是在调度之前设置在SchedulerBinding里的,这里的“Transient”意思是临时的,或者说是一次性的。原因是这些回调函数只会被调用一次。注意看代码里_transientCallbacks被置为空Map了。如果想在下一帧再次调用的话需要提前重新设置回调。这些回调主要和动画有关系。也就是渲染流水线里的第一阶段,动画(Animate)阶段。关于动画后续我会再写文章从框架角度分析一下动画的机制。

在运行回调之前_schedulerPhase的状态被设置为SchedulerPhase.transientCallbacks。回调处理完以后状态更新至SchedulerPhase.midFrameMicrotasks意思是接下来会处理微任务队列。处理完微任务以后,engine会接着回调onDrawFrame()

onDrawFrame

这个回调会直接走到SchedulerBinding.handleDrawFrame()

void handleDrawFrame() {
    try {
      // PERSISTENT FRAME CALLBACKS
      _schedulerPhase = SchedulerPhase.persistentCallbacks;
      for (FrameCallback callback in _persistentCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);

      // POST-FRAME CALLBACKS
      _schedulerPhase = SchedulerPhase.postFrameCallbacks;
      final List<FrameCallback> localPostFrameCallbacks =
          List<FrameCallback>.from(_postFrameCallbacks);
      _postFrameCallbacks.clear();
      for (FrameCallback callback in localPostFrameCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);
    } finally {
      _schedulerPhase = SchedulerPhase.idle;
      _currentFrameTimeStamp = null;
    }
  }

handleDrawFrame里按顺序处理了两类回调,一类叫“Persistent”回调,另一类叫“Post-Frame”回调。

“Persistent”字面意思是永久的。这类回调一旦注册以后是不能取消的。主要用来驱动渲染流水线。渲染流水线的构建(build),布局(layout)和绘制(paint)阶段都是在其中一个回调里的。

“Post-Frame”回调主要是在新帧渲染完成以后的一类调用,此类回调只会被调用一次。

在运行“Persistent”回调之前_schedulerPhase状态变为SchedulerPhase.persistentCallbacks。在运行“Post-Frame”回调之前_schedulerPhase状态变为SchedulerPhase.postFrameCallbacks。最终状态变为SchedulerPhase.idle

这里我们主要关注一个“Persistent”回调:WidgetsBinding.drawFrame()。这个函数是在RendererBinding初始化的时候加入到“Persistent”回调的。

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

这里首先会调用buildOwner.buildScope(renderViewElement)。其入参renderViewElement是element tree的根节点。此时渲染流水线就进入了构建(build)阶段。接下来调用了super.drawFrame()。这个函数定义在RendererBinding中。

void drawFrame() {
  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();
  renderView.compositeFrame(); // this sends the bits to the GPU
  pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}

可以看出渲染流水线的接力棒传到了pipelineOwner的手里,渲染流水线就进入了布局(layout)阶段和绘制(paint)阶段。关于最后这两个阶段本篇不做详细介绍。这里大家只要知道绘制完成以后Flutter框架最终会调用window.render(scene)将新帧的数据送入engine显示到屏幕。

最后调用buildOwner.finalizeTree();。这个函数的作用是清理不再需要的Element节点。在element tree更新以后可能有些节点就不再需要挂载在树上了,在finalizeTree()的时候会将这些节点及其子节点unmount。

构建(build)阶段

void buildScope(Element context, [VoidCallback callback]) {
    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;
      }
      
    } finally {
      for (Element element in _dirtyElements) {
        element._inDirtyList = false;
      }
      _dirtyElements.clear();
      _scheduledFlushDirtyElements = false;
      _dirtyElementsNeedsResorting = null;
     
    }
  }

还记得在调度帧之前会把需要更新的Element标记为“脏”(dirty)并放入BuildOwner_dirtyElements列表。这里Flutter会先按照深度给这个列表排个序。因为Element在重建的时候其子节点也都会重建,这样如果父节点和子节点都为“脏”的话,先重建父节点就避免了子节点的重复重建。

排完序就是遍历_dirtyElements列表。依次调用Element.rebuild()。这个函数又会调用到Element.performRebuild()。我们之前介绍Element的时候说过performRebuild()由其子类实现。

我们之前的出发点是State.setState()。那就先看看StatefulElement如何做的。它的performRebuild()是在其父类ComponentElement里:

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

回忆一下ComponentElement。这个build()函数最终会调用到State.build()了。返回的就是我们自己实例化的Widget。拿到这个新Widget就去调用updateChild()。之前在讲Element的时候我们介绍过updateChild()这个函数。由增,删,改这么几种情况,对于MyWidget,从State.setState()过来是属于改的情况。此时会调用child.update(newWidget);。这个update()函数又是由各个Element子类实现的。这里我们只列举几个比较典型的。

StatefulElementStatelessElementupdate()函数最终都会调用基类Elementrebuild()函数。好像在兜圈圈的感觉。。。

RenderObjectElementupdate()函数就比较简单了

void update(covariant RenderObjectWidget newWidget) {
    super.update(newWidget);
    widget.updateRenderObject(this, renderObject);
    _dirty = false;
  }

更新只是调用了一下RenderObjectWidget.updateRenderObject()。这个函数我们之前介绍过,只是把新的配置设置到现有的RenderObject上。

回到上面那个兜圈圈的问题。理清这里的调用关系的关键就是要搞清楚是此时的Element是在对自己进行操作还是对孩子进行操作。假设我们有这样的一个三层element tree进行更新重建。

  父(StatefulElement)

  子(StatefulElement)

  孙(LeafRenderObjectElement)

那么从父节点开始,调用顺序如下:

父.rebuild()--->父.performRebuild()--->父.updateChild()--->子.update()--->子.rebuild()--->子.performRebuild()--->子.updateChild()--->孙.update()。

可见构建(build)过程是从需要重建的Element节点开始一层层向下逐个更新子节点。直到遇到叶子节点为止。

至此渲染流水线的构建(build)阶段就跑完了。接下来就由pipelineOwner驱动开始布局(layout)和绘制(paint)阶段了。这两个阶段留待以后再给大家介绍一下。

总结

本篇文章从我们熟悉的State.setState()函数出发,大致介绍了Flutter框架是如何运行渲染流水线的。总体来说其运行时分为两个阶段,向engine调度帧之前和Vsync信号到来engine回调Flutter框架之后。剩余篇幅则是以更新Element为例介绍了一下渲染流水线的构建(build)阶段都做了一些什么事情。限于篇幅,没有更多涉及Element的新增和删除步骤。大家感兴趣的话可以直接看源码来了解相关信息。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容