Flutter 1.17 中的导航解密和性能提高

Flutter 1.17 对比上一个稳定版本,更可能是带来了性能上的提高,其中一个关键的优化点就是 Navigator 的内部逻辑,本篇将带你解密 Navigator 从 1.12 到 1.17 的变化,并介绍 Flutter 1.17 上究竟优化了哪些性能。html

1、Navigator 优化了什么?

在 1.17 版本最让人感兴趣的变更莫过于:“打开新的不透明页面以后,路由里的旧页面不会再触发 buildgit

虽然以前介绍过 build 方法自己很轻,可是在“不须要”的时候“不执行”明显更符合咱们的预期,而这个优化的 PR 主要体如今 stack.dartoverlay.dart 两个文件上。github

  • stack.dart 文件的修改,只是为了将 RenderStack 的相关逻辑变为共享的静态方法 getIntrinsicDimensionlayoutPositionedChild ,其实就是共享 Stack 的部分布局能力给 Overlay编程

  • overlay.dart 文件的修改则是此次的灵魂所在。api

2、Navigator 的 Overlay

事实上咱们经常使用的 Navigator 是一个 StatefulWidget, 而经常使用的 poppush 等方法对应的逻辑都是在 NavigatorState 中,而 NavigatorState 主要是经过 Overlay 来承载路由页面,因此导航页面间的管理逻辑主要在于 Overlay数组

2.一、Overlay 是什么?

Overlay 你们可能用过,在 Flutter 中能够经过 Overlay 来向 MaterialApp 添加全局悬浮控件,这是由于Overlay 是一个相似 Stack 层级控件,可是它能够经过 OverlayEntry 来独立地管理内部控件的展现。bash

好比能够经过 overlayState.insert 插入一个 OverlayEntry 来实现插入一个图层,而OverlayEntrybuilder 方法会在展现时被调用,从而出现须要的布局效果。ide

var overlayState = Overlay.of(context);
    var _overlayEntry = new OverlayEntry(builder: (context) {
      return new Material(
        color: Colors.transparent,
        child: Container(
          child: Text(
            "${widget.platform} ${widget.deviceInfo} ${widget.language} ${widget.version}",
            style: TextStyle(color: Colors.white, fontSize: 10),
          ),
        ),
      );
    });
    overlayState.insert(_overlayEntry);
复制代码

2.二、Overlay 如何实现导航?

Navigator 中其实也是使用了 Overlay 实现页面管理,每一个打开的 Route 默认状况下是向 Overlay 插入了两个 OverlayEntry布局

为何是两个后面会介绍。性能

而在 Overlay 中, List<OverlayEntry> _entries 的展现逻辑又是经过 _Theatre 来完成的,在 _Theatre 中有 onstageoffstage 两个参数,其中:

  • onstage 是一个 Stack,用于展现 onstageChildren.reversed.toList(growable: false) ,也就是能够被看到的部分;
  • offstage 是展现 offstageChildren 列表,也就是不能够被看到的部分;
return _Theatre(
      onstage: Stack(
        fit: StackFit.expand,
        children: onstageChildren.reversed.toList(growable: false),
      ),
      offstage: offstageChildren,
    );
复制代码

简单些说,好比此时有 [A、B、C] 三个页面,那么:

  • C 应该是在 onstage
  • A、B 应该是处于 offstage

固然,A、B、C 都是以 OverlayEntry 的方式被插入到 Overlay 中,而 A 、B、C 页面被插入的时候默认都是两个 OverlayEntry ,也就是 [A、B、C] 应该有 6 个 OverlayEntry

举个例子,程序在默认启动以后,首先看到的就是 A 页面,这时候能够看到 Overlay

  • _entries 长度是 2,即 Overlay 中的列表总长度为2;
  • onstageChildren 长度是 2,即当前可见的 OverlayEntry 是2;
  • offstageChildren 长度是 0,即没有不可见的 OverlayEntry

这时候咱们打开 B 页面,能够看到 Overlay 中:

  • _entries 长度是 4,也就是 Overlay 中多插入了两个 OverlayEntry
  • onstageChildren 长度是 4,就是当前可见的 OverlayEntry 是 4 个;
  • offstageChildren 长度是 0,就是当前尚未不可见的 OverlayEntry

其实这时候 Overlay 处于页面打开中的状态,也就是 A 页面还能够被看到,B 页面正在动画打开的过程。

接着能够看到 Overlay 中的 build 又再次被执行:

  • _entries 长度仍是 4;
  • onstageChildren 长度变为 2,即当前可见的 OverlayEntry 变成了 2 个;
  • offstageChildren 长度是 1,即当前有了一个不可见 OverlayEntry

这时候 B 页面其实已经打开完毕,因此 onstageChildren 恢复为 2 的长度,也就是 B 页面对应的那两个 OverlayEntry;而 A 页面不可见,因此 A 页面被放置到了 offstageChildren

为何只把 A 的一个 OverlayEntry 放到 offstageChildren?这个后面会讲到。

