只有记在脑海里的才是自己的,如果还没来得及,请写下来
刻意了解 flutter 之 BuildContext
2020-11-17 / 12 min read

在flutter开发时,我们接触最多的应该就是build()方法,而在这个方法里有一个必不可少的参数BuildContext context,那么这个BuildContext到底是什么?为什么要在这里返回?有什么作用?带着这些疑问我们一步一步去了解一下(当然,以下只是个人观看源码的见解)

1:第一步,我们先实现一个build()方法

我们一般通过继承 StatelessWidget(StatefulWidget) 来自定义 Widget ,在 build 方法中来实现我们的 Widget。

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Text('刻意了解BuildContext'),
      ),
    );
  }
}

我们可以知道 build 是在 StatelessWidget 中定义的,StatelessWidget 是一个抽象类,一共就包含以下两个方法。其中一个 Widget build(BuildContext context); 方法就是我们重写的方法。那么这个方法里的 BuildContext context 参数传的具体是什么?我们还不知道

abstract class StatelessWidget extends Widget {
  /// Initializes [key] for subclasses.
  const StatelessWidget({ Key? key }) : super(key: key);

  @override
  StatelessElement createElement() => StatelessElement(this);

  @protected
  Widget build(BuildContext context);
}

在 StatelessWidget 中还有一个方法 createElement(),这个方法已经被实现了:
StatelessElement createElement() => StatelessElement(this);
createElement() 方法传入了 this,这个 this 我们可以清楚的知道就是我们自定义的Widget MyApp 的实列。

2:第二步,StatelessElement需要了解一下

按照第一步,我们发现了 createElement() 方法,这个方法返回一个 StatelessElement ,我们先转移下视线,来看看 StatelessElement 是什么?(网上有一大堆关于Element 和 Widget 的解释,这里就不重复了,我们从源码上来看)

/// An [Element] that uses a [StatelessWidget] as its configuration.
/// 翻译:一个 Element 是使用了 一个 Widget 作为其配置信息
class StatelessElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  /// 翻译:通过一个 StatelessWidget 创建一个 StatelessElement
  StatelessElement(StatelessWidget widget) : super(widget);

  @override
  StatelessWidget get widget => super.widget as StatelessWidget;

  @override
  Widget build() => widget.build(this);

  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true;
    rebuild();
  }
}

StatelessElement 是使用 StatelessWidget 来创建的,那么我们可以知道:我们自定义一个 Widget,最终会创建一个与之对应的 Element 。
在 StatelessElement 中,我们发现了我们需要的方法build()方法:
Widget build() => widget.build(this);

😌😌😌到这里,我们差不多知道我们重写的 Widget build(BuildContext context) 是怎么调用的了,那么我们也就知道 BuildContext 就是 StatelessElement 实列

  1. 关于 BuildContext 和 StatelessElement 的关系,我们可以通过 StatelessElement 的继承来看
    abstract class Element extends DiagnosticableTree implements BuildContext
  2. 关于为什么参数是 BuildContext 类型,而不是 StatelessElement 类型,估计是为了让使用者更加方便,同时也是因为 flutter 的渲染层次问题(这里可以网上了解一下)

现在我们差不多知道如下一个步骤:

目前为止,我们知道了 BuildContext context 就是 Element 的实列,接下来,我们需要知道 BuildContext 到底有什么作用?

3:第三步 BuildContext 有什么用?

我们先去看看 BuildContext 的定义,内容太多,这里只复制一小部分

abstract class BuildContext {
  /// The current configuration of the [Element] that is this [BuildContext].
  Widget get widget;

  /// The [BuildOwner] for this context. The [BuildOwner] is in charge of
  /// managing the rendering pipeline for this context.
  BuildOwner? get owner;
  ...
}

我们可以看到 BuildContext 中有一个get方法:Widget get widget;
我们回到第二步,可以看到 StatelessElement 中对这个 get 方法做了实现,返回的就是我们定义的Widget MyApp 的实列,我们可以做下验证:

