写点什么

技术干货 | Flutter 在线编程实践总结

  • 2021 年 11 月 11 日
  • 本文字数:8608 字

    阅读完需:约 28 分钟

技术干货 | Flutter在线编程实践总结

1.Flutter 架构

Flutter 的架构主要分成三层:Framework,Engine,Embedder。


1.Framework 使用 dart 实现,包括 Material Design 风格的 Widget,Cupertino(针对 iOS)风格的 Widgets,文本/图片/按钮等基础 Widgets,渲染,动画,手势等。 此部分的核心代码是:flutter 仓库下的 flutter package,以及 sky_engine 仓库下的 io,async,ui(dart:ui 库提供了 Flutter 框架和引擎之间的接口)等 package。


2.Engine 使用 C++实现,主要包括:Skia,Dart 和 Text。Skia 是开源的二维图形库,提供了适用于多种软硬件平台的通用 API。


3.Embedder 是一个嵌入层,即把 Flutter 嵌入到各个平台上去,这里做的主要工作包括渲染 Surface 设置,线程设置,以及插件等。 从这里可以看出,Flutter 的平台相关层很低,平台(如 iOS)只是提供一个画布,剩余的所有渲染相关的逻辑都在 Flutter 内部,这就使得它具有了很好的跨端一致性。


2.Flutter 视图绘制

对于开发者来说,使用最多的还是 framework,我就从 Flutter 的入口函数开始一步步往下走,分析一下 Flutter 视图绘制的原理。


在 Flutter 应用中,main()函数最简单的实现如下


// 参数app是一个widget,是Flutter应用启动后要展示的第一个Widget。void runApp(Widget app) {  WidgetsFlutterBinding.ensureInitialized()    ..scheduleAttachRootWidget(app)    ..scheduleWarmUpFrame();}
复制代码

2.1 WidgetsFlutterBinding

WidgetsFlutterBinding 继承自 BindingBase 并混入了很多 Binding,查看这些 Binding 的源码可以发现这些 Binding 中基本都是监听并处理 Window 对象(包含了当前设备和系统的一些信息以及 Flutter Engine 的一些回调)的一些事件,然后将这些事件按照 Framework 的模型包装、抽象然后分发。


WidgetsFlutterBinding 正是粘连 Flutter engine 与上层 Framework 的“胶水”。


  1. GestureBinding:提供了 window.onPointerDataPacket 回调,绑定 Framework 手势子系统,是 Framework 事件模型与底层事件的绑定入口。

  2. ServicesBinding:提供了 window.onPlatformMessage 回调, 用于绑定平台消息通道(message channel),主要处理原生和 Flutter 通信。

  3. SchedulerBinding:提供了 window.onBeginFrame 和 window.onDrawFrame 回调,监听刷新事件,绑定 Framework 绘制调度子系统。

  4. PaintingBinding:绑定绘制库,主要用于处理图片缓存。

  5. SemanticsBinding:语义化层与 Flutter engine 的桥梁,主要是辅助功能的底层支持。

  6. RendererBinding: 提供了 window.onMetricsChanged 、window.onTextScaleFactorChanged 等回调。它是渲染树与 Flutter engine 的桥梁。

  7. WidgetsBinding:提供了 window.onLocaleChanged、onBuildScheduled 等回调。它是 Flutter widget 层与 engine 的桥梁。


WidgetsFlutterBinding.ensureInitialized()负责初始化一个 WidgetsBinding 的全局单例,代码如下


class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {  static WidgetsBinding ensureInitialized() {    if (WidgetsBinding.instance == null)      WidgetsFlutterBinding();    return WidgetsBinding.instance;  }}
复制代码


看到这个 WidgetsFlutterBinding 混入(with)很多的 Binding,下面先看父类 BindingBase:


