善用 Provider 榨干 Flutter 最后一点性能

·  阅读 8858

Provider 作为 Google 钦定的状态管理框架,以其简单易上手的特点,成为大部分中小 App 的首选。Provider 的使用非常简单,官方文档也不长,基本上半个小时就能上手操作。但要用好 Provider 却不简单,这可关系到 App 的运行效率和流畅度。 下面我就总结了一些 Provider 在使用过程中需要注意的 Tips,帮助你榨干 Flutter 的最后一点性能!

⚠️ 提示:本文不是 Provider 入门教程,需要你对 Provider 有一个基本对了解。初学者建议跳转到问末首先阅读官方文档 & 实例教学。

更新到最新版本

毫无疑问 Flutter 连带整个第三方插件社区都在高密度的迭代,Provider 作为一个发布才1年多的库如今已经迭代到 4.0 了。每一次更新不仅仅是 Bug 的修复,还有大量功能的提升和性能的优化。比如 3.1 推出的 Selector,以及后期加入的针对性能的提示等。

正确地初始化 Provider

所有的 Provider 都有两个构造方法,分别为默认构造方法和便利构造方法。很多人简单地认为便利构造方法只是一种更加简便的构造方法,它们接收的参数是一样的。其实不对。 我们以 ChangeNotifierProvider 为例:

// ✅ 默认构造方法
ChangeNotifierProvider(
  create: (_) => MyModel(),
  child: ...
)
复制代码
// ❌ 默认构造方法
MyModel myModel;
ChangeNotifierProvider(
  create: (_) => myModel,
  child: ...
)
复制代码
// ✅ 便利构造方法
MyModel myModel;
ChangeNotifierProvider.value(
  value: myModel,
  child: ...
)
复制代码
// ❌ 便利构造方法
ChangeNotifierProvider.value(
  value: MyModel(),
  child: ...
)
复制代码

简单的说就是,如果你需要初始化一个新的 Value ,就使用默认构造方法,通过 create 方法的返回值传递。而如果你已经有了这个 Value 的实例,则使用便利构造方法,直接赋值给 value 参数。具体的原因可以参考这个解答

尽量使用 StatelessWidget 替代 StatefulWidget

由于引入了 Provider 进行统一的状态管理,因此大部分 Widget 不再需要继承自 StatefulWidget 来更新数据了。StatelessWidget 的维护成本比 StatefulWidget 要低,构建效率更高。同时更少的代码量会让我们更容易地控制重建范围,提高渲染效率。

当然,对于部分需要依附于 Widget 生命周期的逻辑(比如首次进入页面进行 HTTP 请求),还是得继续使用 StatefulWidget 。

尽量使用 Consumer 替代 Provider.of(context)

Provider 取值有两种方式,一种是 Provider.of(context) ,直接返回 Value。

由于它是一个方法,无法直接在 Widget 树中调用,一般我们放在 build 方法中,return 方法之前。

Widget build(BuildContext context) {
  final text = Provider.of<String>(context);
  return Container(child: Text(text));
}
复制代码

但是,由于 Provider 会监听 Value 的变化而更新整个 context 上下文,因此如果 build 方法返回的 Widget 过大过于复杂的话,刷新的成本是非常高的。那么我们该如何进一步控制 Widget 的更新范围呢?

一个办法是将真正需要更新的 Widget 封装成一个独立的 Widget,将取值方法放到该 Widget 内部。

Widget build(BuildContext context) {
  return Container(child: MyText());
}

class MyText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final text = Provider.of<String>(context);
    return Text(text);
  }
}
复制代码

另一个相对好一点的办法是使用 Builder 方法创造一个范围更小的 context。

Widget build(BuildContext context) {
  return Container(child: Builder(builder: (context) {
    final text = Provider.of<String>(context);
    return Text(text);
  }));
}
复制代码

这两种方法都能够在刷新 Widget 时跳过 Container 直接重建 Text 。无论哪种方法,其根本目的就是缩小 Provider.of(context) 中 context 的范围,减少 Widget 重建数量。但这两个方法都太过繁琐。

Consumer 是 Provier 的另一种取值方式,不同的是它是一个 Widget ,能够方便的嵌入到 Widget 树中调用,类似于上面的 Builder 方案。

Widget build(BuildContext context) {
  return Container(child: Consumer<String>(
    builder: (context, text, child) => Text(text),
  ));
}
复制代码

Consumer 可以直接拿到 context 连带 Value 一并传作为参数传递给 builder ,使用无疑更加方便和直观,大大降低了开发人员对于控制刷新范围的工作成本。

Consumer 的 builder 方法有一个 child 属性,我们可以将 Container 层级下不受 Value 影响的 Widget 写到 child 中,这样当 Value 更新时不会重新构建 child 中的 Widget ,进一步提高效率。