接着以下图所示,再打开 C 页面时,能够看到一样经历了这个过程:

  • _entries 长度变为 6;
  • onstageChildren 长度先是 4 ,以后又变成 2 ,由于打开时有B 和 C 两个页面参与,而打开完成后只剩下一个 C 页面;
  • offstageChildren 长度是 1,以后又变为 2,由于最开始只有 A 不可见,而最后 A 和 B 都不可见;

因此能够看到,每次打开一个页面:

  • 先会向 _entries 插入两个 OverlayEntry
  • 以后会先经历 onstageChildren 长度是 4 的页面打开过程状态;
  • 最后变为 onstageChildren 长度是 2 的页面打开完成状态,而底部的页面因为不可见因此被加入到 offstageChildren 中;

2.三、Overlay 和 Route

为何每次向 _entries 插入的是两个 OverlayEntry

这就和 Route 有关,好比默认 Navigator 打开新的页面须要使用 MaterialPageRoute ,而生成 OverlayEntry 就是在它的基类之一的 ModalRoute 完成。

ModalRoutecreateOverlayEntries 方法中,经过 _buildModalBarrier_buildModalScope 建立了两个 OverlayEntry ,其中:

  • _buildModalBarrier 建立的通常是蒙层;
  • _buildModalScope 建立的 OverlayEntry 是页面的载体;

因此默认打开一个页面,是会存在两个 OverlayEntry ,一个是蒙层一个是页面

@override
  Iterable<OverlayEntry> createOverlayEntries() sync* {
    yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
    yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
  }
复制代码

那么一个页面有两个 OverlayEntry ,可是为何插入到 offstageChildren 中的数量每次都是加 1 而不是加 2?

若是单从逻辑上讲,按照前面 [A、B、C] 三个页面的例子,_entries 里有 6 个 OverlayEntry, 可是 B、C 页面都不可见了,把 B、C 页面的蒙层也捎带上不就纯属浪费了?

如从代码层面解释,在 _entries 在倒序 for 循环的时候:

  • 在遇到 entry.opaqueture 时,后续的 OverlayEntry 就进不去 onstageChildren 中;
  • offstageChildren 中只有 entry.maintainStatetrue 才会被添加到队列;
@override
  Widget build(BuildContext context) {
    final List<Widget> onstageChildren = <Widget>[];
    final List<Widget> offstageChildren = <Widget>[];
    bool onstage = true;
    for (int i = _entries.length - 1; i >= 0; i -= 1) {
      final OverlayEntry entry = _entries[i];
      if (onstage) {
        onstageChildren.add(_OverlayEntry(entry));
        if (entry.opaque)
          onstage = false;
      } else if (entry.maintainState) {
        offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry)));
      }
    }
    return _Theatre(
      onstage: Stack(
        fit: StackFit.expand,
        children: onstageChildren.reversed.toList(growable: false),
      ),
      offstage: offstageChildren,
    ); 
  }
复制代码

而在 OverlayEntry 中:

  • opaque 表示了 OverlayEntry 是否是“阻塞”了整个 Overlay,也就是不透明的彻底覆盖。
  • maintainState 表示这个 OverlayEntry 必须被添加到 _Theatre 中。

因此能够看到,当页面彻底打开以后,在最前面的两个 OverlayEntry

  • 蒙层 OverlayEntryopaque 会被设置为 true,这样后面的 OverlayEntry 就不会进入到 onstageChildren,也就是不显示;
  • 页面 OverlayEntrymaintainState 会是 true ,这样不可见的时候也会进入到 offstageChildren 里;

那么 opaque 是在哪里被设置的?

关于 opaque 的设置过程以下所示,在 MaterialPageRoute 的另外一个基类 TransitionRoute 中,能够看到一开始蒙层的 opaque 会被设置为 false ,以后在 completed 会被设置为 opaque ,而 opaque 参数在 PageRoute 里就是 @override bool get opaque => true;

PopupRouteopaque 就是 false ,由于 PopupRoute 通常是有透明的背景,须要和上一个页面一块儿混合展现。

void _handleStatusChanged(AnimationStatus status) {
    switch (status) {
      case AnimationStatus.completed:
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = opaque;
        break;
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = false;
        break;
      case AnimationStatus.dismissed:
        if (!isActive) {
          navigator.finalizeRoute(this);
          assert(overlayEntries.isEmpty);
        }
        break;
    }
    changedInternalState();
  }
复制代码

到这里咱们就理清了页面打开时 Overlay 的工做逻辑,默认状况下:

  • 每一个页面打开时会插入两个 OverlayEntryOverlay
  • 打开过程当中 onstageChildren 是 4 个,由于此时两个页面在混合显示;
  • 打开完成后 onstageChildren 是 2,由于蒙层的 opaque 被设置为 ture ,后面的页面再也不是可见;
  • 具有 maintainStatetrueOverlayEntry 在不可见后会进入到 offstageChildren

