Flutter 渲染原理解析

108 阅读4分钟
目的:
  1. 理清整体flutter渲染机制如何运行
  2. flutter 优化打基础

目录:

flutter 系统结构概览

flutter 渲染的流程 (为何掉帧、为何会产生卡顿)

flutter 解决卡顿的初步优化方式

flutter 系统结构概览

一张图概括 :

flutter_frame_scheduler_1.png

Flutter Framework: 纯 Dart实现的 SDK,类似于 React在 JavaScript中的作用。它实现了一套基础库, 用于处理动画、绘图和手势。并且基于绘图封装了一套 UI组件库,然后根据 Material 和Cupertino两种视觉风格区分开来。这个纯 Dart实现的 SDK被封装为了一个叫作 dart:ui的 Dart库。我们在使用 Flutter写 App的时候,直接导入这个库即可使用组件等功能。

Flutter Engine: 纯 C++实现的 SDK,其中包括 Skia引擎、Dart运行时、文字排版引擎等。它是 Dart的一个运行时,它可以以 JIT 或者 AOT的模式运行 Dart代码。这个运行时还控制着 VSync信号的传递、GPU数据的填充等,并且还负责把客户端的事件传递到运行时中的代码。

结论:

Flutter Framework 层负责具体的UI处理、事件分发、动画绘制、widget 绘制的工作, 最终将动画和Redering 转化成layer 树绘制命令,传递到Flutter Engine 层中

flutter 渲染流程 (为何掉帧、为何会产生卡顿)

渲染的源头:

我们都知道,显示屏上的渲染都是在VSync信号驱动下进行的,Flutter在Android上的渲染也不例外,它会向Android系统注册并等待VSync信号,等到VSync信号到来以后,调用沿着C++ Engine->Java Engine,到达Dart Framework,开始执行Dart代码,经历Layout、Paint等过程,生成一棵Layer Tree,将绘制指令保存在Layer中,接着进行栅格化和合成上屏。

VSync信号 : 屏幕向系统发送的渲染界面的指令 , 一般是1秒60次 ,也就是60HZ刷新率, 也有120HZ的,就相当于每秒屏幕会渲染多少帧的界面

掉帧和卡顿的由来

一般的机型的刷新率是60Hz , 也就是1秒界面需要渲染60帧画面, 平均16毫秒1帧 ,如果某一帧的时间超过16毫秒,甚至到了200毫秒才渲染,就会导致后续的帧渲染都变慢,视觉中就形成了掉帧, 在更高刷新频率的手机上这种掉帧会更明显

卡顿就是因为频繁掉帧带来的体验效果

flutter 渲染流程图概览

  • UI线程:运行着UI Task Runner,是Flutter Engine用于执行Dart root isolate代码,将其转换为layer tree视图结构;
  • GPU线程:该线程依然是在CPU上执行,运行着GPU Task Runner,处理layer tree,将其转换成为GPU命令并发送到GPU。

flutter_frame_scheduler_2.png

  • 当需要渲染则会调用到Engine的ScheduleFrame()来注册VSYNC信号回调,一旦触发回调doFrame()执行完成后,便会移除回调方法,也就是说一次注册一次回调;
  • 当需要再次绘制则需要重新调用到ScheduleFrame()方法,该方法的唯一重要参数regenerate_layer_tree决定在帧绘制过程是否需要重新生成layer tree,还是直接复用上一次的layer tree;
  • UI线程的绘制过程,最核心的是执行WidgetsBinding的drawFrame()方法,然后会创建layer tree视图树
  • 再交由GPU Task Runner将layer tree提供的信息转化为平台可执行的GPU指令。

flutter_frame_scheduler_3.png

  1. flutter 系统层会向C++ 到 native 层 ,注册监听 ,请求Vsync 信号:

具体 SchedulerBinding.scheduleFrame()

  /// dart
  /// 调用 C++ 到 Native 层,请求 Vsync 信号
  void scheduleFrame() {
    if (_hasScheduledFrame || !framesEnabled)
      return;
    assert(() {
      if (debugPrintScheduleFrameStacks)
        debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.');
      return true;
    }());
    ensureFrameCallbacksRegistered();
    platformDispatcher.scheduleFrame();
    _hasScheduledFrame = true;
  }

关键在 platformDispatcher.scheduleFrame() ,这是一个 native 方法

/// Requests that, at the next appropriate opportunity, the [onBeginFrame] and
  /// [onDrawFrame] callbacks be invoked.
  ///
  /// See also:
  ///
  ///  * [SchedulerBinding], the Flutter framework class which manages the
  ///    scheduling of frames.
  void scheduleFrame() native 'PlatformConfiguration_scheduleFrame';

这个方法会调到原生的JNI方法中,在flutter项目启动时会开始注册, 目的是监听系统垂直同步信号Vsync ,在下一个Vsync信号来临时,会再回调到flutter 中,开始执行具体绘制的方法

最终会通过原生监听Vsync 方法,调用到dart 中

  /// Ensures callbacks for [PlatformDispatcher.onBeginFrame] and
  /// [PlatformDispatcher.onDrawFrame] are registered.
  @protected
  void ensureFrameCallbacksRegistered() {
    platformDispatcher.onBeginFrame ??= _handleBeginFrame;
    platformDispatcher.onDrawFrame ??= _handleDrawFrame;
  }

