Flutter桌面開發 - windows外掛開發

語言: CN / TW / HK

theme: vuepress

通過此篇文章,你將瞭解到: 1. Flutter外掛的基本介紹; 2. windows外掛開發的真實踩坑經驗。

⚠️本文為稀土掘金技術社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!

前言

我們都知道,Flutter的定位更多是作為一個跨平臺的UI框架,對於原生平臺的功能,開發過程中經常需要外掛來提供。不幸的是Windows的生態又極其不完整,外掛開發必不可少。但網上windows的文章少之又少,所以本篇文章,我們一起來聊聊外掛開發的一些技巧。

外掛介紹

Flutter的外掛主要分兩種:package和plugin。 - Package是純dart程式碼的庫,不涉及原生平臺的程式碼; - Plugin是原生外掛庫,是一種特殊的Package。Plugin需要開發者分別在各原生平臺實現對應的能力。

其中Plugin是我們要著重講的,既然是原生平臺實現,那跟dart層就勢必需要通訊。Flutter Plugin的通訊主要有:methodChannel、eventChannel、basicMessageChannel。 - MethodChannel:同步呼叫的通道,呼叫後可以通過result返回結果。可以 Native 端主動呼叫,也可以Flutter主動呼叫,屬於雙向通訊。這種通訊方式是我們日常開發中為最常用的方式, 關鍵點是Native 端的呼叫需要在主執行緒中執行。 - EventChannel:非同步事件通知的通道,一般是Native端主動發出通知,Flutter接收通訊資訊。 - BasicMessageChannel:長連結的通道,雙端可以隨時發出訊息,對方收到訊息後可以使用reply進行回覆。一般常用於需要雙向通訊可不知道何時需要傳送的場景。

windows外掛編寫

Flutter Android的生態算是比較完整的,而且網上95%的外掛文章,都是以移動端為主,對於不熟悉Windows開發的同學極度不友好。因此本篇文章我們不講Android端的實現,重點講Windows端的實踐,不過我也不是C++技術棧的,只能淺淺分享我踩過的坑。 1. 如何建立通訊通道? C++ // MethodChannel void XXXPlugin::RegisterWithRegistrar( flutter::PluginRegistrarWindows* registrar) { // 建立一個MethodChannel auto channel = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>( registrar->messenger(), "usb_tool", &flutter::StandardMethodCodec::GetInstance()); // 建立外掛物件 auto plugin = std::make_unique<XXXPlugin>(); // 把通道設定給外掛,同時傳入訊息的處理入口 channel->SetMethodCallHandler( [plugin_pointer = plugin.get()](const auto& call, auto result) { plugin_pointer->HandleMethodCall(call, std::move(result)); }); } ```C++ // EventChannel

// 建立事件流處理物件 auto eventHandler = std::make_unique< StreamHandlerFunctions>( plugin_pointer = plugin.get() -> std::unique_ptr> { return plugin_pointer->OnListen(arguments, std::move(events)); }, plugin_pointer = plugin.get() -> std::unique_ptr> { return plugin_pointer->OnCancel(arguments); }); // 建立EventChannel物件 auto eventChannel = std::make_unique>( registrar->messenger(), eventChannelName, &flutter::StandardMethodCodec::GetInstance()); // 把通道設定給外掛 eventChannel->SetStreamHandler(std::move(eventHandler)); 最後我們還需要把外掛註冊進專案中C++ registrar->AddPlugin(std::move(plugin)); ``` 2. 如何處理訊息? 在上面建立的過程中,其實已經把處理方法的傳遞給外掛了。

```C++ // MethodChannel的處理 // result即通訊的物件 void XXXPlugin::HandleMethodCall( const flutter::MethodCall& method_call, std::unique_ptr> result) { // 匹配通訊的介面 if (method_call.method_name().compare("getPlatformVersion") == 0) { std::ostringstream version_stream; version_stream << "Windows ";

    if (IsWindows10OrGreater()) {
        version_stream << "10+";
    }
    else if (IsWindows8OrGreater()) {
        version_stream << "8";
    }
    else if (IsWindows7OrGreater()) {
        version_stream << "7";
    }
            // 通過result->Succes回覆訊息
    result->Success(flutter::EncodableValue(version_stream.str()));
} else {
    result->NotImplemented();

}

} C++ // 主動向Flutter端傳送訊息 std::unique_ptr < flutter::StreamHandlerError> XXXPlugin::OnListen(const flutter::EncodableValue* arguments, std::unique_ptr>&& events) { // 主動傳送 events_.reset(events.release()); return nullptr; }

// Flutter取消監聽時觸發 std::unique_ptr < flutter::StreamHandlerError> UsbToolPlugin::OnCancel(const flutter::EncodableValue* arguments) { return nullptr; } ``` BasicMessageChannel我暫時還沒有用過,這裡就不做記錄了。但是看C++的api,還是很簡單就能找到的。至於Flutter端的,無需多言。只要通訊層連通了,其他想怎麼玩都可以。

