目的:
- 理清整体flutter渲染机制如何运行
- flutter 优化打基础
目录:
flutter 系统结构概览
flutter 渲染的流程 (为何掉帧、为何会产生卡顿)
flutter 解决卡顿的初步优化方式
flutter 系统结构概览
一张图概括 :
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。
- 当需要渲染则会调用到Engine的ScheduleFrame()来注册VSYNC信号回调,一旦触发回调doFrame()执行完成后,便会移除回调方法,也就是说一次注册一次回调;
- 当需要再次绘制则需要重新调用到ScheduleFrame()方法,该方法的唯一重要参数regenerate_layer_tree决定在帧绘制过程是否需要重新生成layer tree,还是直接复用上一次的layer tree;
- UI线程的绘制过程,最核心的是执行WidgetsBinding的drawFrame()方法,然后会创建layer tree视图树
- 再交由GPU Task Runner将layer tree提供的信息转化为平台可执行的GPU指令。
- 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 方法中会做哪些事情呢
- 开始绘制flutter 当前时间的动画帧 ,
- 然后处理MicroTask队列中的消息
(注: MicroTask 和 EventQueue 是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 内部布局大小变化,不会影响父布局的状态,完全可以用此属性,避免父布局被重新测量,浪费资源
属性 :
- relayoutBoundary 属性的子控件发生大小变化,不会影响父布局测量
- relayoutBoundary 属性的父布局发生大小变化,relayoutBoundary 属性的子控件会重新测量
- 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 内部布局大小变化,不会影响父布局的状态,完全可以用此属性,避免父布局被重新测量,浪费资源