深入学习Flutter的运行机制

关注个人简介,面试不迷路~

main入口启动

  • Flutter的主入口在"lib/main.dart"的main()函数中。在Flutter应用中,main()函数最简单的实现如下:
    void main() {
      runApp(MyApp());
    }
    
  • 可以看到main()函数只调用了一个runApp()方法,runApp()方法中都做了什么:
    void runApp(Widget app) {
      //初始化操作
      WidgetsFlutterBinding.ensureInitialized()
        //页面渲染
        ..attachRootWidget(app)
        ..scheduleWarmUpFrame();
    }
    
*   参数`app`是一个Widget,是Flutter应用启动后要展示的第一个Widget。
*   `WidgetsFlutterBinding`正是绑定widget 框架和Flutter engine的桥梁。
  • ensureInitialized()方法
    class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
      static WidgetsBinding ensureInitialized() {
        if (WidgetsBinding.instance == null)
          WidgetsFlutterBinding();
        return WidgetsBinding.instance;
      }
    }
    
*   可以看到`WidgetsFlutterBinding`继承自`BindingBase` 并混入了很多`Binding`,在介绍`Binding`之前先介绍一下`Window`,`Window`的官方解释:The most basic interface to the host operating system's user interface.
  • Window正是Flutter Framework连接宿主操作系统的接口。看一下Window类的部分定义:
    class Window {
      // 当前设备的DPI,即一个逻辑像素显示多少物理像素,数字越大,显示效果就越精细保真。
      // DPI是设备屏幕的固件属性,如Nexus 6的屏幕DPI为3.5 
      double get devicePixelRatio => _devicePixelRatio;
      // 绘制回调  
      VoidCallback get onDrawFrame => _onDrawFrame;
      // 发送平台消息
      void sendPlatformMessage(String name,
                               ByteData data,
                               PlatformMessageResponseCallback callback) ;
      ... //其它属性及回调
    }
    
*   `Window`类包含了当前设备和系统的一些信息以及Flutter Engine的一些回调。现在回来看看`WidgetsFlutterBinding`混入的各种Binding。通过查看这些 Binding的源码,可以发现这些Binding中基本都是监听并处理`Window`对象的一些事件,然后将这些事件按照Framework的模型包装、抽象然后分发。可以看到`WidgetsFlutterBinding`正是粘连Flutter engine与上层Framework的“胶水”。
  • 看看attachToRenderTree的源码实现:
    RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
      if (element == null) {
        ...// 代码处理
      } else {
        ...// 代码处理
      }
      return element;
    }
    
  • 该方法负责创建根element,即 RenderObjectToWidgetElement,并且将element与widget 进行关联,即创建出 widget树对应的element树。
    • 如果element 已经创建过了,则将根element 中关联的widget 设为新的,由此可以看出element 只会创建一次,后面会进行复用。那么BuildOwner是什么呢?其实他就是widget framework的管理类,它跟踪哪些widget需要重新构建。

页面渲染

  • 回到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()` 两个方法,在看这两个方法之前我们首先了解一下Frame 和 FrameCallback 的概念:
*   Frame: 一次绘制过程,我们称其为一帧。Flutter engine受显示器垂直同步信号"VSync"的驱使不断的触发绘制。我们之前说的Flutter可以实现60fps(Frame Per-Second),就是指一秒钟可以触发60次重绘,FPS值越大,界面就越流畅。
*   FrameCallback:`SchedulerBinding` 类中有三个FrameCallback回调队列, 在一次绘制过程中,这三个回调队列会放在不同时机被执行:
    *   1.  `transientCallbacks`:用于存放一些临时回调,一般存放动画回调。可以通过`SchedulerBinding.instance.scheduleFrameCallback` 添加回调。
    *   2.  `persistentCallbacks`:用于存放一些持久的回调,不能在此类回调中再请求新的绘制帧,持久回调一经注册则不能移除。`SchedulerBinding.instance.addPersitentFrameCallback()`,这个回调中处理了布局与绘制工作。
    *   3.  `postFrameCallbacks`:在Frame结束时只会被调用一次,调用后会被系统移除,可由 `SchedulerBinding.instance.addPostFrameCallback()` 注册,注意,不要在此类回调中再触发新的Frame,这可以会导致循环刷新。
*   自行查看`handleBeginFrame()` 和 `handleDrawFrame()` 两个方法的源码,可以发现前者主要是执行了`transientCallbacks`队列,而后者执行了 `persistentCallbacks` 和 `postFrameCallbacks` 队列。

页面绘制

  • 渲染和绘制逻辑在RendererBinding中实现,查看其源码,发现在其initInstances()方法中有如下代码:
    void initInstances() {
      ... //省略无关代码
          
      //监听Window对象的事件  
      ui.window
        ..onMetricsChanged = handleMetricsChanged
        ..onTextScaleFactorChanged = handleTextScaleFactorChanged
        ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
        ..onSemanticsAction = _handleSemanticsAction;
       
      //添加PersistentFrameCallback    
      addPersistentFrameCallback(_handlePersistentFrameCallback);
    }
    
*   看最后一行,通过`addPersistentFrameCallback` 向`persistentCallbacks`队列添加了一个回调 `_handlePersistentFrameCallback`:

```
void _handlePersistentFrameCallback(Duration timeStamp) {
  drawFrame();
}