Widget build(BuildContext context) {
 return Container(child: Consumer<String>(
   builder: (context, text, child) => Row(
     children: <Widget>[
       Text(text),
       child
     ],
   ),
   child: Text("不变的内容"),
 ));
}
复制代码

上面代码中将不受 text 控制的 Text 放入 child 中并带入 builder 方法,这样当 text 改变时不会重新构建 child 中的 Text。

尽量使用 Selector 替代 Consumer

Selector 是 3.1 推出的功能,目的是更近一步的控制 Widget 的更新范围,将监听刷新的范围控制到最小。 实际项目中我们往往会根据业务场景或者页面元素来设计 Provider 的 Value,此时的 Value 其实就是 ViewModel。大量的数据都放入 Value 的后果就是,只要一个值的改动,就会触发整个 ViewModel 的 notifyListeners ,进而引发整个 ViewModel 关联 Widget 的刷新。

因此,我们需要一个能力,在执行刷新之前给我们一次机会,判断是否需要刷新,来避免不需要的刷新。这个能力,就是由 Selector 来实现的。

Selector<ViewModel, String>( 
  selector: (context, viewModel) => viewModel.title,
  shouldRebuild: (pre, next) => pre != next,
  builder: (context, title, child) => Text(title)
);
复制代码

Selector 有两个范型参数,分别是 Provider 的 Value 类型以及 Value 中具体用到的参数类型。它有三个参数:

  • selector:是一个 Function,传入 Value ,要求我们返回 Value 中具体使用到的属性。
  • shouldRebuild:这个 Function 会传入两个值,其中一个为之前保持的旧值,以及此次由 selector 返回的新值,我们就是通过这个参数控制是否需要刷新 builder 内的 Widget。如果不实现 shouldRebuild ,默认会对 pre 和 next 进行深比较(deeply compares)。如果不相同,则返回 true。
  • builder:返回 Widget 的地方,第二个参数 title,就是我们刚才 selector 中返回的 String。

有了 Selector ,我们就可以避免 ViewModel 中一人改动全家更新的尴尬了。但 Selector 的使用场景远远不限于 ViewModel 这种重 Value ,即便是用在单一数据上,Selector 也能尽最大限度榨干性能。

比如一个数据列表 List ,如果修改其中一项数据,我们往往会更新整个 ListView 中的 ListTile 。

return ListView.builder(itemBuilder: (context, index) {
    final foo = Provider.of<ViewModel>(context).foos[index]
    return ListTile(title: Text(foo.didSelected),);
});
复制代码

如果通过 Performance 或者 Log 我们会发现,只修改 foos 中的某一个 foo 的 didSelected 属性,会将所有的 ListTile 都重新构建一遍。这无疑是没有必要的。

return ListView.builder(itemBuilder: (context, index) {
  return Selector< ViewModel, Foo>(
    selector: (context, viewModel) => viewModel.foos[index],
    shouldRebuild: (pre, next) => pre != next, // 此行可以省略
    builder: (context, foo, child) {
      return ListTile(
        title: Text(foo.didSelected),
      );
    },
  );
});
复制代码

通过 Selector 不仅能在构建 Widget 的过程中方便的获取 Value ,还能在构建子 Widget 之前留给我们一个额外的机会让我们决定是否需要重新构建子 Widget 。这样,ListView 每次就只会重构被修改的那个 ListTile 了。

善用 Provider.of(context) 的隐藏属性 listen

前面的 Consumer 似乎可以替代 Provider.of 的所有场景,那我们还需要 Provider.of 吗? 我们常常有这样的需求,就是只需要取得上层 Provider 的 Value,不需要监听并刷新数据,比如调用 Value 的方法。

Button(
  onPressed: () =>
      Provider.of<ViewModel>(context).run(),
)
复制代码

上面这样的写法会报错,因为 onPressed 方法只需要拿到 ViewModel 来调用 run 方法,它的内部不关心 ViewModel 是否有变化需不需要刷新。而 Provider.of 默认会监听 ViewModel 的改变并影响运行效率。 其实 Provider.of(context) 方法有一个隐藏属性 listen ,对于这种不关心 Value 是否变化只需要取值的情况,只需要将 listen 设置为 false(默认为 true ),Provider.of 返回的 Value 就不会触发监听刷新啦。

Button(
  onPressed: () =>
      Provider.of<ViewModel>(context, listen: false).run(),
)
复制代码

避免在错误的地方获取 Value

前面提到了,有些逻辑必须依赖 Widget 的生命周期,比如在进入页面时访问 Provider 。因此很多人会将逻辑放到 StatefulWidget 的 initState 或 didChangeDependencies 中。

initState() {
  super.initState();
  print(Provider.of<Foo>(context).value);
}
复制代码

