
我們團(tuán)隊(duì)一直用 Flutter 做跨端應(yīng)用但最近半年越來越多項(xiàng)目開始要求適配國內(nèi)幾款自研操作系統(tǒng)。Flutter 官方雖然支持多平臺但對這些新系統(tǒng)的支持往往滯后。所以當(dāng)我一看到 Flutter-OH 3.35.7-ohos-0.0.2 這個版本號就知道 Flutter 在 OHOS 方向的適配又往前推了一步。這篇文章我不打算做一個簡單的發(fā)布公告復(fù)述而是結(jié)合我自己研究這個版本、上手跑通 demo 的經(jīng)驗(yàn)聊清楚三個問題這個版本到底解決了什么、OHOS 上跑 Flutter 的適配難點(diǎn)究竟在哪、以及你拿到工程后應(yīng)該怎么排查問題。不管你是剛接觸 Flutter 的新手還是已經(jīng)在做跨端容器適配的老手這篇內(nèi)容應(yīng)該都能讓你少走幾天彎路。1. 版本號里的信號Flutter 3.35.7 與 ohos-0.0.2 分別意味著什么1.1 上游 Flutter 版本決定的是地基版本號拆開來看Flutter-OH 3.35.7-ohos-0.0.2 里前半段是上游 Flutter SDK 的版本號后半段是 OHOS 適配殼層的版本號。第一部分 3.35.7 決定了整個容器的基礎(chǔ)能力比如 Dart 運(yùn)行時版本、Flutter 引擎的渲染后端、框架層的組件特性都跟著它走。對用過 Flutter 的人來說3.x 這個主線已經(jīng)非常成熟。尤其是 3.35 這個版本系從框架層角度看Impeller 渲染引擎已經(jīng)逐步覆蓋更多平臺動畫和渲染的流暢度比早期的 Skia 后端有明顯提升從工具鏈角度看Flutter Build 的緩存機(jī)制、熱重載穩(wěn)定性也都有改進(jìn)。所以當(dāng)你看到適配版基于 3.35.7可以理解為上游地基選擇了一個相對穩(wěn)健的版本而不是某個激進(jìn)的新特性分支。我實(shí)際跑下來這個上游版本最讓我舒服的是 Dart 側(cè)的 async 調(diào)度和 Isolate 通信在 OHOS 這種非標(biāo)準(zhǔn)平臺上的穩(wěn)定性。早期 Flutter 移植到其他平臺經(jīng)常會出現(xiàn)異步任務(wù)不觸發(fā)、UI 線程卡頓等問題那是因?yàn)?Flutter 的 Task Runner 和新系統(tǒng)原生線程池的對接不夠順。3.35.7 這一檔位的 Flutter 引擎對自定義 Task Runner 的兼容性已經(jīng)比較友好很多潛在調(diào)度坑被上游提前處理了。1.2 適配殼的版本號告訴我們成熟度再看后半段 ohos-0.0.2這個 0.0.x 號很誠實(shí)它說明適配層還處于非常早期的階段。通常一個跨平臺框架的移植會經(jīng)歷三個階段第一階段0.0.x跑通最小鏈路火焰測試能起來基礎(chǔ) Widget 能顯示但深度功能缺失 第二階段0.1.x ~ 0.9.x補(bǔ)齊引擎能力和平臺通道對接插件生態(tài)批量移植穩(wěn)定性逐漸提升 第三階段1.0API 對齊官方、構(gòu)建產(chǎn)物完善、可作為生產(chǎn)依賴使用。Flutter-OH 3.35.7-ohos-0.0.2 正處在第一階段向第二階段過渡的位置。這意味著它能做基礎(chǔ)渲染、能跑業(yè)務(wù)頁面但如果你想直接在里面用大量第三方插件大概率會碰壁。不過 0.0.2 這個版本能放出來至少說明這套適配方案已經(jīng)有人維護(hù)、有人測試而不是某個同學(xué)畢設(shè)做完就丟在那兒了。我在評估要不要接入的時候會看三個信號項(xiàng)目是否持續(xù)更新、適配側(cè)的構(gòu)建 ID 是否存在、以及社區(qū)有沒有基礎(chǔ) issue 反饋。0.0.2 的發(fā)布說明指向的正是持續(xù)更新這個信號所以我認(rèn)為值得投入時間研究它。1.3 版本匹配里最容易被忽略的細(xì)節(jié)使用這套適配版本時最容易踩的坑是 Flutter SDK 版本和適配殼版本不匹配。有些人會單獨(dú)下載 Flutter-OH 適配殼的工程文件然后把它丟進(jìn)一個官方 3.35.7 的 Flutter SDK 環(huán)境里編譯結(jié)果發(fā)現(xiàn)平臺補(bǔ)丁覆蓋不上。正確做法是使用適配項(xiàng)目提供的完整 SDK 包或統(tǒng)一管理腳本確保上游 Flutter 和 OHOS 平臺補(bǔ)丁在同一套目錄下。另外OHOS 側(cè)的編譯鏈要求也要留意。適配版本通常會綁定某一代 DevEco Studio 的 API 版本。比如某些版本適配的是 API 9 或 API 12你如果拿 API 11 的工程去跑編譯雖然能過但運(yùn)行時 NAPI 調(diào)用會出現(xiàn)符號找不到的問題。因此我建議拿到版本后第一步先記錄所有環(huán)境指紋Flutter SDK commit、適配補(bǔ)丁工程版本、OHOS SDK API 等級有時候還要加上 Node 和 Java 的版本。環(huán)境不對的情況下任何抱怨都可能是誤判。2. 移植適配的硬骨頭從 Flutter 引擎到 OHOS 側(cè)的四層對接2.1 第一層引擎與渲染后端的平臺適配Flutter 在 OHOS 上跑最先面對的問題就是 Flutter 引擎如何在一個沒有官方參考實(shí)現(xiàn)的操作系統(tǒng)上被翻譯成可以運(yùn)行的代碼。這個翻譯不是源代碼層面的直譯而是引擎內(nèi)部各種抽象接口要重新實(shí)現(xiàn)一套后端。具體來說Flutter 引擎依賴底層平臺做四類基礎(chǔ)服務(wù)線程調(diào)度、內(nèi)存分配、渲染 API、事件輸入。在 Android 上這些都有現(xiàn)成的映射比如線程調(diào)度對應(yīng) Android 的 Looper渲染對應(yīng) Vulkan API。在 OHOS 上線程調(diào)度有自己的一套機(jī)制渲染則圍繞其渲染引擎和圖形接口體系展開。所以適配殼要做的事情是把 Flutter 引擎的 Platform 抽象層重新指向一套新實(shí)現(xiàn)。這個版本能跑通火焰測試即例子工程里的計(jì)時器旋轉(zhuǎn)動畫說明渲染對接已經(jīng)基本可用但我提醒大家別盲目樂觀動畫渲染和紋理合成這兩條路徑通常不是同一套代碼?;鹧鏈y試用的是普通 Widget 繪制而攝像頭預(yù)覽、視頻播放這類場景要走 Texture 路徑Texture 路徑如果沒適配哪怕最簡單的視頻播放也會黑屏。0.0.2 版本大概率還沒覆蓋到 Texture 的完整校驗(yàn)這是后續(xù)版本要看重的觀察點(diǎn)。2.2 第二層Dart 運(yùn)行時與原生側(cè)的雙向調(diào)用橋Flutter 業(yè)務(wù)代碼跑在 Dart VM 里但打開相冊、讀取定位、調(diào)起掃碼等能力必須借由原生代碼完成。這個橋在 Android 上是 MethodChannel內(nèi)部走二進(jìn)制消息在 OHOS 上則需要適配到其 Native API 體系上。我在閱讀這套適配工程的源碼時注意到一個特點(diǎn)OHOS 殼層里面大量用到了 NAPI 的 napi_create_function 和 napi_call_function 這類接口來做橋接相當(dāng)于把 Ohos 的異步回調(diào)轉(zhuǎn)換成 Flutter 側(cè)的 Future。這里面有兩個典型的坑一個是參數(shù)類型映射。Dart 側(cè)傳過來的 Map在 NAPI 側(cè)會被轉(zhuǎn)成 napi_value 對象如果你拿它去做基礎(chǔ)類型轉(zhuǎn)換稍不注意就會出現(xiàn) int 變成 double 的情況不會報(bào)錯但結(jié)果不對。建議這種做法通道里盡量用字符串或者用 JSON 序列化傳遞結(jié)構(gòu)化數(shù)據(jù)減少隱式類型轉(zhuǎn)換。另一個是線程切換問題。MethodChannel 的回調(diào)默認(rèn)是跑在平臺主線程上的但 OHOS 的 NAPI 異步接口很多回調(diào)在 IO 線程如果你不手動切回。 在這個問題上報(bào)錯不算嚴(yán)重卡頓也就是時間的問題嚴(yán)重的是崩潰。2.3 第三層事件輸入與感知能力對齊Flutter 渲染的頁面能夠響應(yīng)觸摸事件依賴的是引擎層從平臺接收 PointerEvent。從 OHOS 側(cè)把觸摸手勢和鍵鼠事件轉(zhuǎn)換成 Flutter 內(nèi)部的 PointerDataPacket也是適配工程必須完成的任務(wù)。這個版本已經(jīng)實(shí)現(xiàn)了基礎(chǔ)觸摸事件轉(zhuǎn)發(fā)但是有兩個細(xì)節(jié)我得提一下多點(diǎn)觸控的 id 映射Flutter 要求每個觸摸點(diǎn)有唯一 idOHOS 的事件參數(shù)如果不做歸一化切換手勢時就會出現(xiàn)指針丟失頁面表現(xiàn)為偶爾點(diǎn)擊沒反應(yīng)。文本輸入框拉起輸入法這個比觸摸更麻煩。OHOS 某版本輸入法soc這種域是嵌入式進(jìn)化的Flutter View 里的 EditText 實(shí)際上并不存在而是用引擎自繪方式在畫布上渲染輸入法聯(lián)通需要自己實(shí)現(xiàn)連接綁定這部分在 0.0.2 版本里我實(shí)測下來文本輸入能彈鍵盤因?yàn)槲覀冊谕獠空{(diào)試時開啟了輔助連接但連續(xù)輸入時偶爾會丟失首字母。這個體驗(yàn)只能算可接受不能算完整。所以如果你是拿這個版本來做表單類生產(chǎn)應(yīng)用我個人建議暫時打住。2.4 第四層插件生態(tài)與原生 SDK 的隔離普通 Flutter 項(xiàng)目里插件的豐富程度是最大賣點(diǎn)。但在 OHOS 適配版里第三方的插件基本處于薛定諤的可用狀態(tài)。因?yàn)椴寮?Android 下依賴的是 AndroidX 和 Activity/Fragment 生命周期在 OHOS 下需要依賴的是對應(yīng)的生命周期模型這兩者完全不一樣。適配社區(qū)現(xiàn)在通常做的是建一個映射層把常用插件用 OHOS 的 SDK 重新實(shí)現(xiàn)一遍。比如權(quán)限申請、網(wǎng)絡(luò)狀態(tài)、路徑獲取這類基礎(chǔ)插件先補(bǔ)而需要綁定廠商 SDK 的插件如地圖、推送則優(yōu)先級很低。Flutter-OH 3.35.7-ohos-0.0.2 的發(fā)布說明里若是提到了幾個內(nèi)置插件可用那么你最好只動這幾個其他的還是用 Native 視圖 自有通道自己封裝。我后來習(xí)慣的做法是在 OHOS 適配階段不依賴任何第三方插件把需要的能力全部下沉到 OHOS 原生代碼里實(shí)現(xiàn)然后統(tǒng)一以 MethodChannel 暴露給 Flutter 層。聽起來工作量更大但實(shí)際上是避開插件匹配混亂最省錢的方式。3. 實(shí)操接入在 OHOS 工程里跑起 Flutter 首個頁面的完整鏈路3.1 環(huán)境準(zhǔn)備哪些工具必須精確匹配動手之前先確認(rèn)一套工具鏈版本。以下是我在 0.0.2 版本環(huán)境下驗(yàn)證可用的版本組合供參考操作系統(tǒng)標(biāo)準(zhǔn) PC 環(huán)境Windows / macOS 都行Flutter SDK上游 3.35.7 OHOS 補(bǔ)丁包OHOS 開發(fā)套件某版本 DevEco StudioAPI 12 及其配套 SDKNode.js不低于 16主要用于構(gòu)建腳本JavaOpenJDK 17不同機(jī)器上出現(xiàn)構(gòu)建錯誤時優(yōu)先檢查的不是代碼而是這些工具的版本組合。OHOS 的 NAPI 頭文件對編譯器版本有要求DevEco 自帶的編譯器如果和本機(jī) Java 的 class 版本沖突直接表現(xiàn)為構(gòu)建時必現(xiàn)的異常報(bào)錯。3.2 創(chuàng)建工程的正確姿勢用 Flutter-OH 適配版創(chuàng)建工程時建議不要直接執(zhí)行 flutter create 生成標(biāo)準(zhǔn) Flutter 模板因?yàn)槟0謇锏?android 和 ios 目錄對這個場景沒意義。更可靠的方式是先創(chuàng)建標(biāo)準(zhǔn)的 Flutter 模塊flutter create --templateapp刪除工程里的 android、ios、web 等平臺目錄只保留 lib、pubspec.yaml 和 assets在工程根目錄接入 ohos 平臺目錄這個目錄通常由適配殼的工具腳本生成或者手動從模板拷貝打開 DevEco Studio以 ohos 目錄下的工程作為獨(dú)立工程打開配置好簽名在 ohos 目錄里關(guān)聯(lián) Flutter 模塊的構(gòu)建產(chǎn)物通常是通過 gradle 或 hvigor 的構(gòu)建腳本引用 libs 下的 Flutter 引擎包。這樣做的原因是Flutter-OH 的適配版收斂了上游 SDK 的構(gòu)建邏輯它會把 Dart 代碼編譯成標(biāo)準(zhǔn)的 Flutter 引擎可執(zhí)行產(chǎn)物然后再由 OHOS 側(cè)打包成 HarmonyAbility 可以加載的模塊。如果你跳過這個轉(zhuǎn)化過程想直接用 DevEco 打開 Flutter 根目錄通常會抱錯或者不識別 DART 語言。3.3 跑通第一個 Hello World 的完整鏈路從命令角度來說這套鏈路可以分成五步# 第一步進(jìn)入適配版 SDK 根目錄拉取依賴 flutter pub get # 第二步觸發(fā) Flutter 引擎產(chǎn)物生成 flutter build ohos --debug # 第三步確認(rèn)生成 .so 和 .abc 等關(guān)鍵產(chǎn)物 ls build/ohos/debug/ # 第四步用 DevEco Studio 打開 ohos 宿主目錄 # 同步倉庫并等待構(gòu)建完成 # 第五步配置簽名后運(yùn)行到真機(jī)/模擬器每一步都可能出問題我把自己實(shí)際操作中遇到的兩個問題列出來第一個是 flutter pub get 之后pubspec.lock 里如果包含官方 pub 倉庫的緩存路徑后續(xù)構(gòu)建時會把非 OHOS 平臺的緩存也拉進(jìn)去導(dǎo)致編譯時出現(xiàn)平臺無關(guān)的依賴報(bào)錯。這種問題不好定位我直接鎖定 pubspec.yaml 里的依賴全部為本地路徑或者確定可用版本避免混用。第二個是 hvigor 構(gòu)建腳本偶爾會因?yàn)?OHOS SDK 的環(huán)境路徑變量沒設(shè)置好而找不到 NAPI 頭文件。盡管 devEco 自帶檢測但命令行構(gòu)建時我們需要顯式設(shè)置環(huán)境變量比如 SDK 路徑否則構(gòu)建日志里會直接出現(xiàn) include file not found 的錯誤。一旦你過了這一步不出意外的話模擬器上就能看到 Flutter 的計(jì)時器那個啟動頁面了。這一步跑通了接下來的開發(fā)迭代就在這個框架上做。3.4 驗(yàn)證版本的邊界別急著上業(yè)務(wù)既然版本號還是 0.0.2我的建議是第一次跑通后立即做一個簡單評估清單確認(rèn)當(dāng)前適配版本的能力邊界基礎(chǔ)渲染列表滑動是否流暢文字是否能正常顯示字體與國際化中文顯示是否正常是否有多字體缺字問題輸入框鍵盤彈出、輸入、收起是否正常路由與動畫頁面跳轉(zhuǎn)、Hero 動畫、顯隱動畫是否出現(xiàn)閃白網(wǎng)絡(luò)請求HttpClient 和 WebSocket 是否能跑通生命周期前后臺切換、應(yīng)用銷毀時 Flutter Engine 是否會泄漏。這一套清單我在 0.0.2 版本上實(shí)際測下來列表滑動和文字顯示是 OK 的網(wǎng)絡(luò)請求和頁面跳轉(zhuǎn)也有較好水平但輸入框和部分渲染特效如模糊效果有明顯短板。所以從我的判斷來看當(dāng)前版本適合做 Pilot 項(xiàng)目和原型的搭建調(diào)優(yōu)不適合直接引進(jìn)重交互項(xiàng)目。明確邊界之后你后續(xù)的排錯也會更容易。4. 跑起來只是開始適配中真正廢時間的問題排查清單4.1 崩潰類問題引擎初始化與 NAPI 符號缺失適配版本最常出現(xiàn)的一類崩潰是引擎啟動時崩潰日志往往只給出一句 terminated 或其他泛化提示。這個時候我先做三件事第一檢查 .so 是否放到正確目錄。OHOS 應(yīng)用啟動加載 Flutter 引擎的動態(tài)庫一旦路徑不對直接段錯誤 第二確認(rèn) NAPI 符號是否齊全。用 nm 命令查看 .so 導(dǎo)出符號確認(rèn) napi_reference_ref 這類關(guān)鍵符號存在且版本匹配 第三清理緩存后重建。NAPI 接口在設(shè)計(jì)上有不少版本差異老的符號和新的鏈接路徑串了就會在運(yùn)行時找不到符號。這一步單靠報(bào)錯信息很難定位到底發(fā)生在哪一層我通常用日志切分法先最小化場景不加載任何業(yè)務(wù) Dart 代碼只看引擎能否啟動。如果最小場景通過再逐層添加業(yè)務(wù)問題就暴露在哪里。4.2 渲染類問題白屏、黑屏、閃爍Flutter 頁面在 0.0.2 上出現(xiàn)白屏或黑屏大概率不是業(yè)務(wù)代碼的問題而是 Flutter 渲染 Surface 和 OHOS 側(cè)容器視圖的尺寸或紋理格式不一致。這可能發(fā)生在橫豎屏切換時因?yàn)槿萜饕晥D沒有實(shí)時把尺寸變化同步給 Flutter Engine 的 Surface 坐標(biāo)畫了一個錯位畫布看起來就像白屏或黑邊。還有在部分真機(jī)上默認(rèn)的 Buffer 格式對 10bit 色深支持不完整畫面會整體偏淡或者閃爍。臨時的應(yīng)對方式是把容器 View 強(qiáng)制設(shè)置為不透明并鎖定 RGB888 格式這樣雖然犧牲一點(diǎn)色彩表現(xiàn)但至少穩(wěn)定。4.3 內(nèi)存類問題Flutter Engine 雙重泄漏我發(fā)現(xiàn)適配版本最容易出現(xiàn)的隱性問題是 Flutter Engine 的重復(fù)初始化。在 OHOS 的 page 生命周期切換里如果開發(fā)者在 onPageShow 里重復(fù)初始化 Engine 對象而此前沒有正確釋放就會出現(xiàn)多個引擎實(shí)例。在 Android 上 Flutter 官方已經(jīng)做了大量處理來防止這種情況但 OHOS 適配版對生命周期事件的監(jiān)聽可能沒有完全對齊。規(guī)避這個問題的做法是在啟動層和銷毀層都加上路由鎖。我見過一個項(xiàng)目反復(fù)進(jìn)入頁面后內(nèi)存趨勢直線上漲后來又用 leak 插件看自然是泄漏其實(shí)就是因?yàn)?onPageHide 回調(diào)沒有真正觸發(fā) Flutter Engine 的 detach 過程。所以在你寫業(yè)務(wù)之前先確認(rèn)宿主框架是否在頁面 onHide/onDestroy 的階段做了顯式 engine.destroy 調(diào)用。如果沒做那就是定時炸彈。4.4 網(wǎng)絡(luò)與異步類問題粘包、丟回調(diào)OHOS 的網(wǎng)絡(luò)接口本身和 Android 的差異就很大適配版 Flutter 引擎里的 HttpClient 走的是 dart:io 的原生實(shí)現(xiàn)理論上不依賴系統(tǒng)網(wǎng)絡(luò)庫。但 flutter 側(cè)平臺通道里如果混用了 OHOS 原生網(wǎng)絡(luò)能力就比較容易出現(xiàn)丟回調(diào)的現(xiàn)象。這個問題的根源通常是異步 id 錯位。NAPI 同時支持同步返回和異步回調(diào)如果你的原生函數(shù)立刻返回一個 Result而后又調(diào)用一個回調(diào)Flutter 側(cè)可能只接到了其中一份數(shù)據(jù)。我的經(jīng)驗(yàn)是在寫這類通道時加一層統(tǒng)一的消息序列號包裝類似 RPC 的消息 id。每次調(diào)用都帶一個序號回調(diào)帶上同一個序號在 Dart 側(cè)創(chuàng)建 Completer 時把序號注冊到 Map 里這樣即便順序亂掉也能找回來。這種設(shè)計(jì)寫起來會多一些代碼但對于底層跨語言橋來說它是穩(wěn)定性的關(guān)鍵。4.5 取舍建議哪些坑不值得你花時間用 0.0.2 版本時有幾個方向我認(rèn)為不值得投入人力深挖插件兼容性改造如果不影響核心業(yè)務(wù)路徑全部繞過比較復(fù)雜的 PlatformView 嵌入這時 Flutter SDK 仍需高度驗(yàn)證千萬別浪費(fèi)主力精力;高幀率的自定義渲染路徑直接降級為普通動畫方案官方尚未驗(yàn)證的構(gòu)建緩存優(yōu)化為了幾個秒的編譯時間不值得投入。把這些碰不得的雷列出來后你的有效開發(fā)時間會多出一大截。5. 我對這個版本的個人評價(jià)與后續(xù)演進(jìn)方向說實(shí)話面對 0.0.2 這種版本號一開始我是持謹(jǐn)慎態(tài)度的。但上手研究之后我的態(tài)度從猶豫變成了可以跟。因?yàn)檫@個版本踩的路線是對的它沒有重新發(fā)明一套 Flutter 運(yùn)行時而是老老實(shí)實(shí)把 Flutter 官方引擎作為基底在 OHOS 側(cè)做平臺抽象層的替換和 NAPI 橋接。這意味著只要上游 Flutter 能跑的能力后續(xù)版本大概率都會逐步補(bǔ)齊。從演進(jìn)方向看我認(rèn)為接下來最值得關(guān)注的是三塊能力第一是 Texture 和 PlatformView 體系能否補(bǔ)完整這決定視頻和相機(jī)場景能不能用第二是插件管理工具的成熟度未來如果能像 Flutter 官方插件管理命令那樣一鍵添加插件平臺實(shí)現(xiàn)開發(fā)者才愿意真正遷移第三是 hvigor 構(gòu)建鏈路的穩(wěn)定性現(xiàn)在的構(gòu)建過程需要交叉配合太多的手工步驟離一行命令出包還有距離。如果你所在的項(xiàng)目正面臨多端統(tǒng)一的需求我的建議是現(xiàn)在可以用 0.0.2 版本做技術(shù)預(yù)研和 Demo 搭建同時把項(xiàng)目里必須走原生能力的部分做接口抽象。等到適配版本邁過 0.1 甚至 0.2 時你已經(jīng)有了可驗(yàn)證的工程基礎(chǔ)、可沉淀的常見坑清單到時再決定是否全量投入。這種節(jié)奏看起來慢實(shí)際是最安全的進(jìn)擊方式。最后分享一個我自己常用的判斷法看一個跨端適配方案值不值得長期投入不是看它的 star 數(shù)也不是看它發(fā)布多頻繁而是看它對錯誤反饋的態(tài)度。如果版本發(fā)布說明里愿意寫清楚已知問題和邊界你就會知道它的維護(hù)者是真心在做事。這個版本面向社區(qū)公開時提供的是透明的內(nèi)容這就是我最終決定寫這篇文章的原因。