Flutter 的 Widget,刻意加上 const,真的值得吗?

Hi,大家好,这里是承香墨影!

最近入坑 Flutter,又恰逢 Flutter 发布 2.0,正式全平台支持,那我也来分享一些 Flutter 相关的技术点,今天先从 const Widget 这个比较小的优化点来展开。看看它的原理和使用限制。

在 Flutter 中,可谓是 "一切皆 Widget",而 Widget 本质是一组信息的配置,它可以是 UI 展示,也可以是手势、导航等功能支持。 但 Widget 被设计成不可变的,任何改变都是通过销毁/重建得到修改,在 Widget 不可变的背后,则是依赖 Element 可变/可复用的支持,所以 Flutter 将 Widget 设计的极其轻量,重建成本非常低。

一些优化的技术提到,在构建 Widget 的时候,可以使用 const Widget,以此得到一个类似「缓存」的效果。虽然 Flutter 本身也会尽量的复用 Element,但是如果我们直接缓存 Widget,每次复用 Widget,最少节省了 Widget build 的步骤。

但是这个 const widget 的优化点,背后的原理是什么?优化的收益大吗?值得我们重点关注吗?今天就唠唠这个。

一、const 常量

在开始给 const 加上 Widget 前,我们先从 Dart 语言的 const 关键字开始。

说到 const,不得不提与它类似的概念 final,它们都有常量的语义,即一旦赋值就不可修改。

它们的区别在于,const 是编译时常量,用 const 修饰的常量,必须在声明时初始化,并且是可以确定的值。而 final 则是运行时常量,用 final 修饰的常量,必须在声明时初始化,或者在构造函数中初始化,但它的值可以动态计算。

话不多说,我们上个 Demo 就清晰了。

在我们用 const 声明类构造方法的时候,其相关的字段,必须是 final 的,也就是在设置后,就不可变了。

那么一个 Dart 类的对象,是否允许 const 修饰为常量,完全取决于类声明时,构造方法上是否被 const 修饰。

从上面的例子可以看到,当使用 const 构造的 Point 对象时,只要其入参一样,得到的就是同一个对象,例如示例中,p1 == p2 输出为 true。

而一旦 Point 的入参不同时,const 后的对象也不相同,例如示例中 p2 == p3 则输出为 false。

好了,我就不带大家捋这其中的逻辑了,大家仔细对比一下代码和输出就了解了。

前面提到,const 关键字修饰的常量,必须是初始时就可以确定的值,也就是赋值的时候也必须是一个常量。例如我稍微修改一下 const Point() 的入参,让它从一个方法获取,此时就会导致编译失败。

到这里,我们可以得到结论:使用 const 修饰的常量对象,只要保证构造时的入参不变化,我们可以得到相同的对象。

这背后的原理,无外乎就是 Dart VM 内部实现了常量池,利用享元模式复用了相同的对象。

二、const Widget

说清楚 Dart 中的 const 的意义以及如何使用,接下来我们把 Widget 加上,看看 const Widget 优化的原理。

在 Flutter 中,Widget 本质只是一个信息配置的元素,它被定义为不可变的,任何的变化反映出来就是销毁 & 重建。而 Widget 不可变之所以不太会影响效率,背后是因为 Element 实现了对 Widget 变化的抽象。也就是虽然 Widget 会被重建,但是 Widget 背后的 Element,却得到了复用。

同时 Flutter Framework,自有一套更新策略,确保 Widget 变化的同时,尽可能的复用 Element 对象。

这个策略的逻辑在 Element 的 updateChild() 方法中,该方法在 Element 树建立和更新的过程中都非常的重要。我们这里只关心更新的流程,即父 Element 节点已经存在一个子 Element 节点了。

父 Element 节点通过 updateChild() 方法,判断如何处理自身的子 Element 节点,是新建、更新,或者直接移除。

updateChild() 的实现可以看出,Element 依据新 Widget 更新的时候,会尽量复用已经存在的子 Element 节点,当无法被复用时,才会通过 inflateWidget() 创建新的子 Element 节点。

updateChild() 有 3 个入参,其中比较关键的是 child 和 newWidget,它们分别表示旧的 Element 子节点和新的 Widget 对象。

Element 本身也持有 Widget 对象,若 child 持有的 widget 和 newWidget 相等时,表示新旧 Widget 没有变化,updateChild() 的逻辑是直接复用 child,即旧的 Element,不会做任何额外的操作。