额外介绍下,路由被插入的位置会和 route.install 时传入的 OverlayEntry 有关,好比: push 传入的是 _history(页面路由堆栈)的 last 。

3、新版 1.17 中 Overlay

那为何在 1.17 以前,打开新的页面时旧的页面会被执行 build 这里面其实主要有两个点:

  • OverlayEntry 都有一个 GlobalKey<_OverlayEntryState> 用户表示页面的惟一;
  • OverlayEntry_Theatre 中会有从 onstageoffstage 的过程;

3.一、为何会 rebuild

由于 OverlayEntryOverlay 内部是会被转化为 _OverlayEntry 进行工做,而 OverlayEntry 里面的 GlobalKey 天然也就用在了 _OverlayEntry 上,而当 Widget 使用了 GlobalKey,那么其对应的 Element 就会是 "Global" 的。

Element 执行 inflateWidget 方法时,会判断若是 Key 值是 GlobalKey,就会调用 _retakeInactiveElement 方法返回“已存在”的 Element 对象,从而让 Element 被“复用”到其它位置,而这个过程 Element 会从本来的 parent 那里被移除,而后添加到新的 parent 上。

这个过程就会触发 Elementupdate ,而 _OverlayEntry 自己是一个 StatefulWidget ,因此对应的 StatefulElementupdate 就会触发 rebuild

3.二、为何 1.17 不会 rebuild

那在 1.17 上,为了避免出现每次打开页面后还 rebuild 旧页面的状况,这里取消了 _Theatreonstageoffstage ,替换为 skipCountchildren 参数。

而且 _TheatreRenderObjectWidget 变为了 MultiChildRenderObjectWidget,而后在 _RenderTheatre 中复用了 RenderStack 共享的布局能力。

@override
  Widget build(BuildContext context) {
    // This list is filled backwards and then reversed below before
    // it is added to the tree.
    final List<Widget> children = <Widget>[];
    bool onstage = true;
    int onstageCount = 0;
    for (int i = _entries.length - 1; i >= 0; i -= 1) {
      final OverlayEntry entry = _entries[i];
      if (onstage) {
        onstageCount += 1;
        children.add(_OverlayEntryWidget(
          key: entry._key,
          entry: entry,
        ));
        if (entry.opaque)
          onstage = false;
      } else if (entry.maintainState) {
        children.add(_OverlayEntryWidget(
          key: entry._key,
          entry: entry,
          tickerEnabled: false,
        ));
      }
    }
    return _Theatre(
      skipCount: children.length - onstageCount,
      children: children.reversed.toList(growable: false),
    );
  }
复制代码

这时候等于 Overlay 中全部的 _entries 都处理到一个 MultiChildRenderObjectWidget 中,也就是同在一个 Element 中,而不是以前控件须要在 onstageStackoffstage 列表下来回切换。

在新的 _Theatre 将两个数组合并成一个 children 数组,而后将 onstageCount 以外的部分设置为 skipCount ,在布局时获取 _firstOnstageChild 进行布局,而当 children 发生改变时,触发的是 MultiChildRenderObjectElementinsertChildRenderObject ,而不会去“干扰”到以前的页面,因此不会产生上一个页面的 rebuild

RenderBox get _firstOnstageChild {
    if (skipCount == super.childCount) {
      return null;
    }
    RenderBox child = super.firstChild;
    for (int toSkip = skipCount; toSkip > 0; toSkip--) {
      final StackParentData childParentData = child.parentData as StackParentData;
      child = childParentData.nextSibling;
      assert(child != null);
    }
    return child;
  }

  RenderBox get _lastOnstageChild => skipCount == super.childCount ? null : lastChild;
复制代码

最后以下图所示,在打开页面后,children 会经历从 4 到 3 的变化,而 onstageCount 也会从 4 变为 2,也印证了页面打开过程和关闭以后的逻辑其实并没发生本质的变化。

从结果上看,这个改动确实对性能产生了不错的提高。固然,这个改进主要是在不透明的页面之间生效,若是是透明的页面效果好比 PopModal 之类的,那仍是须要 rebuild 一下。

4、其余优化

Metal 是 iOS 上相似于 OpenGL ES 的底层图形编程接口,能够在 iOS 设备上经过 api 直接操做 GPU 。

而 1.17 开始,Flutter 在 iOS 上对于支持 Metal 的设备将使用 Metal 进行渲染,因此官方提供的数据上看,这样能够提升 50% 的性能。更多可见:github.com/flutter/flu…

Android 上也因为 Dart VM 的优化,体积能够降低大约 18.5% 的大小。

1.17对于加载大量图片的处理进行了优化,在快速滑动的过程当中能够获得更好的性能提高(经过延时清理 IO Thread 的 Context),这样理论上能够在本来基础上节省出 70% 的内存。

好了,这一期想聊的聊完了,最后容我“厚颜无耻”地推广下鄙人最近刚刚上架的新书 《Flutter 开发实战详解》,感兴趣的小伙伴能够经过如下地址了解:

相关文章
相关标签/搜索