通过 (context.widget as MyApp).name 我们可以确定,BuildContext 中的 get方法 返回的确实是MyApp的实列

class MyApp extends StatelessWidget {
  final String name = 'xxxx';
  const MyApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Text(
            (context.widget as MyApp).name
          ),
        ),
      ),
    );
  }
}

现在我们发现了BuildContext的第一个作用

  1. 作用一:BuildContext 保存了对当前 Widget 实列的引用,通过 context.widget 方法可以获取到当前的 Widget 实列

因为 BuildContext context 实质上是一个 Element,那么我们还可以这样,手动标记当前的Widget需要刷新:

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: InkWell(
            child: Text(
              (context.widget as MyApp).name + DateTime.now().toString()
            ),
            onTap: () {
              print(context.owner.toString());
              (context as Element).markNeedsBuild();
            },
          )
        ),
      ),
    );
  }

上面点击后,执行 markNeedsBuild() 标记当前 widget 需要重新 build,可以发现界面更新了。

🤗🤗🤗注意:我们现在知道每一个Widget都对应一个Element(BuildContext);那么我们在使用context时就需要注意当前对应的是哪个Widget,比如 Navigator.of(context) 这里的 context

我们再来看看另一个 get 方法:BuildOwner? get owner;

4:偏离一下方向,记录对 BuildOwner 的理解

BuildOwner 从字面上理解,应该是构建管理者,既然 BuildContext 中包含了对 BuildOwner 的引用,那么 BuildOwner 应该参与了 widget 的构

4.1:我们可以从 markNeedsBuild() 方法入手:
void markNeedsBuild() {
    .....
    if (dirty)
      return;
    _dirty = true;
    owner!.scheduleBuildFor(this);
  }

这里我们可以看到,我们对一个 element 标记需要重新构建,owner(BuildOwner)会调用scheduleBuildFor(this)方法。
另外我们可以看看 setState 方法:

void setState(VoidCallback fn) {
    .....
    _element!.markNeedsBuild();
}

setState() 最后也是调用了markNeedsBuild()
😣😣😣这里需要知道,为什么只有在setState中去修改数据,界面才会改变?因为setState() 最后调用了_element!.markNeedsBuild(); 标记当前的element需要重新执行Build()方法。

4.2:scheduleBuildFor() 将需要重新build 的 element 记录下来

_dirtyElements 列表记录需要执行 build 的 element

void scheduleBuildFor(Element element) {
    ......
    _dirtyElements.add(element);
    element._inDirtyList = true;
    assert(() {
      if (debugPrintScheduleBuildForStacks)
        debugPrint('...dirty list is now: $_dirtyElements');
      return true;
    }());
  }

那么 _dirtyElements 在什么时候被使用,我们可以搜索到另一个方法 buildScope:

