iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 15
0
Mobile Development

Why Flutter why? 從表層到底層,從如何到為何。系列 第 15

days[14] = "想瞭解Hot Reload如何運作,就自己來實作!"

對,我說真的,今天的內容其實沒有很難。我們只需要稍微把Dart VM叫出來溝通一下,全部程式碼頂多30行,也沒有什麼複雜難理解的邏輯。最重要的是這很有趣,一起來試試看吧!

流程

首先來複習一下整個Hot Reload大致的流程:
https://ithelp.ithome.com.tw/upload/images/20200915/20129053ElJZ61xOCV.png
source

  1. 從專案source code中找出被修改的檔案
  2. 將檔案編譯成Dart Kernel檔(註:Dart Kernel是一種中間語言,不是什麼OS的Kernel)
  3. 將更新的Kernel檔透過HTTP發送給DartVM
  4. DartVM透過RPC,重新讀取Kernel檔
  5. DartVM透過RPC,呼叫Flutter的reassemble

我們可以看到流程進行到5.才進入Flutter,1~4其實都是純Dart就可以做到的事情。而這個5.雖然講起來簡單,實際上還是牽涉到很多Flutter的複雜邏輯,所以今天我們會實作的是純Dart版的Hot Reload,並在最後稍微偷瞄一下Flutter hot reload的實作。

永遠的Hello World

一開始我們要來寫一個會不停印出Hello, World!的程式,然後試著在程式執行時,用Hot Reload把它改成Hello, Dart!。首先建立我們今天要使用的專案資料夾hot_reload

mkdir hot_reload
cd hot_reload

接著建立一個main.dart

import 'dart:async';

void main() {
  Timer.periodic(Duration(seconds: 3), (timer) {
    runApp();
  });
}

void runApp() {
  print("Hello, World!");
}
  1. 我們用一個Timer來使它持續執行下去,因為我們想要在程式執行過程中進行Hot Reload。
  2. 我們須要獨立一個runApp函數出來,因為main函數裡的改動無法被Hot Reload。

然後試著執行它:

D:\hot_reload>dart --enable-vm-service main.dart
Observatory listening on http://127.0.0.1:8181/xZqIQ1NpRNA=/
Hello, World!
Hello, World!
Hello, World!
...

很好,沒什麼特別的,除了--enable-vm-service和一串既陌生又熟悉的網址...

使用Observatory進行Hot Reload

--enable-vm-service讓我們可以透過Observatory,在瀏覽器中觀察Dart VM運作的各種資訊:
https://ithelp.ithome.com.tw/upload/images/20200915/20129053Qo31fKYqvZ.png
裡面有很多有趣的東西,大家可以趁機瀏覽一下。接下來我們進入上圖紅框的Isolate:
https://ithelp.ithome.com.tw/upload/images/20200915/20129053gdbKtmWoK3.png
右上角的Reload Source,就是Observatory透過http呼叫Dart VM,進行Hot Reload的地方。讓我們回去修改runApp程式碼,再回來點點看吧:

void runApp() {
  print("Hello, Dart!");
}

// output after Reload Source
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, Dart!
Hello, Dart!
Hello, Dart!

果然成功reload了!話說這Observatory也就是個web介面,既然它能呼叫Dart VM,當然我們也可以。

使用vm_service進行Hot Reload

接下來我們就要模仿Observatory,在我們的程式裡導入vm_service,透過程式去呼叫reload source。

Dependencies

首先在我們的專案資料夾中建立pubspec.yaml

name: hot_reload

dependencies:
  watcher:
  vm_service:

除了vm_service之外,我們還須要watcher來觀察檔案的變化,讓我們可以在每次程式碼有變更時觸發Hot Reload。別忘了執行flutter pub get,你可以趁這個機會觀察一下實際上到底發生了什麼事。

watcher

接下來我們先看看watcher怎麼運作,回到main.dart,修改成如下程式碼:

import 'package:watcher/watcher.dart';

void main() {
  final watcher = DirectoryWatcher(".");
  watcher.events.listen(print);
}

執行之後,每當我們修改hot_reload中的任何檔案,都會收到event並被print出來:

D:\hot_reload>dart main.dart
add .\test.dart
modify .\test.dart
remove .\test.dart

vm_service

知道watcher怎麼使用之後,我們再來看看怎麼使用vm_service建立hotReload函數:

Future<ReloadReport> hotReload() async{
  final Uri serverUri = (await Service.getInfo()).serverUri;
  final Uri webSocketUri = convertToWebSocketUrl(serviceProtocolUrl: serverUri);
  final VmService service = await vmServiceConnectUri(webSocketUri.toString());
  final VM vm = await service.getVM();
  final String isolateId = vm.isolates.first.id;
  final ReloadReport report = await service.reloadSources(isolateId);
  return report;
}