abstract class BindingBase {   ...  ui.SingletonFlutterWindow get window => ui.window;//获取window实例  @protected  @mustCallSuper  void initInstances() {    assert(!_debugInitialized);    assert(() {      _debugInitialized = true;      return true;    }());  }}
复制代码


看到有句代码 Window get window => ui.window 链接宿主操作系统的接口,也就是 Flutter framework 链接宿主操作系统的接口。系统中有一个 Window 实例,可以从 window 属性来获取,看看源码:


// window的类型是一个FlutterView,FlutterView里面有一个PlatformDispatcher属性ui.SingletonFlutterWindow get window => ui.window;// 初始化时把PlatformDispatcher.instance传入,完成初始化ui.window = SingletonFlutterWindow._(0, PlatformDispatcher.instance);// SingletonFlutterWindow的类结构class SingletonFlutterWindow extends FlutterWindow {  ...  // 实际上是给platformDispatcher.onBeginFrame赋值  FrameCallback? get onBeginFrame => platformDispatcher.onBeginFrame;  set onBeginFrame(FrameCallback? callback) {    platformDispatcher.onBeginFrame = callback;  }    VoidCallback? get onDrawFrame => platformDispatcher.onDrawFrame;  set onDrawFrame(VoidCallback? callback) {    platformDispatcher.onDrawFrame = callback;  }    // window.scheduleFrame实际上是调用platformDispatcher.scheduleFrame()  void scheduleFrame() => platformDispatcher.scheduleFrame();  ...}class FlutterWindow extends FlutterView {  FlutterWindow._(this._windowId, this.platformDispatcher);  final Object _windowId;  // PD  @override  final PlatformDispatcher platformDispatcher;  @override  ViewConfiguration get viewConfiguration {    return platformDispatcher._viewConfigurations[_windowId]!;  }}
复制代码

2.2 scheduleAttachRootWidget

scheduleAttachRootWidget 紧接着会调用 WidgetsBinding 的 attachRootWidget 方法,该方法负责将根 Widget 添加到 RenderView 上,代码如下:


 void attachRootWidget(Widget rootWidget) {    final bool isBootstrapFrame = renderViewElement == null;    _readyToProduceFrames = true;    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(      container: renderView,      debugShortDescription: '[root]',      child: rootWidget,    ).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);    if (isBootstrapFrame) {      SchedulerBinding.instance!.ensureVisualUpdate();    }  }
复制代码


renderView 变量是一个 RenderObject,它是渲染树的根。renderViewElement 变量是 renderView 对应的 Element 对象。可见该方法主要完成了根 widget 到根 RenderObject 再到根 Element 的整个关联过程。


RenderView get renderView => _pipelineOwner.rootNode! as RenderView;
复制代码


renderView 是 RendererBinding 中拿到 PipelineOwner.rootNode,PipelineOwner 在 Rendering Pipeline 中起到重要作用:


随着 UI 的变化而不断收集『 Dirty Render Objects 』随之驱动 Rendering Pipeline 刷新 UI。


简简单讲,PipelineOwner 是『RenderObject Tree』与『RendererBinding』间的桥梁。


最终调用 attachRootWidget,执行会调用 RenderObjectToWidgetAdapter 的 attachToRenderTree 方法,该方法负责创建根 element,即 RenderObjectToWidgetElement,并且将 element 与 widget 进行关联,即创建出 widget 树对应的 element 树。如果 element 已经创建过了,则将根 element 中关联的 widget 设为新的,由此可以看出 element 只会创建一次,后面会进行复用。BuildOwner 是 widget framework 的管理类,它跟踪哪些 widget 需要重新构建。代码如下


RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {  if (element == null) {    owner.lockState(() {      element = createElement();      assert(element != null);      element.assignOwner(owner);    });    owner.buildScope(element, () {      element.mount(null, null);    });  } else {    element._newWidget = this;    element.markNeedsBuild();  }  return element;}
复制代码

2.3 scheduleWarmUpFrame

runApp 的实现中,当调用完 attachRootWidget 后,最后一行会调用 WidgetsFlutterBinding 实例的 scheduleWarmUpFrame() 方法,该方法的实现在 SchedulerBinding 中,它被调用后会立即进行一次绘制(而不是等待"vsync" 信号),在此次绘制结束前,该方法会锁定事件分发,也就是说在本次绘制结束完成之前 Flutter 将不会响应各种事件,这可以保证在绘制过程中不会再触发新的重绘。


下面是 scheduleWarmUpFrame() 方法的部分实现(省略了无关代码):