void buildScope(Element context, [ VoidCallback? callback ]) {
    ....
        try {
          _dirtyElements[index].rebuild();
        } catch (e, stack) {
            .....
    } finally {
      for (final Element element in _dirtyElements) {
        assert(element._inDirtyList);
        element._inDirtyList = false;
      }
      _dirtyElements.clear();
      _scheduledFlushDirtyElements = false;
      _dirtyElementsNeedsResorting = null;
    ....
  }

buildScope 方法 对标记的 element 进行排序,并调用了rebuild() 方法,最后清空 _dirtyElements 列表

这里 buildScope 方法在什么时候调用,我们可以打断点,可以看到在 void drawFrame() 方法里调用了 buildScope 方法。

drawFrame 方法就是绘制帧,断点往上就是底层库的一些方法调用,涉及太广,暂不深入。

5:第四步,BuildContext 为什么要在这里返回 ?

前面我们有提到 Navigator.of(context) ,这里其实是导航跳转,我们发现这里使用了 context ,那么为什么要使用 context ?

5.1:Navigator.of(context)方法中,context 的作用

我们去看看 Navigator.of(context) 里面的源码:

  static NavigatorState? of(
    BuildContext context, {
    bool rootNavigator = false,
    bool nullOk = false,
  }) {
    // Handles the case where the input context is a navigator element.
    NavigatorState? navigator;
    if (context is StatefulElement && context.state is NavigatorState) {
        navigator = context.state as NavigatorState;
    }
    if (rootNavigator) {
      navigator = context.findRootAncestorStateOfType<NavigatorState>() ?? navigator;
    } else {
      navigator = navigator ?? context.findAncestorStateOfType<NavigatorState>();
    }

    .......
    return navigator;
  }

在源码里面,我们通常会走到 context.findAncestorStateOfType<NavigatorState>(); 这个方法是在 BuildContext 中定义T? findAncestorStateOfType<T extends State>();,在 Element 类里面实现的(前面我们提到abstract class Element extends DiagnosticableTree implements BuildContext {,Element 实现了 BuildContext 接口),我们去看看这个方法做了什么?

  @override
  T? findAncestorStateOfType<T extends State<StatefulWidget>>() {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    Element? ancestor = _parent;
    while (ancestor != null) {
      if (ancestor is StatefulElement && ancestor.state is T)
        break;
      ancestor = ancestor._parent;
    }
    final StatefulElement? statefulAncestor = ancestor as StatefulElement?;
    return statefulAncestor?.state as T?;
  }

这里的泛型 T 指的是 NavigatorState 类型,当前方法自下而上查找一个 NavigatorState。

5.2:NavigatorState 是 Navigator 组件的状态管理类

现在我们知道 Navigator.of(context) 通过当前的 context 自下而上 找到一个 NavigatorState 状态管理类,现在我们去看看这个状态管理在哪里定义的?顺便去了解一下导航跳转?

NavigatorState 是一个状态管理,对应的 StatefulWidget 就是 Navigator。我们去看看 Navigator 在哪里创建的?

  1. 既然 Navigator 不是我们手动去创建的,我们猜想应该在 MaterialApp 中就创建了。
    找到 Navigator 对应的状态管理类 _MaterialAppState
    class _MaterialAppState extends State<MaterialApp>
    接着找到 _MaterialAppState 的 build() 方法,我们发现,build() 方法返回的是WidgetsApp
  2. WidgetsApp 也是 StatefulWidget 类型,对应 _WidgetsAppState 状态管理类,我们在 _WidgetsAppState 中的 build() 方法里找到了关于 Navigator 的创建
routing = Navigator(
    restorationScopeId: 'nav',
    key: _navigator,
    initialRoute: _initialRouteName,
    onGenerateRoute: _onGenerateRoute,
    onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
        ? Navigator.defaultGenerateInitialRoutes
        : (NavigatorState navigator, String initialRouteName) {
        return widget.onGenerateInitialRoutes!(initialRouteName);
        },
    onUnknownRoute: _onUnknownRoute,
    observers: widget.navigatorObservers!,
    reportsRouteUpdateToEngine: true,
    );

到现在为止,我们找到了导航状态管理的类,接下来试着去看看导航的跳转?

  1. 我们写一个导航跳转,如下:
Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(
            '刻意了解'
          ),
        ),
        body: InkWell(
          child: Text('xxxxxx'),
          onTap: () {
            print(context.owner.hashCode);
            context.findRenderObject();
            Navigator.push(context, MaterialPageRoute(builder: (content1){
              print(content1.owner == context.owner);
              return Scene1();
            }));
          },
        ),
      );
  }

跟着断点,我们发现这么一个方法_pushEntry(),在_pushEntry()中有一个_history的数组,用来保存路由需要进入的对象,_flushHistoryUpdates() 方法去刷新历史记录。

  void _pushEntry(_RouteEntry entry) {
    assert(!_debugLocked);
    assert(() {
      _debugLocked = true;
      return true;
    }());
    assert(entry.route != null);
    assert(entry.route._navigator == null);
    assert(entry.currentState == _RouteLifecycle.push);
    _history.add(entry);
    _flushHistoryUpdates();
    assert(() {
      _debugLocked = false;
      return true;
    }());
    _afterNavigation(entry.route);
  }