(這邊其實就照程式碼翻譯而已,搞不好你直接看程式碼比較快)

  1. 透過dart:developer提供的Service類別,取得目前Dart VM開啟的web server的URI
  2. 將URI轉成web socket的URI
  3. 透過web socket URI和Dart VM service連線
  4. 透過VM service取得VM第一個Isolate的id
  5. 透過VM service重新讀取第一個Isolate的程式碼
  6. 回傳reload的報告

完成程式

最後把它們合併起來,讓我們在每次watcher監聽到程式碼修改時,呼叫hotReload吧:

import 'dart:developer';

import 'package:vm_service/utils.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
import 'package:watcher/watcher.dart';

void main() {
  final watcher = DirectoryWatcher(".");
  watcher.events.listen((event) async {
    final report = await hotReload();
    if (report.success) {
      print("Hot reload succeed.");
      runApp();
    } else {
      print("Hot reload failed.");
      print(report.json['notices'][0]['message']);
    }
  });
  runApp();
}

void runApp() {
  print("Hello, World!");
}

Future<ReloadReport> hotReload() async{
  final Uri serverUri = (await Service.getInfo()).serverUri;
  final Uri webSocketUri = convertToWebSocketUrl(serviceProtocolUrl: serverUri);
  final VmService service = await vmServiceConnectUri(webSocketUri.toString());
  final VM vm = await service.getVM();
  final String isolateId = vm.isolates.first.id;
  final ReloadReport report = await service.reloadSources(isolateId);
  return report;
}

實際測試

程式完成了,趕快來玩玩看我們自己實作的Hot Reload吧!

D:\hot_reload>dart --enable-vm-service main.dart
Observatory listening on http://127.0.0.1:8181/NT7CNzm-hqU=/
Hello, World!
Hot reload succeed.
Hello, Dart!
Hot reload failed.
main.dart:24:23: Error: Expected ';' after this.
  print("Hello, Dart!")
                      ^
Hot reload succeed.
Hello, Dart!

完美!不但程式正確時可以成功執行Hot Reload,錯誤時也有我們熟悉的錯誤訊息。

Flutter Hot Reload

最後我們來稍微看一下Flutter這邊是怎麼做的。首先開啟flutter_tools這個專案,位於flutter/packages/flutter_tools。注意如果你開啟後IDE抱怨找不到Dart SDK,你須要進行設定:
https://ithelp.ithome.com.tw/upload/images/20200915/20129053FFsgTo5hhn.png

然後請在專案中搜尋FlutterDevice.reloadSource()函數:
https://ithelp.ithome.com.tw/upload/images/20200915/20129053mjxRIAlbOM.png
這是不是跟我們的Dart Hot Reload很像呢?同樣透過vmService取得vm,再透過vmService更新某個isolate。

這次我就不深入探索整個Flutter Hot Reload的運作機制了,如果你很有興趣,或跟我一樣常常吃飽太閒的話,可以透過接下來的方式在執行APP時進入flutter_tools。

Debug flutter_tools

首先我們須要一個Flutter App:

import 'package:flutter/material.dart';

void main() => runApp(App());

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: FlutterLogo(),
        ),
      ),
    );
  }
}

接著為flutter_tools專案新增debug config,注意這裡5和7要換成你自己的flutter_tool和測試專案路徑:
https://ithelp.ithome.com.tw/upload/images/20200915/20129053gWrmBeXvYb.png

用debug mode執行之後,你會在console看到這樣的訊息:
https://ithelp.ithome.com.tw/upload/images/20200915/20129053Y5bMB4Ja8T.png

使用r來進行hot reload吧:
https://ithelp.ithome.com.tw/upload/images/20200915/20129053AwtnVqyCqz.png
中斷成功!到這裡你就可以開始自行探索整個Flutter Hot Reload的流程細節了,下次可以考慮來發一篇文吧!

總結

這次我們為了瞭解Dart語言的Hot Reload機制是如何運作的,首先使用Observatory進行reload source,接著嘗試自己用簡單的Dart程式來實作Observatory的功能。我們利用watcher來觀察source code的變化,並呼叫vm_service來進行reloadSource。我們也看到flutter_tools中,用了和我們相似的作法來實現Flutter Hot Reload。最後我們為flutter_tools建立debug config,成功在hot reload時進入中斷點,為大家後續的深入研究做好了準備。


上一篇
days[13] = "IntelliJ/AS做得比VSCode好的幾件事"
下一篇
days[15] = "為什麼你應該使用StatelessWidget而非Functional Widget?"
系列文
Why Flutter why? 從表層到底層,從如何到為何。30

尚未有邦友留言

立即登入留言