这两个方法将被依次调用

handleBeginFrame 方法中会做哪些事情呢

  1. 开始绘制flutter 当前时间的动画帧 ,
  2. 然后处理MicroTask队列中的消息

(注: MicroTaskEventQueue 是flutter 中的两个消息队列,由同一个Looper 消费,类似于Android的Handler , 我们平时用的 延时Future 和 await 都是往EventQueue中发送消息 ,scheduleMicrotask 则是往MicroTask里发送消息,MicroTask 的执行优先级是高于EventQueue 的,也就是 Main -> MicroTask -> EventQueue)

handleDrawFrame 方法中会做哪些事呢:

开始处理一帧绘制的准备工作 ,会执行一个需要绘制的脏集合,里面放置了当前帧需要绘制的element 树和RenderObject 树, 也就是 build、layout、paint 方法

(注 : 这个集合在页面刚运行的时候 ,创建Element 时,会加入进去一次 ,后续setState() 方法中也会将对应的Element 加入该集合)

Build 方法 :setState() 将 element 加入了脏集合么?这个阶段,Flutter 会通过 widget 更新所有脏集合中的节点(需要更新)中的 element 与 RenderObject 树

Layout 方法:RenderObject 树进行布局测量,用于确定每一个展示元素的大小和位置。

Paint 方法:Paint 阶段会触发 RenderObject 对象绘制,生成第四棵树:Layer Tree,最终合成光栅化后完成渲染。

flutter 解决卡顿的初步优化方式

在flutter 执行layout 去重新测量所有widget 大小时 ,会最终执行到markNeedsLayout() 是递归从下到上的 ,也是深度优先算法,会优先测量最深层的,再测量父级,

  void markNeedsLayout() {
    assert(_debugCanPerformMutations);
    if (_needsLayout) {
      assert(_debugSubtreeRelayoutRootAlreadyMarkedNeedsLayout());
      return;
    }
    if (_relayoutBoundary == null) {
      _needsLayout = true;
      if (parent != null) {
        // _relayoutBoundary is cleaned by an ancestor in RenderObject.layout.
        // Conservatively mark everything dirty until it reaches the closest
        // known relayout boundary.
        markParentNeedsLayout();
      }
      return;
    }
    if (_relayoutBoundary != this) {
      markParentNeedsLayout();
    } else {
      _needsLayout = true;
      if (owner != null) {
        assert(() {
          if (debugPrintMarkNeedsLayoutStacks)
            debugPrintStack(label: 'markNeedsLayout() called for $this');
          return true;
        }());
        owner!._nodesNeedingLayout.add(this);
        owner!.requestVisualUpdate();
      }
    }
  }

_relayoutBoundary 参数 (边界布局) : 如果当前widget 为 relayoutBoundary ,如果需要更新widget ,则只需要更新当前widget , 不会更改父widget

用于性能优化 如果只是当前widget 内部布局大小变化,不会影响父布局的状态,完全可以用此属性,避免父布局被重新测量,浪费资源

属性 :

  1. relayoutBoundary 属性的子控件发生大小变化,不会影响父布局测量
  2. relayoutBoundary 属性的父布局发生大小变化,relayoutBoundary 属性的子控件会重新测量
  3. relayoutBoundary 的兄弟控件 (非relayoutBoundary )发生大小改变 ,父布局大小会改变,兄弟控件会重新测量 ,relayoutBoundary属性的控件不会测量

(这个一般flutter 给我们很多控件已经封装好了 ,我们自己极少用到 )

flutter 执行layout 方法时: 会最终执行到markNeedsPaint() (这个推荐使用)

  void markNeedsPaint() {
    assert(!_debugDisposed);
    assert(owner == null || !owner!.debugDoingPaint);
    if (_needsPaint)
      return;
    _needsPaint = true;
    if (isRepaintBoundary) {
      assert(() {
        if (debugPrintMarkNeedsPaintStacks)
          debugPrintStack(label: 'markNeedsPaint() called for $this');
        return true;
      }());
      // If we always have our own layer, then we can just repaint
      // ourselves without involving any other nodes.
      assert(_layerHandle.layer is OffsetLayer);
      if (owner != null) {
        owner!._nodesNeedingPaint.add(this);
        owner!.requestVisualUpdate();
      }
    } else if (parent is RenderObject) {
      final RenderObject parent = this.parent! as RenderObject;
      parent.markNeedsPaint();
      assert(parent == this.parent);
    } else {
      assert(() {
        if (debugPrintMarkNeedsPaintStacks)
          debugPrintStack(label: 'markNeedsPaint() called for $this (root of render tree)');
        return true;
      }());
      // If we're the root of the render tree (probably a RenderView),
      // then we have to paint ourselves, since nobody else can paint
      // us. We don't add ourselves to _nodesNeedingPaint in this
      // case, because the root is always told to paint regardless.
      if (owner != null)
        owner!.requestVisualUpdate();
    }
  }

isRepaintBoundary 参数 (边界绘制):如果当前widget 为 isRepaintBoundary ,如果需要绘制widget ,则只需要绘制当前widget , 不会绘制父widget

用于性能优化 如果只是当前widget 内部布局大小变化,不会影响父布局的状态,完全可以用此属性,避免父布局被重新测量,浪费资源