Flutter入口runApp源码分析

Posted Beason_H

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter入口runApp源码分析相关的知识,希望对你有一定的参考价值。

​ Flutter APP跟其他应用程序一样,入口也是在main方法,其内部就是一行代码,调用runAppp方法,将我们定义的根widget添加到界面上。作为Flutter的入口函数,我们有必要了解其背后的工作原理,runApp都做了些啥,能让我们的widget显示在界面上,同事支持各种事件操作,界面刷新等。

首先我们来看看runApp源码:

// App是一个widget,是Flutter应用启动以后要展示的第一个组件
void runApp(Widget app) {
	// 1. 确保WidgetsFlutterBinding被初始化。
  WidgetsFlutterBinding.ensureInitialized()
    // 2. 将传递过来的根widget app attach到某个地方
    ..scheduleAttachRootWidget(app)
    // 3. 调度一个‘热身’帧
    ..scheduleWarmUpFrame();
}

接下来我们就继续对着runApp内三行代码进行逐一突破:

1、WidgetsFlutterBinding初始化

直接看ensureInitialized()源码:

// WidgetsFlutterBinding可以理解为是widget框架和Flutter引擎的桥梁
class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      WidgetsFlutterBinding();
    return WidgetsBinding.instance!;
  }
}

WidgetsFlutterBinding类继承自BindingBase并且混入[mixin]了很多其他Binding类,看名称都是绑定各种不同的功能;

BindingBase,上面的各个mixin Binding类都是继承自它,各个mixin类都重写了initInstances()方法,并且调用了super.initInstances(),所以他们所有的initInstans()方法都会被串行顺序执行。如果对mixin机制不是很理解可以先看看”小白都能看懂的关于Mixins机制的理解“。最终FlutterWidgetBinding()初始化的逻辑为:

WidgetsFlutterBinding经过mixin依赖,实现了所有的Binding类的功能,下面逐一大概介绍一下每个Binding的作用:

  1. GestureBinding:提供了window.onPointerDataPacket的回调,绑定Fragmework子系统,是Framework事件模型与底层事件的绑定入口。
  2. ServicesBinding:提供了window.onPlatformMessage回调,用于绑定平台消息通道(messagechannel),主要处理原生和Flutter之间的通信。
  3. SchedulerBinding:提供了window.onBeginFrame和window.onDrawFrame回调,监听刷新事件,绑定Framework绘制调度子系统
  4. PaintingBinding:绑定绘制库,主要用于处理图片缓存。
  5. SematicsBinding:语义化层与Flutter engine的桥梁,主要是辅助功能的底层支持。
  6. RenderBinding:提供了window.onMetricsChange、window.onTextScaleFactorChanged等回调。它是渲染树与Flutter engine的桥梁。
  7. WidgetsBinding:提供了window.onLocaleChanged,onBuildScheduled等回调。它是Flutter widget层与engine的桥梁。

很明显,可以看到Window类提供了各种平台的回调方法,正是我们Flutter Framework连接宿主操作系统的接口。我们来大致看下源码:

class Window {
    
  // 当前设备的DPI,即一个逻辑像素显示多少物理像素,数字越大,显示效果就越精细保真。
  // DPI是设备屏幕的固件属性,如Nexus 6的屏幕DPI为3.5 
  double get devicePixelRatio => _devicePixelRatio;
  
  // Flutter UI绘制区域的大小
  Size get physicalSize => _physicalSize;

  // 当前系统默认的语言Locale
  Locale get locale;
    
  // 当前系统字体缩放比例。  
  double get textScaleFactor => _textScaleFactor;  
    
  // 当绘制区域大小改变回调
  VoidCallback get onMetricsChanged => _onMetricsChanged;  
  // Locale发生变化回调
  VoidCallback get onLocaleChanged => _onLocaleChanged;
  // 系统字体缩放变化回调
  VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged;
  // 绘制前回调,一般会受显示器的垂直同步信号VSync驱动,当屏幕刷新时就会被调用
  FrameCallback get onBeginFrame => _onBeginFrame;
  // 绘制回调  
  VoidCallback get onDrawFrame => _onDrawFrame;
  // 点击或指针事件回调
  PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket;
  // 调度Frame,该方法执行后,onBeginFrame和onDrawFrame将紧接着会在合适时机被调用,
  // 此方法会直接调用Flutter engine的Window_scheduleFrame方法
  void scheduleFrame() native 'Window_scheduleFrame';
  // 更新应用在GPU上的渲染,此方法会直接调用Flutter engine的Window_render方法
  void render(Scene scene) native 'Window_render';

  // 发送平台消息
  void sendPlatformMessage(String name,
                           ByteData data,
                           PlatformMessageResponseCallback callback) ;
  // 平台通道消息处理回调  
  PlatformMessageCallback get onPlatformMessage => _onPlatformMessage;
  
  ... //其它属性及回调
   
}

