今天收到这个问题,本来想着简单回复下,但是感觉这个话题又可以稍微展开讲讲,干脆就整理成一篇简单的科普,这样也能更方便清晰地回答这个问题。
聊这个问题之前,我们需要把一个“老生常谈”的概念拿出来说,那就是:Flutter 里 Widget 是不可变的,它不是真正的 View,Widget 只是一个「配置文件」的作用。
后面只有基于这个概念,结合 const 的「深度不变性」 ,才能更全面理解为什么 Flutter 中推荐 Widget 使用 const 。
Dart 里的 final & const
我们先简单过一遍 Dart 里的 final 和 const 的区别,要解答开头那个问题,只讲 const 明显是不够,在 Dart 里:
-
final :变量只能赋值一次,值在运行时确定
-
const:变量必须是编译时常量,值在编译时已知
虽然都是「不可变」声明,但是对于 Dart 来说,final 和 const 最大的区别就在于一个是运行时确定,一个是编译时确定。
final
针对 final , final 虽然也不可变 ,但是它的值可以在运行时确定,同时它还允许延迟初始化(late),如下代码所示:
- 变量
a
可以是late final
result
的数可以是通过doSomeThing
返回
late final String a;void runResult() {final int result = doSomeThing();
}
也就是 final 可以在运行时赋值,之后就不可以改变,类似场景就可以对应在 Widget 的构造函数上,通过 final 关键字创建不可变的实例变量,这些变量在构造函数级别初始化,并且对于每个类实例都是唯一的:因为 Flutter 里 Widget 是不可变的,所以对于 Widget 来说,它内部的变量也应该是不可变。
class MyHomePage extends StatelessWidget {MyHomePage({super.key, this.title});final String? title;
}
const
const 属于编译时不可变声明,可以理解为它是比 final 更高级的 「深度不变」,也就是编译时就确定了它的值,所以它会有更好的性能优势,例如:
- 作为编译时常量的, const 变量在编译时已知,因此它在编译期间只会被“评估”一次,这意味着 Dart 编译器可以对它们进行优化,从而节省内存并缩短需要的启动时间
- 当 const 变量在不同位置使用时,Dart 编译器只会给它分配一次空间,并且该值将在引用它的其他位置重复使用。
从这个角度理解,const 确实可以一定程度提高性能和节约内存 ,再举个典型例子解释下 「深度不变」,如下代码所示:
可以看到通过 const 声明的 list, 它内部的 item 也是在编译时确定,并且是不允许被修改,不仅列表本身是一个编译时常量,它内部每个元素也是编译时常量。
const List<int> list = [0,0,0,0,0,0];list[2] = 3;
而对于 class 而言,const 声明的构造函数,会被要求内部变量需要使用 final 声明,从而确保对象是可传递的不可变的,这样就可以保证静态数据的完整,并且对象一旦设置就无法被篡改。
class Test {final int a;const Test(this.a);
}void runTest() {const Test test = Test(0);test.a = 100; /// error
}
这个看起来是不是很眼熟?对,没错,就是 Flutter 里的 Widget,当 Widget 的构造函数是 const 的时候,它内部的变量都需要时 final ,不然就会在编译时报错。
class MyHomePage extends StatelessWidget {const MyHomePage({super.key, this.title});final String? title;
}
如果没有 const ,默认规则下只会是警告:This class (or a class that this class inherits from) is marked as ‘@immutable’, but one or more of its instance fields aren’t final 。
另外,const 声明的构造函数,对于 class 来说也会在编译时优化,如下代码所示,可以看到它们都在编译期得到了优化:
- test 1 和 test 2 的 hashCode 是一样的
- test 3 和 test 4 的 hashCode 是一样的
class Test {final int a;final int b;const Test(this.a, this.b);}void runTest() {const Test test1 = Test(0, 0);print("test1 hash code is: ${test1.hashCode}");const Test test2 = Test(0, 0);print("test2 hash code is: ${test2.hashCode}");const Test test3 = Test(1, 1);print("test3 hash code is: ${test3.hashCode}");const Test test4 = Test(1, 1);print("test4 hash code is: ${test4.hashCode}");const Test test5 = Test(2, 2);print("test5 hash code is: ${test5.hashCode}");}
Flutter
那么回到最初的问题,因为 Flutter 里的 Widget 不是真正的 View ,它只是个配置文件,背后是 Element 和 RenderObject 实体在工作,所以对于「不可变」的 Widget 来说,const 去声明一个「配置文件」做优化,明显可以提高性能和减少内存占用。
至于为什么说 Widget 不是真正的 View , 详细的可以看我以前的文章,这里简单展示一个我经常提到的例子,如下代码所示,textUseAll
如果是一个真正的 View ,它是不能同时被多个地方添加,从这个例子可以更直观体现 Widget 是配置信息的作用。
对于 Flutter 来说,Flutter 会严重依赖 Widget 树的 「配置信息」来表示 UI,在 rebuild 期间遇到标记为const 的 Widget 时,Flutter 会将其识别为预构建且不可变的对象 ,这个情况下, Flutter 可以重复使用现有对象,而不必创建新对象,这种重复使用可避免不必要的计算和对象分配。
同时前面提到过,const 在编译时会执行优化,这些优化包括前面提到的预分配内存和常量折叠,这意味着在运行时可以更快地创建对象并减少垃圾回收触发。
另外,对于 Widget Tree 来说,const 可以确保只有当它们的引用实际发生变化时才会 rebuild,进而减少了不必要的 Widget 创建和重构。
所以,是不是无用知识又增长了?