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

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

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

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

image

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

「以下例子基于 dart 2.12.3 测试」

那这里想要讨论什么呢?

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

  • 首先我们定义了一个叫 idxint 型参数;

  • 然后在 for 循环里添加了三个 InkWell 可点击控件;

  • 最后在 onTap 里面将 idx 打印出来;

class MyHomePage extends StatelessWidget {  
var images = ["RRR", "RRR", "RRR",];
  @override  
Widget build(BuildContext context) {    
List<Widget> contents = [];    
int idx = 0;   
 for (var imgUrl in images) {     
 contents.add(InkWell(          
onTap: () { 
           print("######## $idx"); 
         },          
child: Container(            
height: 100,            
width: 100,            
color: Colors.red,            
child: Text(imgUrl),
          )));      
idx++;
    }    
return Scaffold(
      appBar: AppBar(), 
     body: Center(
          child: Column(
        children: [
          ...contents,
        ], 
     )));
  }}

  • 问题来了,「你觉得点击这三个 InkWell 打印出来的会是什么结果?」

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

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

    @#C475    
method build(fra2::BuildContext* context) → fra2::Widget* {
      core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);      
core::int* idx = 0; 
     {        
core::Iterator<core::String*>* :sync-for-iterator = this.{main::MyHomePage::images}.{core::Iterable::iterator};        
for (; :sync-for-iterator.{core::Iterator::moveNext}(); ) {         
 core::String* imgUrl = :sync-for-iterator.{core::Iterator::current};
          {           
 [@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 {  
            core::print("######## ${idx}");            
}, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, $creationLocationd_0dea112b090073317d4: #C66610), $creationLocationd_0dea112b090073317d4: #C66614)); 
           idx = idx.{core::num::+}(1); 
         } 
       }
      }

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

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

class MyHomePage extends StatelessWidget {
var images = ["RRR", "RRR", "RRR",];  
@override  
Widget build(BuildContext context) {   
 List<Widget> contents = [];   
 int idx = 0;    
for (var imgUrl in images) {     
 int index = idx;     
 contents.add(InkWell(         
 onTap: () {            
print("######## $index"); 
         },         
 child: Container( 
  height: 100,          
  width: 100,           
 color: Colors.red,            
child: Text(imgUrl),          
)));     
 idx++; 
   }   
 return Scaffold(     
 appBar: AppBar(),    
  body: Center(  
        child: Column(   
     children: [          ...contents,        ],     
 ))); 
 }}

为什么呢?

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

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

    @#C475   
 method build(fra2::BuildContext* context) → fra2::Widget* {      
core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);    
  core::int* idx = 0;      
{      
  core::Iterator<core::String*>* :sync-for-iterator = this.{main::MyHomePage::images}.{core::Iterable::iterator};       
 for (; :sync-for-iterator.{core::Iterator::moveNext}(); ) {         
 core::String* imgUrl = :sync-for-iterator.{core::Iterator::current};         
 {           
 core::int* index = idx;          
  [@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 {             
 core::print("######## ${index}");         
   }, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, $creationLocationd_0dea112b090073317d4: #C66610), $creationLocationd_0dea112b090073317d4: #C66614));          
  idx = idx.{core::num::+}(1);       
   }      
  }    
  }

「那再来个不一样的写法」

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

class MyHomePage extends StatelessWidget {  
var images = ["RRR", "RRR", "RRR",];  
@override 
 Widget build(BuildContext context) {  
  List<Widget> contents = [];    
int idx = 0;  
  getItem(int index, String imgUrl) {   
   return InkWell(       
   onTap: () {           
 print("######## $index");         
 },         
 child: Container(      
      height: 100,    
        width: 100,    
        color: Colors.red,     
       child: Text(imgUrl)));    
}    
for (var imgUrl in images) { 
     contents.add(getItem(idx, imgUrl));  
    idx++;   
 }   
 return Scaffold(    
  appBar: AppBar(),     
 body: Center(         
 child: Column(      
  children: [          ...contents,        ],      
))); 
 }}

为什么呢?

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

    @#C475   
 method build(fra2::BuildContext* context) → fra2::Widget* {      
core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);      
core::int* idx = 0;      
function getItem(core::int* index) → ink5::InkWell* {      
  return new ink5::InkWell::•(onTap: () → Null {          
core::print("######## ${index}");      
  }, 
child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, $creationLocationd_0dea112b090073317d4: #C66610), $creationLocationd_0dea112b090073317d4: #C66614);     
 }     
 {     
   core::Iterator<core::String*>* :sync-for-iterator = this.{main::MyHomePage::images}.{core::Iterable::iterator};      
  for (; :sync-for-iterator.{core::Iterator::moveNext}(); )
 {        
  core::String* imgUrl = :sync-for-iterator.{core::Iterator::current};         
 {   
 [@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));            idx = idx.{core::num::+}(1);        
  }      
  }    
  }      

「最后我们再换种写法。」

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

class MyHomePage extends StatelessWidget {  
var images = [ "RRR","RRR", "RRR"];  
@override  
Widget build(BuildContext context) { 
   List<Widget> contents = [];   
 for (int idx = 0; idx < images.length; idx++) {    
  contents.add(InkWell(        
  onTap: () {          
  print("######## $idx");        
  },        
  child: Container(           
 height: 100,         
   width: 100,         
   color: Colors.red,          
  child: Text(images[idx]),          
)));    
}   
 return Scaffold( 
       appBar: AppBar(),       
 body: Center(          
  child: Column(        
  children: [            ...contents,          ],     
   ))); 
 }}

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

为什么呢?

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

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

    @#C475    
method build(fra2::BuildContext* context) → fra2::Widget* {   
   core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);     
 for (core::int* idx = 0; idx.{core::num::<}(this.{main::MyHomePage::images}.{core::List::length}); idx = idx.{core::num::+}(1)) {      
  [@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 {          
core::print("######## ${idx}");       
 }, 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));    
  }

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

class MyHomePage extends StatelessWidget {  
var images = [ "RRR", "RRR","RRR"];  
@override  
Widget build(BuildContext context) {   
 List<Widget> contents = [];   
 int idx = 0;   
 for (; idx < images.length; idx++) {     
 contents.add(InkWell(        
  onTap: () {            
print("######## $idx");          
},         
 child: Container(          
  height: 100,          
  width: 100,          
  color: Colors.red,            
child: Text(images[idx]),          
)));    
}    
return Scaffold(    
    appBar: AppBar(),    
    body: Center(          
  child: Column(        
  children: [            ...contents,          ],      
  ))); 
 }}

这是为什么呢?

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

    @#C475    
method build(fra2::BuildContext* context) → fra2::Widget* {     
 core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);      
core::int* idx = 0;   
   for (; idx.{core::num::<}(this.{main::MyHomePage::images}.{core::List::length}); idx = idx.{core::num::+}(1)) {      
  [@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 {        
  core::print("######## ${idx}");       
 }, 
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));    
  }

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

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

至于为什么会有这样的逻辑,在深入的运行时逻辑就没有去探索了(懒),「推测应该是编译后的二进制文件在运行时,局部变量捕获变成值引用,全局还是值传递 」

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

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 编译后的区别。

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
推荐阅读
相关专栏
前端与跨平台
90 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。