Windows外掛的一些坑

這是本篇文章的重點。我們都知道Flutter是單執行緒的機制,來到原生平臺也一樣,Platform是執行在Flutter的主執行緒的,自然是不能做任何耗時的,不然會卡住主執行緒,系統會把我們認為無響應的應用,從而殺死應用。

我們經常會在使用windows外掛時,感覺點選卡頓,其實就是很多外掛沒有做這個處理,導致事件佇列等待排程。這主要是因為在windows的開發習慣上,耗時操作會丟到子執行緒非同步執行,然後主執行緒如何等待執行結果?使用while一直去查詢是否執行完成,這在windows上成為掛起。

不過一個有趣的現象是:當有耗時操作的時候,Flutter的動畫是可以流程播放的,但是點選事件卻卡住了,這時候C++的同學就會扯,你看動畫都是流程的,問題肯定出在Flutter上?其實是因為動畫在Flutter中屬於微任務,它的優先順序是高於事件佇列的。而while也是分配到事件佇列中,所以動畫優先執行,點選卻需要一直等到while結束。

在Android中,為了避免這個問題,我們一般會使用協程,把耗時操作丟給協程,讓系統幫我們進行任務排程,通過await拿到執行完之後的結果,再把結果返回給dart層。整個機制其實還是保留了flutter的單執行緒機制,從而避免了卡頓問題。

在Windows端,其實也有協程這個概念,比如WinRT、C++都有提供協程的能力。但問題在於協程這個東西,對於C++來說太新了,同時C++的歷史包袱實在太重,到現在還是用著很老版本的庫。這就導致很多C++的庫沒辦法遷移到協程這種方式,至少在我現在的業務中,切換成本極高,幾乎沒辦法完成。

但問題總得解決,目前我們主要使用非同步通知的方式,來解決這個問題。此非同步是真非同步,非flutter單執行緒任務排程的非同步。我們會把耗時的操作丟給子執行緒,但是我們不再通過while進行非同步轉同步,而是在子執行緒中,主動通過channel去通知會Dart層。 C++ if (*method == "getAsync") { async_pipe_stream_->Get(request, std::bind(&XXXPlugin::OnResponse, this, std::placeholders::_1, *uuid)); // 直接返回true,但真正的執行結果再OnResponse中主動返回 result->Success(EncodableValue(true)); return; } 在外掛的dart程式碼中,我們需要主動建立一個MethodChannel的接收器,非同步接收到後,通過執行業務端傳入的回撥通知回去。 ```Dart class NativePlugin { static const MethodChannel _channel = MethodChannel('com.open.flutter/xxx/xxx');

static NativePlugin? _instance;

// 獲取例項,單例 static NativePlugin getInstance({String defaultToken = _token}) { _instance ??= NativePlugin._internal(defaultToken); return _instance!; }

// 私有命名建構函式,做一次初始化 NativePlugin._internal(String defaultToken) { _defaultToken = defaultToken;

_channel.setMethodCallHandler((MethodCall call) async {
  if (call.method == 'onResponse') {
    final arguments = Map<String, dynamic>.from(call.arguments);
    // 執行業務端傳入的回撥
    await _onResponse(arguments);
  }
});

} ``` 外掛的Flutter層需要接收/維護回撥列表,不過此方式有隱患,傳入的回撥容易造成閉包問題,增加一些記憶體洩露的風險;但是對於沒辦法使用協程的C++外掛來說,此方案確實可以解決不少問題。親測可用的!

寫在最後

這篇文章,適合熟悉Flutter外掛開發,但是想接觸C++的同學學習討論。
此專欄從視窗管理、解析度適配、桌面小工具、專案框架、外掛編寫;下次我們講講如何進行打包!