Window类包含了当前设备和系统的一些信息以及Flutter Engine的一些回调。通过这些Binding 监听Window对象的一些事件,然后将这些事件按照Framework的模型包装,抽象再分发。

2、scheduleAttachRootWidget

WidgetsFlutterBinding初始化之后,接着会调用WidgetsBinding.attachRootWidget方法,该方法负责将根Widget添加到RenderView上,

  void attachRootWidget(Widget rootWidget) {
    _readyToProduceFrames = true;
    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]',
      child: rootWidget,
    ).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);
  }

注意:

代码中的renderView是一个RenderObject,它渲染树的根

renderViewElement是renderView对应的Element对象,可见该方法主要完成根widget到根RenderObject再到跟Element的整个关联过程。

再来看看attachToRenderTree源码实现:

 /// Inflate this widget and actually set the resulting [RenderObject] as the
  /// child of [container].
  ///
  /// If `element` is null, this function will create a new element. Otherwise,
  /// the given element will have an update scheduled to switch to this widget.
  ///
  /// Used by [runApp] to bootstrap applications.
  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);
      });
      // This is most likely the first time the framework is ready to produce
      // a frame. Ensure that we are asked for one.
      SchedulerBinding.instance!.ensureVisualUpdate();
    } else {
      element._newWidget = this;
      element.markNeedsBuild();
    }
    return element!;
  }

该方法负责创建根element,即:RenderObjectToWidgetElement,并且将element于widget进行关联,即创建出widget数对对应的element树。如果element已经创建过了,则将根element中关联的widget设为新的,由此可以看出element只会创建一次,后面会进行复用。那么BuildOwner是什么呢?其实它就是widget fragment的管理类,它跟踪哪些widget需要重新构建。

3、热身帧绘制

​ 组件数在构建(build)完成以后,回到runApp实现中,当attachRootWidget后,最后一行调用WidgetsFlutterBinding实例的scheduleWarmUpFrame()方法,该方法在实例SchedulerBinding中,它被调用后会立即进行一次绘制,在此次绘制结束之前,该方法会锁定事件分发,也就是说在本次绘制结束完成之前Flutter将不会响应各个事件,这可以保证在绘制过程中不会被再出发新的绘制。

scheduleWarmUpFrame()源码

void scheduleWarmUpFrame() {
    if (_warmUpFrame || schedulerPhase != SchedulerPhase.idle)
      return;

    _warmUpFrame = true;
    Timeline.startSync('Warm-up frame');
    final bool hadScheduledFrame = _hasScheduledFrame;
    // We use timers here to ensure that microtasks flush in between.
    Timer.run(() {
      assert(_warmUpFrame);
      handleBeginFrame(null);
    });
    Timer.run(() {
      assert(_warmUpFrame);
      handleDrawFrame();
      // We call resetEpoch after this frame so that, in the hot reload case,
      // the very next frame pretends to have occurred immediately after this
      // warm-up frame. The warm-up frame's timestamp will typically be far in
      // the past (the time of the last real frame), so if we didn't reset the
      // epoch we would see a sudden jump from the old time in the warm-up frame
      // to the new time in the "real" frame. The biggest problem with this is
      // that implicit animations end up being triggered at the old time and
      // then skipping every frame and finishing in the new time.
      resetEpoch();
      _warmUpFrame = false;
      if (hadScheduledFrame)
        scheduleFrame();
    });

    // Lock events so touch events etc don't insert themselves until the
    // scheduled frame has finished.
    lockEvents(() async {
      await endOfFrame;
      Timeline.finishSync();
    });
  }

这个函数其实就调用了两个函数,onBeginFrameonDrawFrame,最后渲染出来的首帧场景送入engine显示到屏幕。这里使用 Timer.run()来异步运行两个回调,就是为了在他们被调用之前有机会处理完微任务队列(microtaskqueue)。

我们之前说渲染流水线是由Vsync信号驱动的,但是上述过程都是在runApp()里完成的。并没有看到什么地方告诉engine去调度一帧。这是因为我们是在做Flutter的初始化。为了节省等待Vsync信号的时间,所以就直接把渲染流程跑完做出来第一帧图像来了。

总结

Flutter 入口runApp分析完。我们了解到其实主要Flutter 框架的初始化过程核心作用主要是:

  1. 各种mixin 类 Binding的创建,建立与Flutter Engine的桥梁
  2. Element ,Render根节点创立以及Element,Render,Widget之间的关联
  3. 初始帧的绘制

以上是关于Flutter入口runApp源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Flutter 的 runApp 与三棵树诞生流程源码分析

Flutter 的 runApp 与三棵树诞生流程源码分析

Flutter 的 runApp 与三棵树诞生流程源码分析

Flutter学习----初识 Flutter

Flutter 的 runApp 与三棵树诞生流程源码分析

Flutter状态管理——InheritedWidget数据共享的原理分析