帶你深入 Dart 解析一個有趣的引用和編譯實驗

戀貓de小郭發表於2021-05-28

本篇主要通過一個簡單例子,討論一下 Dart 程式碼裡一個有趣的現象。

我們都知道 Dart 裡一切都是物件,就連基礎型別 intdoublebool 也都是 class

當我們對於 intdouble 這些 class 進行的 +-*\ 等操作時,其實是執行了這個 classoperator 操作符的操作, 然後返回了新的 num 物件。

對於這些 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
複製程式碼

相關文章