本篇主要通过一个简单例子,讨论一下 Dart 代码里一个有趣的现象。

    我们都知道 Dart 里一切都是对象,就连基础类型 intdoublebool 也都是 class

    当我们对于 intdouble 这些 class 进行的 +-*\ 等操作时,其实是执行了这个 classoperator 操作符的操作, 然后返回了新的 num 对象。

    带你深入 Dart 解析一个有趣的引用和编译实验 - 图1

    对于这些 operator 操作最终会通过 VM 去进行实现返回,而本质上 dart 代码也只是文本,需要最终编译成二进制去运行。

    带你深入 Dart 解析一个有趣的引用和编译实验 - 图2

    以下例子基于 dart 2.12.3 测试

    那这里想要讨论什么呢?

    首先我们看一段代码,如下代码所示,可以看到:

    • 首先我们定义了一个叫 idxint 型参数;
    • 然后在 for 循环里添加了三个 InkWell 可点击控件;
    • 最后在 onTap 里面将 idx 打印出来;
    1. class MyHomePage extends StatelessWidget {
    2. var images = ["RRR", "RRR", "RRR",];
    3. @override
    4. Widget build(BuildContext context) {
    5. List<Widget> contents = [];
    6. int idx = 0;
    7. for (var imgUrl in images) {
    8. contents.add(InkWell(
    9. onTap: () {
    10. print("######## $idx");
    11. },
    12. child: Container(
    13. height: 100,
    14. width: 100,
    15. color: Colors.red,
    16. child: Text(imgUrl),
    17. )));
    18. idx++;
    19. }
    20. return Scaffold(
    21. appBar: AppBar(),
    22. body: Center(
    23. child: Column(
    24. children: [
    25. ...contents,
    26. ],
    27. )));
    28. }
    29. }
    • 问题来了,你觉得点击这三个 InkWell 打印出来的会是什么结果?

    • 答案是打印出来的都是 3。

    为什么呢?让我们看这段代码编译后的逻辑,如下所示代码,可以看到上述代码编译后, print 函数里指向的永远是 idx 这个 int* 指针,当我们点击时,最终打印出来的都是最后的 idx 的值

    1. @#C475
    2. method build(fra2::BuildContext* context) fra2::Widget* {
    3. core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);
    4. core::int* idx = 0;
    5. {
    6. core::Iterator<core::String*>* :sync-for-iterator = this.{main::MyHomePage::images}.{core::Iterable::iterator};
    7. for (; :sync-for-iterator.{core::Iterator::moveNext}(); ) {
    8. core::String* imgUrl = :sync-for-iterator.{core::Iterator::current};
    9. {
    10. [@vm.call-site-attributes.metadata=receiverType:dart.core::List<library package:flutter/src/widgets/framework.dart::Widget*>*] contents.{core::List::add}(new ink5::InkWell::•(onTap: () Null {
    11. core::print("######## ${idx}");
    12. }, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, $creationLocationd_0dea112b090073317d4: #C66610), $creationLocationd_0dea112b090073317d4: #C66614));
    13. idx = idx.{core::num::+}(1);
    14. }
    15. }
    16. }

    那如果我们需要打印出来的是每个 InkWell 自己的 index 呢?

    如下代码所示,我们在 for 循环里增加了一个 index 参数,把每次 idx 都赋值给 index ,这样点击打印出来的结果,就会是点击对应的 index

    1. class MyHomePage extends StatelessWidget {
    2. var images = ["RRR", "RRR", "RRR",];
    3. @override
    4. Widget build(BuildContext context) {
    5. List<Widget> contents = [];
    6. int idx = 0;
    7. for (var imgUrl in images) {
    8. int index = idx;
    9. contents.add(InkWell(
    10. onTap: () {
    11. print("######## $index");
    12. },
    13. child: Container(
    14. height: 100,
    15. width: 100,
    16. color: Colors.red,
    17. child: Text(imgUrl),
    18. )));
    19. idx++;
    20. }
    21. return Scaffold(
    22. appBar: AppBar(),
    23. body: Center(
    24. child: Column(
    25. children: [
    26. ...contents,
    27. ],
    28. )));
    29. }
    30. }

    为什么呢?

    让我们看新编译出来的代码,如下所示,可以看到对了 core::int* index = idx; 这段代码,然后回忆下前面所说的,Dart 里基本类型都是对象,而 operator 操作符运算后返回新的对象。

    这样就等于用 index 把每次的操作到保存下来,而 print 打印的自然就是每次被保存下来的 idx

    1. @#C475
    2. method build(fra2::BuildContext* context) fra2::Widget* {
    3. core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);
    4. core::int* idx = 0;
    5. {
    6. core::Iterator<core::String*>* :sync-for-iterator = this.{main::MyHomePage::images}.{core::Iterable::iterator};
    7. for (; :sync-for-iterator.{core::Iterator::moveNext}(); ) {
    8. core::String* imgUrl = :sync-for-iterator.{core::Iterator::current};
    9. {
    10. core::int* index = idx;
    11. [@vm.call-site-attributes.metadata=receiverType:dart.core::List<library package:flutter/src/widgets/framework.dart::Widget*>*] contents.{core::List::add}(new ink5::InkWell::•(onTap: () Null {
    12. core::print("######## ${index}");
    13. }, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, $creationLocationd_0dea112b090073317d4: #C66610), $creationLocationd_0dea112b090073317d4: #C66614));
    14. idx = idx.{core::num::+}(1);
    15. }
    16. }
    17. }

    那再来个不一样的写法

    如下代码所示,把 InkWell 放到一个 getItem 函数里返回,然后 index 通过函数参数传递进来,可以看到运行后的结果,也是点击对应 InkWell 打印对应的 index

    1. class MyHomePage extends StatelessWidget {
    2. var images = ["RRR", "RRR", "RRR",];
    3. @override
    4. Widget build(BuildContext context) {
    5. List<Widget> contents = [];
    6. int idx = 0;
    7. getItem(int index, String imgUrl) {
    8. return InkWell(
    9. onTap: () {
    10. print("######## $index");
    11. },
    12. child: Container(
    13. height: 100,
    14. width: 100,
    15. color: Colors.red,
    16. child: Text(imgUrl)));
    17. }
    18. for (var imgUrl in images) {
    19. contents.add(getItem(idx, imgUrl));
    20. idx++;
    21. }
    22. return Scaffold(
    23. appBar: AppBar(),
    24. body: Center(
    25. child: Column(
    26. children: [
    27. ...contents,
    28. ],
    29. )));
    30. }
    31. }

    为什么呢?

    我们继续看编译后的代码,如下代码所示,其实就是每次的 idx 都通过 getItem.call(idx)getItemindex 引用,然后下次又再次传递一个对应的 idx 进去,原理其实和上面的情况一样,所以每次点击也会打印对应的 index

    1. @#C475
    2. method build(fra2::BuildContext* context) fra2::Widget* {
    3. core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);
    4. core::int* idx = 0;
    5. function getItem(core::int* index) ink5::InkWell* {
    6. return new ink5::InkWell::•(onTap: () Null {
    7. core::print("######## ${index}");
    8. }, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, $creationLocationd_0dea112b090073317d4: #C66610), $creationLocationd_0dea112b090073317d4: #C66614);
    9. }
    10. {
    11. core::Iterator<core::String*>* :sync-for-iterator = this.{main::MyHomePage::images}.{core::Iterable::iterator};
    12. for (; :sync-for-iterator.{core::Iterator::moveNext}(); ) {
    13. core::String* imgUrl = :sync-for-iterator.{core::Iterator::current};
    14. {
    15. [@vm.call-site-attributes.metadata=receiverType:dart.core::List<library package:flutter/src/widgets/framework.dart::Widget*>*] contents.{core::List::add}([@vm.call-site-attributes.metadata=receiverType:library package:flutter/src/material/ink_well.dart::InkWell* Function(dart.core::int*)*] getItem.call(idx));
    16. idx = idx.{core::num::+}(1);
    17. }
    18. }
    19. }

    最后我们再换种写法。

    如下代码所示,直接用最基本的 for 循环添加 InkWell 并打印 idx ,结果会怎么样呢?

    1. class MyHomePage extends StatelessWidget {
    2. var images = [ "RRR","RRR", "RRR"];
    3. @override
    4. Widget build(BuildContext context) {
    5. List<Widget> contents = [];
    6. for (int idx = 0; idx < images.length; idx++) {
    7. contents.add(InkWell(
    8. onTap: () {
    9. print("######## $idx");
    10. },
    11. child: Container(
    12. height: 100,
    13. width: 100,
    14. color: Colors.red,
    15. child: Text(images[idx]),
    16. )));
    17. }
    18. return Scaffold(
    19. appBar: AppBar(),
    20. body: Center(
    21. child: Column(
    22. children: [
    23. ...contents,
    24. ],
    25. )));
    26. }
    27. }

    答案就是:点击对应 InkWell 打印对应的 index

    为什么呢?

    我们继续看编译后的代码,可以看到都是打印的 idx ,为什么这样就可以正常呢?

    这里最大的不同就是idx 被声明的位置不同

    1. @#C475
    2. method build(fra2::BuildContext* context) fra2::Widget* {
    3. core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);
    4. for (core::int* idx = 0; idx.{core::num::<}(this.{main::MyHomePage::images}.{core::List::length}); idx = idx.{core::num::+}(1)) {
    5. [@vm.call-site-attributes.metadata=receiverType:dart.core::List<library package:flutter/src/widgets/framework.dart::Widget*>*] contents.{core::List::add}(new ink5::InkWell::•(onTap: () Null {
    6. core::print("######## ${idx}");
    7. }, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, child: new text::Text::•(this.{main::MyHomePage::images}.{core::List::[]}(idx), $creationLocationd_0dea112b090073317d4: #C66607), $creationLocationd_0dea112b090073317d4: #C66613), $creationLocationd_0dea112b090073317d4: #C66617));
    8. }

    那这时候我们重新调整下,idx 放到 for 外面,点击测试会发现,打印的结果又都是 3

    1. class MyHomePage extends StatelessWidget {
    2. var images = [ "RRR", "RRR","RRR"];
    3. @override
    4. Widget build(BuildContext context) {
    5. List<Widget> contents = [];
    6. int idx = 0;
    7. for (; idx < images.length; idx++) {
    8. contents.add(InkWell(
    9. onTap: () {
    10. print("######## $idx");
    11. },
    12. child: Container(
    13. height: 100,
    14. width: 100,
    15. color: Colors.red,
    16. child: Text(images[idx]),
    17. )));
    18. }
    19. return Scaffold(
    20. appBar: AppBar(),
    21. body: Center(
    22. child: Column(
    23. children: [
    24. ...contents,
    25. ],
    26. )));
    27. }
    28. }

    这是为什么呢?

    看编译后的代码,唯一不同的就是 core::int* idx 的声明位置,那原因究竟是什么呢?

    1. @#C475
    2. method build(fra2::BuildContext* context) fra2::Widget* {
    3. core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);
    4. core::int* idx = 0;
    5. for (; idx.{core::num::<}(this.{main::MyHomePage::images}.{core::List::length}); idx = idx.{core::num::+}(1)) {
    6. [@vm.call-site-attributes.metadata=receiverType:dart.core::List<library package:flutter/src/widgets/framework.dart::Widget*>*] contents.{core::List::add}(new ink5::InkWell::•(onTap: () Null {
    7. core::print("######## ${idx}");
    8. }, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, child: new text::Text::•(this.{main::MyHomePage::images}.{core::List::[]}(idx), $creationLocationd_0dea112b090073317d4: #C66607), $creationLocationd_0dea112b090073317d4: #C66613), $creationLocationd_0dea112b090073317d4: #C66617));
    9. }

    因为 onTap 是在点击后才输出参数的,而对于 for (core::int* idx = 0; 来说,idx 的作用域是在 for 循环之内,所以编译后在 onTap 内要有对应持有一个值,来保存需要输出的结果。

    而对于 for 循环外定义的 core::int* idx , 循环内的所有 onTap 都可以指向它这个地址,所以导致点击时都输出了同一个 idx 的值。

    至于为什么会有这样的逻辑,在深入的运行时逻辑就没有去探索了(懒),推测应该是编译后的二进制文件在运行时,针对循环外的参数和循环内的参数优化有关系

    理论上,应该是属于变量捕获:

    • 对于全局变量,不会捕获,通过全局变量访问。
    • 对于局部变量,自动变量将会捕获,且是值传递。

    最后,如果你也想查看 dill 内容,可以通过 mac 下的 xxd 命令:

    1. xxd /Users/xxxxxxx/workspace/flutter-wrok/flutter_app_test/.dart_tool/flutter_build/bf7ed8e7e7b3e64f28f0af8a89a29ca9/app.dill

    也可以通过 dump_kernel.dart (在完整版 dart-sdk/Users/guoshuyu/workspace/dart-sdk/pkg/vm 目录下)执行如下命令,生成 app.dill.txt 查看,比如你可以查看 finalconst 编译后的区别。

    1. dart dump_kernel.dart /Users/xxxxxxx/workspace/flutter-wrok/flutter_app_test/.dart_tool/flutter_build/bf7ed8e7e7b3e64f28f0af8a89a29ca9/app.dill /Users/xxxxxxx/workspace/flutter-wrok/flutter_app_test/.dart_tool/flutter_build/bf7ed8e7e7b3e64f28f0af8a89a29ca9/app.dill.txt