关于前端:技术干货-Flutter在线编程实践总结

42次阅读

共计 12669 个字符,预计需要花费 32 分钟才能阅读完成。

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();}

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.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;
}
 

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()办法源码:

@override
void 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。

// WidgetsBinding
void 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();
    ...
  }

4. 小结

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

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

3.Flutter 性能监控

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

1.Dart VM Service

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 后,能够进行性能剖析,如图

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}

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);
});
 

1.Flutter framework 异样捕捉

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

FlutterError.onError = (FlutterErrorDetails details) {reportError(details.exception, details.stack);
};

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.git
Framework • revision f4abaa0735 (4 months ago) • 2021-07-01 12:46:11 -0700
Engine • revision 241c87ad80
Tools • 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 来检测工程性能,这样有助于咱们实现健壮性更强的利用,在排查过程中,我发现视频详情页存在渲染耗时的问题,如图

1.build 耗时优化

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


所以这里咱们的优化计划是进步 build 效率,升高 Widget tree 遍历的出发点,将 setState 刷新数据尽量下发到底层节点,所以将 VideoControl 内触发刷新的子组件抽取成独立的 Widget,setState 下发到抽取出的 Widget 外部

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

2.paint 耗时优化

接下来剖析下 paint 过程有没有能够优化的局部,咱们关上 debugProfilePaintsEnabled 变量剖析能够看到 Timeline 显示的 paint 层级,如图

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

看下优化后的成果,如图

3. 小结

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

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

2. 进步 paint 效率,RepaintBoundry 创立独自 layer 缩小重绘区域。

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

5. 总结

1. 回顾

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

2. 优缺点

长处:

咱们能够看到 Flutter 在视图绘制过程中造成了闭环,双端根本放弃了一致性,所以咱们的开发效率失去了极大的晋升,性能监控和性能优化也比拟不便。

毛病:

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

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

正文完
 0