关于 _flushHistoryUpdates() 方法,逻辑处理较多:

void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
    .......
    .......
    final List<_RouteEntry> toBeDisposed = <_RouteEntry>[];
    while (index >= 0) {
      switch (entry!.currentState) {
        case _RouteLifecycle.add:
          assert(rearrangeOverlay);
          entry.handleAdd(
            navigator: this,
            previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
          );
          assert(entry.currentState == _RouteLifecycle.adding);
          continue;
        case _RouteLifecycle.adding:
          if (canRemoveOrAdd || next == null) {
            entry.didAdd(
              navigator: this,
              isNewFirst: next == null
            );
            assert(entry.currentState == _RouteLifecycle.idle);
            continue;
          }
          break;
        case _RouteLifecycle.push:
        case _RouteLifecycle.pushReplace:
        case _RouteLifecycle.replace:
          assert(rearrangeOverlay);
          entry.handlePush(
            navigator: this,
            previous: previous?.route,
            previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
            isNewFirst: next == null,
          );
          assert(entry.currentState != _RouteLifecycle.push);
          assert(entry.currentState != _RouteLifecycle.pushReplace);
          assert(entry.currentState != _RouteLifecycle.replace);
          if (entry.currentState == _RouteLifecycle.idle) {
            continue;
          }
          break;
        case _RouteLifecycle.pushing: // Will exit this state when animation completes.
          if (!seenTopActiveRoute && poppedRoute != null)
            entry.handleDidPopNext(poppedRoute);
          seenTopActiveRoute = true;
          break;
        case _RouteLifecycle.idle:
         ........
        case _RouteLifecycle.pop:
         .......
        case _RouteLifecycle.popping:
         .......
        case _RouteLifecycle.removing:
         .......
        case _RouteLifecycle.dispose:
            toBeDisposed.add(_history.removeAt(index));
            entry = next;
          .......
      }
      .......
    }
    ......
    for (final _RouteEntry entry in toBeDisposed) {
      for (final OverlayEntry overlayEntry in entry.route.overlayEntries)
        overlayEntry.remove();
      entry.dispose();
    }
     ......
  }

我们看 while 循环,主要目的是修改 entry.currentState 当前的状态

  • 当我们执行push时,entry.currentState 的状态记录为 push,然后变成 pushing,当我们执行完成状态修改为_RouteLifecycle.idle(闲着状态);
  • 当我们执行pop时,entry.currentState 的状态记录为pop,然后变成popping,最后变成 _RouteLifecycle.dispose,并记录entry需要执行 disposed。

我们在 NavigatorState 中发现,NavigatorState 记录了路由跳转的历史,标记了路由的跳转状态,并对pop的路由执行 dispose() 方法。

6:BuildContext 为什么要在这里返回 ?

除了 Navigator.of(context) 需要使用 context 来查找 NavigatorState 来进行跳转,还有很多地方需要用到 context,比如 showDialog、showModalBottomSheet 弹框,当然这些也是使用了 Navigator.of(context) 进行操作的。还有一些常用的状态管理,比如 provider

关于provider会在下一篇文章详细去了解它的内部实现。

7:总结一些:

  1. 在我们熟悉Widget build(BuildContext context)方法中,BuildContext context(本质上 context 是 Element 的实列);我们可以context as Element得到 Element 实列,并调用一些我们了解的方法,比如 (context as Element).markNeedsBuild();

  2. BuildContext context (本质上 context 是 Element 的实列)保存了对当前widget实列的引用,我们可以使用context.widget得到;并将这个widget转换成我们定义的widget,比如
    (context.widget as MyApp).name

  3. Navigator.of(context)可以通过当前的context自下而上找到NavigatorState进行路由切换等