void scheduleWarmUpFrame() {...Timer.run(() {handleBeginFrame(null);});Timer.run(() {handleDrawFrame();


resetEpoch();});// 锁定事件 lockEvents(() async {await endOfFrame;Timeline.finishSync();});...}该方法中主要调用了 handleBeginFrame() 和 handleDrawFrame() 两个方法


查看 handleBeginFrame() 和 handleDrawFrame() 两个方法的源码,可以发现前者主要是执行了 transientCallbacks 队列,而后者执行了 persistentCallbacks 和 postFrameCallbacks 队列。


1. transientCallbacks:用于存放一些临时回调,一般存放动画回调。
可以通过SchedulerBinding.instance.scheduleFrameCallback 添加回调。
2. persistentCallbacks:用于存放一些持久的回调,不能在此类回调中再请求新的绘制帧,持久回调一经注册则不能移除。
SchedulerBinding.instance.addPersitentFrameCallback(),这个回调中处理了布局与绘制工作。
3. postFrameCallbacks:在Frame结束时只会被调用一次,调用后会被系统移除,可由 SchedulerBinding.instance.addPostFrameCallback() 注册。
注意,不要在此类回调中再触发新的Frame,这可以会导致循环
复制代码


真正的渲染和绘制逻辑在 RendererBinding 中实现,查看其源码,发现在其 initInstances()方法中有如下代码:

void initInstances() {  ... // 省略无关代码  addPersistentFrameCallback(_handlePersistentFrameCallback);}void _handlePersistentFrameCallback(Duration timeStamp) {  drawFrame();}void drawFrame() {  assert(renderView != null);  pipelineOwner.flushLayout(); // 布局  pipelineOwner.flushCompositingBits(); //重绘之前的预处理操作,检查RenderObject是否需要重绘  pipelineOwner.flushPaint(); // 重绘  renderView.compositeFrame(); // 将需要绘制的比特数据发给GPU  pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.}
复制代码


需要注意的是:由于 RendererBinding 只是一个 mixin,而 with 它的是 WidgetsBinding,所以需要看看 WidgetsBinding 中是否重写该方法,查看 WidgetsBinding 的 drawFrame()方法源码:


@overridevoid drawFrame() { ...//省略无关代码  try {    if (renderViewElement != null)      buildOwner.buildScope(renderViewElement);     super.drawFrame(); //调用RendererBinding的drawFrame()方法    buildOwner.finalizeTree();  } }
复制代码


在调用 RendererBinding.drawFrame()方法前会调用 buildOwner.buildScope() (非首次绘制),该方法会将被标记为“dirty” 的 element 进行 rebuild()我们再来看 WidgetsBinding,在 initInstances()方法中创建 BuildOwner 对象,然后执行buildOwner!.onBuildScheduled = _handleBuildScheduled;,这里将_handleBuildScheduled 赋值给了 buildOwnder 的 onBuildScheduled 属性。


BuildOwner 对象,它负责跟踪哪些 widgets 需要重新构建,并处理应用于 widgets 树的其他任务,其内部维护了一个_dirtyElements 列表,用以保存被标“脏”的 elements。


每一个 element 被新建时,其 BuildOwner 就被确定了。一个页面只有一个 buildOwner 对象,负责管理该页面所有的 element。


// WidgetsBindingvoid initInstances() {  ...  buildOwner!.onBuildScheduled = _handleBuildScheduled;  ...  }());}
当调用buildOwner.onBuildScheduled()时,便会走下面的流程。
// WidgetsBinding类void _handleBuildScheduled() { ensureVisualUpdate();}// SchedulerBinding类void ensureVisualUpdate() { switch (schedulerPhase) { case SchedulerPhase.idle: case SchedulerPhase.postFrameCallbacks: scheduleFrame(); return; case SchedulerPhase.transientCallbacks: case SchedulerPhase.midFrameMicrotasks: case SchedulerPhase.persistentCallbacks: return; } }
当schedulerPhase处于idle状态,会调用scheduleFrame,然后经过window.scheduleFrame()中的performDispatcher.scheduleFrame()去注册一个VSync监听
void scheduleFrame() { ...
window.scheduleFrame(); ... }
复制代码

2.4 小结

Flutter 从启动到显示图像在屏幕主要经过:首先监听处理 window 对象的事件,将这些事件处理包装为 Framework 模型进行分发,通过 widget 创建 element 树,接着通过 scheduleWarmUpFrame 进行渲染,接着通过 Rendererbinding 进行布局,绘制,最后通过调用 ui.window.render(scene)Scene 信息发给 Flutter engine,Flutter engine 最后调用渲染 API 把图像画在屏幕上。


我大致整理了一下 Flutter 视图绘制的时序图,如下


3.Flutter 性能监控

在对视图绘制有一定的了解后后,思考一个问题,怎么在视图绘制的过程中去把控性能,优化性能,我们先来看一下 Flutter 官方提供给我们的两个性能监控工具

3.1 Dart VM Service

3.1.1 observatory

observatory: 在 engine/shell/testings/observatory 可以找到它的具体实现,它开启了一个 ServiceClient,用于获取 dartvm 运行状态.flutter app 启动的时候会生成一个当前的 observatory 服务器的地址


flutter: socket connected in service Dart VM Service Protocol v3.44 listening on http://127.0.0.1:59378/8x9XRQIBhkU=/
复制代码



比方说选择了 timeline 后,可以进行性能分析,如图


3.1.2 devTools

devTools 也提供了一些基本的检测,具体的细节没有 Observatory 提供的完善. 可视性比较强


可以通过下面命令安装


flutter pub global activate devtools
复制代码


安装完成后通过 devtools 命令打开,输入 DartVM 地址



打开后的页面



devtools 中的 timeline 就是 performance,我们选择之后页面如下,操作体验上好了很多



observatory 与 devtools 都是通过 vm_service 实现的,网上使用指南比较多,这边就不多赘述了,我这边主要介绍一下 Dart VM Service (后面 简称 vm_service)


是 Dart 虚拟机内部提供的一套 Web 服务,数据传输协议是 JSON-RPC 2.0。


不过我们并不需要要自己去实现数据请求解析,官方已经写好了一个可用的 Dart SDK 给我们用:vm_service。 vm_service 在启动的时候会在本地开启一个 WebSocket 服务,服务 URI 可以在对应的平台中获得:


1)Android 在 FlutterJNI.getObservatoryUri() 中;


2)iOS 在 FlutterEngine.observatoryUrl 中。


有了 URI 之后我们就可以使用 vm_service 的服务了,官方有一个帮我们写好的 SDK: vm_service


 Future<void> connect() async {    ServiceProtocolInfo info = await Service.getInfo();    if (info.serverUri == null) {      print("service  protocol url is null,start vm service fail");      return;    }    service = await getService(info);    print('socket connected in service $info');    vm = await service?.getVM();    List<IsolateRef>? isolates = vm?.isolates;    main = isolates?.firstWhere((ref) => ref.name?.contains('main') == true);    main ??= isolates?.first;    connected = true;  }
Future<VmService> getService(info) async { Uri uri = convertToWebSocketUrl(serviceProtocolUrl: info.serverUri); return await vmServiceConnectUri(uri.toString(), log: StdoutLog()); }
复制代码


获取 frameworkVersion,调用一个 VmService 实例的 callExtensionService,传入'flutterVersion',就能拿到当前的 flutter framework 和 engine 信息


 Future<Response?> callExtensionService(String method) async {    if (_extensionService == null && service != null && main != null) {      _extensionService = ExtensionService(service!, main!);      await _extensionService?.loadExtensionService();    }    return _extensionService!.callMethod(method);  }
复制代码



获取内存信息,调用一个 VmService 实例的 getMemoryUsage,就能拿到当前的内存信息


  Future<MemoryUsage> getMemoryUsage(String isolateId) =>      _call('getMemoryUsage', {'isolateId': isolateId});
复制代码



获取 Flutter APP 的 FPS,官方提供了好几个办法来让我们在开发 Flutter app 的过程中可以使用查看 fps 等性能数据,如 devtools,具体见文档 Debugging Flutter apps 、Flutter performance profiling 等。


// 需监听fps时注册void start() {  SchedulerBinding.instance.addTimingsCallback(_onReportTimings);}// 不需监听时移除void stop() {  SchedulerBinding.instance.removeTimingsCallback(_onReportTimings);}void _onReportTimings(List<FrameTiming> timings) {  // TODO}
复制代码

3.2 崩溃日志捕获上报

flutter 的崩溃日志收集主要有两个方面:


1)flutter dart 代码的异常(包含 app 和 framework 代码两种情况,一般不会引起闪退,你猜为什么)