但是这么做是有矛盾的,而且也会报错。既然将 load 方法放到了 initState 回调中,就意味着你希望该方法在 Widget 生命周期内只走一次,也就就意味着此处的 Value 并不关心值会不会改变。

因此,如果你只是想要拿到 Value 而不需要监听,直接使用上面的 listen 参数关闭监听即可。

initState() {
  super.initState();
  print(Provider.of<Foo>(context, listen: false).value);
}
复制代码

而如果你需要持续监听 Value 并作出反应,则不应该将逻辑放入 initState 中,didChangeDependencies 更适合这样的逻辑。但是由于 didChangeDependencies 会频繁调用多次,获取 Value 之后需要判断一下 Value 是否有改变,避免 didChangeDependencies 方法死循环。

Value value;

didChangeDependencies() {
  super.didChangeDependencies();
  final value = Provider.of<Foo>(context).value;
  if (value != this.value) {
    this.value = value;
    print(value);
  }
}
复制代码

但是!

以上方案只适用于访问 Value ,如果需要修改 Value 并触发更新(例如访问网络),则会报错。因为 initState didChangeDependencies 中是不能触发状态更新的(包括调用 setState ),这样可能会导致 Widgets 在上次构建还没完成之前状态就又被更新,最终导致状态不统一。

因此,官方的建议是,如果 Provider Value 的方法不依赖外部参数,直接在 Value 初始化的时候执行方法。

class MyApi with ChangeNotifier {
  MyApi() {
    load();
  }

  Future<void> load() async {}
}
复制代码

如果 Provider Value 的方法必须依赖 Widgets 提供的外部参数,可以用 Future.microtask 将调用过程包在一个异步方法中。异步方法由于 event loop 的缘故会推迟到下一个周期运行,避开了冲突。

initState() {
  super.initState();
  Future.microtask(() =>
    Provider.of<MyApi>(context, listen: false).load(page: page);
  );
}
复制代码

及时释放资源

及时释放不再使用的资源是优化的重点。Provider 提供了两套方案方便我们及时释放资源。

  1. Provider 的默认构造方法中有一个 dispose 回调,会在 Provider 销毁时触发。我们只需要在这个回调中释放我们的资源即可。
Provider(
    create:(_) => Model(),
    dispose:(context, value) {
        // 释放资源
    }
)
复制代码
  1. 重写 ChangeNotifier 的 dispose 方法。细心的同学可能会发现,ChangeNotifierProvider 的初始化方法中是没有 dispose 这个参数的,这是因为 ChangeNotifierProvider 会在销毁时自动帮我们调用 Value 的 dispose 方法。我们所需要做的,仅仅是重写 Value 的 dispose 方法罢了。
class Model with ChangeNotifier { 
  @override
  void dispose() {
    // 释放资源
    super.dispose();
  }
}
复制代码

其实,这恰恰也是 ChangeNotifierProvider 和 ListenableProvider 的最大区别。ChangeNotifierProvider 继承自 ListenableProvider ,只不过 ChangeNotifierProvider 对 Value 的类型要求更高,必须实现 ChangeNotifier ,而 dispose 是 ChangeNotifier 的一个方法。

除此之外,我们还应该避免将所有 Provider 状态都放置到顶层。虽然取用起来比较方便,但全局的 Provider 资源都无法释放,对性能的影响会越来越大。我们应该在构建新页面和新功能的时候就理清业务,让 Provider 只覆盖它所负责的范围,并在退出该功能页面后及时释放资源。

多打 Log 多跑 Performance

最简单最无脑的方式就是在 Widget 之间插入 Log 来观察 Widget 的刷新范围,一旦发现刷新范围过大,和实际逻辑不符就应该尝试查找优化点。这种排查方式虽然相对粗旷,但对于尚未怎么优化的项目而言效果显著。

对于已经做过初步优化的项目而言,如果还想近一步榨干 Flutter 的性能,就只能通过跑 Performance 搭配工具来分析出性能瓶颈。

总结

其实上面所有的 Tips ,背后其实都在做一件事情:减少 Widget 的重建。

虽然我们知道 Flutter 在内部做了大量高效的算法和策略来避免无效的重建和渲染,但再高效的算法也是有成本的,更何况算法对我们来说是一个黑盒子,我们无法保证它能一直有效,因此我们需要在源头就掐断无用的 Widget 重建。

最后是浓缩版的建议:

  1. 每一次通过 Provider 取值的时候都问自己一遍,我是否需要监听数据,还是只是单纯访问 Value 。
  2. 每一次通过 Provider 取值的时候都问自己一遍,是否可以用 Selector 替代 Consumer,不行的话是否可以用 Consumer 替代 Provider.of(context)。

provider

Flutter | 状态管理指南篇——Provider

分类:
阅读
标签:
分类:
阅读
标签:
收藏成功!
已添加到「」, 点击更改