关于前端:技术干货-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();
}

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)实现动态化机制,目前没有比拟好的开源技术能够去借鉴

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理