```
  • 该方法直接调用了RendererBindingdrawFrame()方法

flushLayout()

  • 代码如下所示
    void flushLayout() {
       ...
        while (_nodesNeedingLayout.isNotEmpty) {
          final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
          _nodesNeedingLayout = <RenderObject>[];
          for (RenderObject node in 
               dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
            if (node._needsLayout && node.owner == this)
              node._layoutWithoutResize();
          }
        }
      } 
    }
    
  • 源码很简单,该方法主要任务是更新了所有被标记为“dirty”的RenderObject的布局信息。主要的动作发生在node._layoutWithoutResize()方法中,该方法中会调用performLayout()进行重新布局。

flushCompositingBits()

  • 代码如下所示
    void flushCompositingBits() {
      _nodesNeedingCompositingBitsUpdate.sort(
          (RenderObject a, RenderObject b) => a.depth - b.depth
      );
      for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
        if (node._needsCompositingBitsUpdate && node.owner == this)
          node._updateCompositingBits(); //更新RenderObject.needsCompositing属性值
      }
      _nodesNeedingCompositingBitsUpdate.clear();
    }
    
  • 检查RenderObject是否需要重绘,然后更新RenderObject.needsCompositing属性,如果该属性值被标记为true则需要重绘。

flushPaint()

  • 代码如下所示
    void flushPaint() {
     ...
      try {
        final List<RenderObject> dirtyNodes = _nodesNeedingPaint; 
        _nodesNeedingPaint = <RenderObject>[];
        // 反向遍历需要重绘的RenderObject
        for (RenderObject node in 
             dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
          if (node._needsPaint && node.owner == this) {
            if (node._layer.attached) {
              // 真正的绘制逻辑  
              PaintingContext.repaintCompositedChild(node);
            } else {
              node._skippedPaintingOnLayer();
            }
          }
        }
      } 
    }
    
*   该方法进行了最终的绘制,可以看出它不是重绘了所有 `RenderObject`,而是只重绘了需要重绘的 `RenderObject`。真正的绘制是通过`PaintingContext.repaintCompositedChild()`来绘制的,该方法最终会调用Flutter engine提供的Canvas API来完成绘制。

compositeFrame()

  • 代码如下所示
    void compositeFrame() {
      ...
      try {
        final ui.SceneBuilder builder = ui.SceneBuilder();
        final ui.Scene scene = layer.buildScene(builder);
        if (automaticSystemUiAdjustment)
          _updateSystemChrome();
        ui.window.render(scene); //调用Flutter engine的渲染API
        scene.dispose(); 
      } finally {
        Timeline.finishSync();
      }
    }
    
  • 这个方法中有一个Scene对象,Scene对象是一个数据结构,保存最终渲染后的像素信息。
    • 这个方法将Canvas画好的Scene传给window.render()方法,该方法会直接将scene信息发送给Flutter engine,最终由engine将图像画在设备屏幕上。

最后

  • 需要注意:由于RendererBinding只是一个mixin,而with它的是WidgetsBinding,所以需要看看WidgetsBinding中是否重写该方法,查看WidgetsBindingdrawFrame()方法源码:
    @override
    void drawFrame() {
     ...//省略无关代码
      try {
        if (renderViewElement != null)
          buildOwner.buildScope(renderViewElement); 
        super.drawFrame(); //调用RendererBinding的drawFrame()方法
        buildOwner.finalizeTree();
      } 
    }
    
  • 发现在调用RendererBinding.drawFrame()方法前会调用 buildOwner.buildScope() (非首次绘制),该方法会将被标记为“dirty” 的 element 进行 rebuild()
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352

推荐阅读更多精彩内容