2)flutter engine 的崩溃日志(一般会闪退)


Dart 有一个 Zone 的概念,有点类似sandbox的意思。不同的 Zone 代码上下文是不同的互不影响,Zone 还可以创建新的子 Zone。Zone 可以重新定义自己的printtimersmicrotasks还有最关键的 how uncaught errors are handled 未捕获异常的处理


runZoned(() {    Future.error("asynchronous error");}, onError: (dynamic e, StackTrace stack) {    reportError(e, stack);});
复制代码

3.2.1 Flutter framework 异常捕获

注册 FlutterError.onError 回调,用于收集 Flutter framework 外抛的异常。


FlutterError.onError = (FlutterErrorDetails details) {    reportError(details.exception, details.stack);};
复制代码

3.2.2 Flutter engine 异常捕获

flutter engine 部分的异常,以 Android 为例,主要为 libfutter.so 发生的错误。


这部份可以直接交给 native 崩溃收集 sdk 来处理,比如 firebase crashlytics、 bugly、xCrash 等等


我们需要将 dart 异常及堆栈通过 MethodChannel 传递给 bugly sdk 即可。


收集到异常之后,需要查符号表(symbols)还原堆栈。


首先需要确认该 flutter engine 所属版本号,在命令行执行:


flutter --version 输出如下:


Flutter 2.2.3 • channel stable • https://github.com/flutter/flutter.gitFramework • revision f4abaa0735 (4 months ago) • 2021-07-01 12:46:11 -0700Engine • revision 241c87ad80Tools • Dart 2.13.4
复制代码


可以看到 Engine 的 revision 为 241c87ad80。


其次,在 flutter infra 上找到对应 cpu abi 的 symbols.zip 并下载,解压后,可以得到带有符号信息的 debug so 文件—— libflutter.so,然后按照平台文档上传进行堆栈还原就可以了,如 bugly 平台就提供了上传工具


java -jar buglySymbolAndroid.jar -i xxx

4 Flutter 性能优化

在业务开发中我们要学会用 devtools 来检测工程性能,这样有助于我们实现健壮性更强的应用,在排查过程中,我发现视频详情页存在渲染耗时的问题,如图


4.1 build 耗时优化

VideoControls 控件的 build 耗时是 28.6ms,如图


所以这里我们的优化方案是提高 build 效率,降低 Widget tree 遍历的出发点,将 setState 刷新数据尽量下发到底层节点,所以将 VideoControl 内触发刷新的子组件抽取成独立的 Widget,setState 下发到抽取出的 Widget 内部


优化后为 11.0ms,整体的平均帧率也达到了了 60fps,如图


4.2 paint 耗时优化

接下来分析下 paint 过程有没有可以优化的部分,我们打开 debugProfilePaintsEnabled 变量分析可以看到 Timeline 显示的 paint 层级,如图



我们发现频繁更新的_buildPositionTitle 和其他 Widget 在同一个 layer 中,这里我们想到的优化点是利用 RepaintBoundary 提高 paint 效率,它为经常发生显示变化的内容提供一个新的隔离 layer,新的 layer paint 不会影响到其他 layer


看下优化后的效果,如图

4.3 小结

在 Flutter 开发过程中,我们用 devtools 工具排查定位页面渲染问题时,主要有两点:


1.提高 build 效率,setState 刷新数据尽量下发到底层节点。


2.提高 paint 效率,RepaintBoundry 创建单独 layer 减少重绘区域。


当然 Flutter 中性能调优远不止这一种情况,build / layout / paint 每一个过程其实都有很多能够优化的细节。

5.总结

5.1 回顾

这篇文章主要从三个维度来介绍 Flutter 这门技术,分别为绘制原理讲解,我们 review 了一下源码,发现整个渲染过程就是一个闭环,Framework,Engine,Embedder 各司其职,简单来说就是 Embedder 不断拿回 Vsync 信号,Framework 将 dart 代码交给 Engine 翻译成跨平台代码,再通过 Embedder 回调宿主平台;性能监控就是不断得在这个循环中去插入我们的哨兵,观察整个生态,获取异常数据上报;性能优化通过一次项目实践,学习怎么用工具提升我们定位问题的效率。

5.2 优缺点

优点:

我们可以看到 Flutter 在视图绘制过程中形成了闭环,双端基本保持了一致性,所以我们的开发效率得到了极大的提升,性能监控和性能优化也比较方便。


缺点:

1)声明式开发 动态操作视图节点不是很友好,不能像原生那样命令式编程,或者像前端获取 dom 节点那般容易


2)实现动态化机制,目前没有比较好的开源技术可以去借鉴

发布于: 2021 年 11 月 11 日阅读数: 12
用户头像

高效学习,从有道开始 2021.03.10 加入

分享有道人的技术思考与实践。

评论

发布
暂无评论
技术干货 | Flutter在线编程实践总结