那么如果我们的 Widget 被定义成 const,在 updateChild() 更新阶段,新旧 Widget 就是同一个对象,则不会重建 Widget,其背后的 Element 也不会更新,自然能起到了缓存复用的作用,加快构建的效率。

三、const Widget 的收益大吗?

一切优化的技巧都是手段,背后的优化效果才是我们的最终目的。一顿疯狂的操作,结果输出不明显,这肯定不是我们想要的。

那么 const Widget 优化的效果明显吗?值得我们在写代码的时候,分出点思绪专门考虑,当下这个场景是否需要使用它吗?

先上结论:大可不必。

既然是优化,当然是做个测试,拿实测数据来说,是最右份量的。这里我找到了 @Cirzant Lai 分享的一个例子,就很能说明问题,之后再分享一些我的看法。

3.1 实测看数据

这里实现了一个小程序,显式一个随机显示 Image。

我们就对这个 Image 是否使用 const 关键字构造来做比对。

下面是使用 const 关键字的部分代码。

const Image(
  width:100,
  height:100,
  image:AssetImage('assets/logo.png'),
)
复制代码

另外一个参照程序没有使用 const 关键字。

这个 Image 通过 AnimatedPositioned 部件,每秒更新一次位置。然后使用 --profile 方式运行程序,并通过 Dart DevTools 观测其 "performance" 和 "memory"。

为了分析的严谨性,这里使用了一个较老的设备进行测试:Sony Xperia Z2,如果 Flutter 可以在此设备上依然很轻松的保持每秒 60 帧的速度,那么说明 Flutter 本身的优化已经非常好了。

除了设备之外,为了让测试数据更明显,这里逐步增多 Image 图标的数量,当增加到 1000 个时,每帧渲染所需的时间为 80ms,即约为 12.5 FPS。我想就没有继续增加数量的必要了,这个测试就以 1000 为边界,分别取 10、100、1000 三组数据,看看 "performance" 和 "memory" 分别的表现。

GPU & UI:

Memory:

从上面分享的数据可以看到,当 Image 有 1000 个的时候,FPS 约提升了 8.4%,内存使用量下降了 20%。虽然这个数据看似非常的明显,但是实际体验上,2 个测试的流畅度并没有非常明显的差异。

另外,我们在一个屏幕上放 1000 个 Image 的场景也不现实,对于大多数应用来说,每屏能有几十个 Widget,已经算是非常丰富了。而如果需要大量渲染,最好是直接使用 Canvas 绘制提升效率,而非依赖 const Widget 的这一点点优化。

3.2 const 对 GC 的优化

在 Flutter 中,Widget 作为配置信息,本身被设计的非常的轻量,就是为了适应频繁的销毁重建,这个操作必然会引起对旧 Widget 对象的回收。

当我们时候 const 关键字声明常量的时候,背后是利用的是类似常量池的概念,将 const 的对象缓存在常量池中,以待后续复用。

常量会具备普通对象更长的生命周期,这有好处也有坏处。好处是常量对象会对 GC 不那么敏感,也就是不需要频繁的触发 GC。坏处是常量池中的声明周期较长,可能导致不常用的对象被缓存后,没有合适的释放时机,导致内存占用过高。

实测下来,const 确实对 GC 有一些影响。

这里使用的是前文中的例子,不断的创建小 Widget,最终对比使用 const 前后对内存的占用情况。可以看到使用 const 修饰 Widget 后,GC 减少了,并且也更平缓了。

小结

到这里我们应该就清楚,使用 const 修饰的 Widget,确实对 Flutter 的 build 过程有一些优化,但是优化并非很明显。在使用过程中,我们只需要按需使用就行,无需刻意的追求大量的 const 化。

最后小结一下,对于 Dart 语言来说。

  1. 一个 Dart 类的对象是否能用 const 修饰,取决于类的构造方法上是否被 const 修饰;
  2. 使用 const 修饰的构造方法中,所有成员必须被 final 修饰;
  3. 构造 const 对象时,传参也必须是 const 的常量;
  4. const 修饰的构造方法,不能有方法体;

对于 Flutter 来说,const 修饰的优化点:

  1. 利用常量池复用 Widget,在更新频繁的 Widget 场景中,有优化作用,避免了 Widget 的回收和重建;
  2. const 对 GC 有一定的抑制左右,在会创建大量相同对象的场景下,创建的对象少了,自然 GC 也会变少;

References:

更多精彩内容,欢迎关注公众号:承香墨影(ID:cxmyDev),在公众号回复关键字「成长」,可领取独家资料。

Guess you like

Origin juejin.im/post/6977212326394986510