用遷移 OpenHarmony 實戰(zhàn):數(shù)學(xué)練習(xí)應(yīng)用開發(fā)與調(diào)試)
這兩年我一直有意把一些跨平臺項目往 OpenHarmony 上搬剛好遇到一個適合練手的場景數(shù)學(xué)練習(xí)。大概背景是我手上有一份用 React Native 寫的口算練習(xí) Demo之前在 iOS 和 Android 上都跑得很順后來一次技術(shù)交流中看到 React Native for OpenHarmony 的適配層已經(jīng)能支撐常見業(yè)務(wù)組件就決定拿它做一次完整遷移實戰(zhàn)。這篇文章把整個過程拆開講從工程初始化、功能實現(xiàn)到真機調(diào)試覆蓋我自己踩過也估計你們大概率會踩的坑。適合三類人看已經(jīng)在 RN 上寫過業(yè)務(wù)、想了解它能不能跑 OpenHarmony 的開發(fā)者剛接觸 OpenHarmony、需要快速上手跨平臺方案的開發(fā)者以及單純想找一個工具型 App 完整落地案例的學(xué)生。1. 項目定位與整體拆解1.1 數(shù)學(xué)練習(xí)這個場景為什么適合做實驗田先說業(yè)務(wù)背景。我要做的不是那種復(fù)雜題庫系統(tǒng)而是面向小學(xué)低年級的口算訓(xùn)練工具隨機出加減乘除題孩子輸入答案系統(tǒng)判斷對錯一輪做完給正確率和用時錯題自動進錯題本。這類應(yīng)用有個特點業(yè)務(wù)邏輯幾乎都集中在計算和狀態(tài)流轉(zhuǎn)上UI 以文本、按鈕、輸入框為主不需要地圖、相機、高性能渲染這些重度原生能力。換句話說它是典型的“邏輯驅(qū)動型”應(yīng)用非常適合驗證一個跨平臺框架在一個新系統(tǒng)上的成熟度。如果把應(yīng)用換成視頻剪輯或者帶大量自定義畫布的工具那 RN 適配層的壓力會大很多很多原生能力可能還沒鋪齊容易把框架短板全部暴露出來。數(shù)學(xué)練習(xí)不一樣它需要的組件無非是 View、Text、TextInput、FlatList、TouchableOpacity適配層只要把這些映射穩(wěn)了開發(fā)體驗和運行效果就不會差。所以我說它是 OpenHarmony 上做 RN 遷移的理想“小白鼠”項目。還有一個隱性好處教育類工具對 UI 動態(tài)反饋要求高比如答對時背景閃綠、答錯時抖動這些動畫用 RN 的 Animated 或 LayoutAnimation 就能處理不需要深入 ArkUI 寫自定義動畫。這樣我可以把精力放在驗證跨平臺邏輯上而不是和系統(tǒng) UI 框架死磕。1.2 為什么不是直接用原生 ArkUI 開發(fā)這個問題我在動手前問過自己。如果團隊里已經(jīng)有人熟悉 OpenHarmony 原生開發(fā)直接上 ArkUI 肯定是最穩(wěn)的畢竟沒有中間層損耗。但我這邊的現(xiàn)實情況是核心業(yè)務(wù)代碼已經(jīng)用 React Native 寫好團隊對 RN 生態(tài)也更熟ArkUI 雖然聲明式寫起來不復(fù)雜但為了一個工具型應(yīng)用重新維護一套原生代碼成本并不劃算。更重要的是React Native for OpenHarmony 的適配方式不是套個 WebView 的殼而是把 RN 的 JS 邏輯跑在系統(tǒng)自帶的 JS 引擎上再通過適配層把虛擬節(jié)點映射成 OpenHarmony 的原生組件。換句話說業(yè)務(wù)層寫的是 TypeScript / JavaScript渲染層走的是原生組件不是 HTML 頁面。這保證了性能下限和原生交互體驗和傳統(tǒng)的 Hybrid 方案有本質(zhì)區(qū)別。當(dāng)然跨平臺不是免費的。適配層目前還沒有覆蓋 iOS / Android 上的全部 API尤其是一些偏門原生模塊可能需要我寫自定義原生橋接。而數(shù)學(xué)練習(xí)這個項目基本不碰這些偏門能力所以我只需要關(guān)注 RN 標(biāo)準(zhǔn)組件在 OpenHarmony 上的行為差異成本可控。1.3 功能范圍與版本規(guī)劃我給自己定的第一個版本功能清單如下模塊功能點優(yōu)先級出題引擎隨機生成加減乘除題支持難度分級P0答題交互輸入答案、提交、即時反饋P0練習(xí)會話每輪 10 題計時完成后統(tǒng)計正確率P0錯題本自動記錄錯題支持重新練習(xí)P1歷史成績本地保存最近 20 條練習(xí)記錄P1設(shè)置頁選擇題型范圍、題目數(shù)量、是否計時P2我不建議第一個版本就把家長端、老師端、云端同步全做進去那會沖淡“驗證 RN on OpenHarmony”這個主目標(biāo)。先把單機版跑通再把 P1、P2 功能加上整個項目節(jié)奏會舒服很多。2. 環(huán)境準(zhǔn)備與工程骨架搭建2.1 工具鏈準(zhǔn)備與版本搭配OpenHarmony 應(yīng)用開發(fā)繞不開 DevEco Studio這是官方 IDE工程構(gòu)建、簽名、真機安裝都靠它。RN 側(cè)則需要 Node.js 環(huán)境以及社區(qū)提供的 OpenHarmony 適配腳手架和 npm 依賴包。動手之前我建議先把以下工具裝齊Node.js建議 LTS 版本我這里用的是 18后面跑 Metro 和打包都要靠它DevEco Studio用當(dāng)前正式發(fā)布版本即可OpenHarmony SDK在 DevEco Studio 里通過 SDK Manager 下載一臺 OpenHarmony 真機或模擬器模擬器調(diào)試方便但有些硬件能力測不了。版本搭配是最容易被忽略的坑。RN 社區(qū)迭代快適配層一般會跟著 RN 版本走比如某個適配版本只支持某個 RN 大版本。我一開始圖省事直接裝最新版 RN結(jié)果適配包還沒跟上最后只能降級到受支持的版本。建議先查一下適配包說明里寫的“已支持 RN 版本范圍”再決定初始化用哪個模板別盲目追新。2.2 初始化工程把 RN 工程和 OpenHarmony 工程串起來這里有兩種路徑一種是從零創(chuàng)建用社區(qū)腳手架直接生成同時包含 RN 和 OpenHarmony 殼的工程另一種是已有 RN 工程再補一個 OpenHarmony 工程接入。我這次因為手頭有舊 Demo走的是第二種。從零創(chuàng)建的大致命令是這樣以我用的腳手架版本為例具體命令名以你拉到的版本為準(zhǔn)npx react-native-ohos/generator-cli init MathPracticeApp --template react-native-ohos/template執(zhí)行完會生成一個同時包含harmony目錄和標(biāo)準(zhǔn) RN 目錄的工程。如果已有 RN 工程需要手動在工程里加入 OpenHarmony 殼工程并把node_modules/react-native-ohos/...下對應(yīng)的底層庫引入殼工程。初始化完成后的第一件事不是急著寫業(yè)務(wù)代碼而是先跑一個空模板確認 “RN 的 JS 層打包出來的 bundle 能被 OpenHarmony 殼加載”。這個環(huán)節(jié)通了后面所有開發(fā)才有意義。我當(dāng)時第一次跑的時候卡在簽名上沒簽名裝不到真機模擬器又不穩(wěn)定折騰了小半天才把鏈路打通。2.3 工程結(jié)構(gòu)與目錄說明生成后的工程結(jié)構(gòu)大致長這樣MathPracticeApp ├── App.tsx # RN 業(yè)務(wù)入口 ├── harmony # OpenHarmony 殼工程 │ ├── entry │ │ ├── src/main/ets # 原生入口和橋接代碼 │ │ └── src/main/resources # 應(yīng)用圖標(biāo)、字符串等資源 │ ├── hvigorfile.ts # 構(gòu)建腳本 │ └── oh-package.json5 # 原生依賴 ├── index.js ├── package.json └── tsconfig.json理解這個結(jié)構(gòu)的關(guān)鍵是RN 代碼是業(yè)務(wù)主體harmony目錄只是一個殼負責(zé)把應(yīng)用跑在 OpenHarmony 系統(tǒng)上并提供 JS 引擎和渲染橋接。日常開發(fā)絕大部分時間在App.tsx和 src 目錄下的業(yè)務(wù)組件里度過不太需要動原生殼。有一點需要注意不要隨便改harmony目錄下的構(gòu)建文件名和包名配置。包名bundleName是 OpenHarmony 應(yīng)用的唯一標(biāo)識牽扯到安裝和更新我見過有人改了 bundleName 后舊數(shù)據(jù)讀不到排查半天才發(fā)現(xiàn)是包名變了AsyncStorage 的數(shù)據(jù)跟著失效。3. 數(shù)學(xué)練習(xí)核心功能設(shè)計與實現(xiàn)3.1 題目生成器避開這幾個數(shù)學(xué)坑數(shù)學(xué)練習(xí)的靈魂是出題??此坪唵螌嶋H需要考慮三個原則不能出現(xiàn)“不夠減”的減法、除法必須整除、不同運算符難度要可控。如果忽略這些二年級小朋友練到一半發(fā)現(xiàn) 3 - 8 -5或者 7 ÷ 2 3.5產(chǎn)品就沒法用了。我實現(xiàn)了一個可配置的題目生成器核心思路是先確定運算規(guī)則再根據(jù)規(guī)則生成合法數(shù)字interface Problem { id: string; left: number; right: number; operator: | - | × | ÷; answer: number; expression: string; } function generateProblem( maxNumber: number, operator: | - | × | ÷, ): Problem { const left Math.floor(Math.random() * (maxNumber 1)); const right Math.floor(Math.random() * maxNumber) 1; if (operator -) { // 保證結(jié)果不為負 const a Math.max(left, right); const b Math.min(left, right); return { id: ${Date.now()}-${Math.random()}, left: a, right: b, operator, answer: a - b, expression: ${a} - $, }; } if (operator ÷) { // 通過“被除數(shù) 除數(shù) * 商”來構(gòu)造整除題 const divisor right; const quotient Math.floor(Math.random() * (maxNumber / divisor)) 1; const dividend divisor * quotient; return { id: ${Date.now()}-${Math.random()}, left: dividend, right: divisor, operator, answer: quotient, expression: ${dividend} ÷ ${divisor}, }; } // 加法和乘法直接算即可 const answer operator ? left right : left * right; return { id: ${Date.now()}-${Math.random()}, left, right, operator, answer, expression: ${left} ${operator} ${right}, }; }減法的關(guān)鍵是用Math.max和Math.min把兩個數(shù)對調(diào)從根上避免負數(shù)。除法的關(guān)鍵是用divisor * quotient反推出被除數(shù)而不是隨機出兩個數(shù)再用浮點數(shù)相除。順便提一句Math.random()的隨機性足夠出題用不需要用加密級隨機數(shù)別過度設(shè)計。出題引擎寫完后我建議花十分鐘補幾個邊界測試比如maxNumber5時連續(xù)生成 200 題確認沒有結(jié)果超范圍除法題確認所有answer都是整數(shù)。這些測試雖然簡單但能攔住后面改代碼時不小心的回歸。3.2 答題交互與校驗邏輯答題界面這邊最需要小心的是輸入框和鍵盤。數(shù)學(xué)練習(xí)只需要數(shù)字輸入所以 TextInput 要設(shè)置keyboardTypenumeric。但注意在 OpenHarmony 的原生鍵盤適配還不是很完美的情況下numeric有時候會調(diào)起支持小數(shù)點的鍵盤孩子誤按小數(shù)點了就容易提交非法內(nèi)容。我的校驗邏輯是把用戶輸入先用Number(input)轉(zhuǎn)換再用Number.isNaN排除異常值最后比較答案時用一個極小誤差閾值。為什么不用parseInt因為parseInt(12abc)會返回 12把用戶輸入的臟數(shù)據(jù)當(dāng)合法值處理了不適合做嚴格校驗。function checkAnswer(problem: Problem, input: string): boolean { const value Number(input); if (Number.isNaN(value)) { return false; } return Math.abs(value - problem.answer) 1e-6; }提交后的反饋也要設(shè)計好。我這里做了三檔答對了按鈕變成綠色提示“正確”答錯了按鈕抖一下顯示正確答案空提交直接忽略不判對也不判錯。這里有個交互細節(jié)空提交不該消耗一次答題機會否則孩子誤觸就白白錯了會打擊積極性。3.3 練習(xí)輪次與難度配置練習(xí)過程我定義成三個狀態(tài)ready準(zhǔn)備開始、doing答題中、finished本輪結(jié)束。從doing到finished的切換條件是當(dāng)前題號達到總題數(shù)而不是用一個單獨的“結(jié)束”按鈕。這個設(shè)計更貼近實際使用孩子把最后一道題做完結(jié)果頁直接出現(xiàn)中間不需要額外操作。難度配置放在設(shè)置頁影響的是題目生成器參數(shù)。比如難度最大數(shù)字可用運算符入門10、-基礎(chǔ)20、-、×進階100、-、×、÷配置信息會在每次生成題目時傳入但我建議在做題中途不改難度。最簡單的方式是每次點“開始”時讀取一次配置鎖定到本輪上下文里。如果你用全局變量存難度做題中途孩子去設(shè)置頁切了難度當(dāng)前這輪題目就亂了。4. 狀態(tài)管理與數(shù)據(jù)持久化4.1 練習(xí)會話的狀態(tài)建模useState 還不夠一開始我用三個useState分別存當(dāng)前題、答對數(shù)、計時器結(jié)果發(fā)現(xiàn)代碼很亂提交答案時需要同時更新題號、正確數(shù)、答案列表三個 setState 經(jīng)常漏一個。后來我把一輪練習(xí)的所有狀態(tài)合并成一個對象用useReducer管理思路立刻清晰了。type SessionState { index: number; problems: Problem[]; answers: (string)[]; correctCount: number; startTime: number; elapsedSeconds: number; }; type Action | { type: START; problems: Problem[] } | { type: SUBMIT_ANSWER; answer: string; isCorrect: boolean } | { type: FINISH };SUBMIT_ANSWER時把index 1、correctCount、answers一并更新結(jié)果頁只需要讀取這一個 state 對象UI 和邏輯完全對應(yīng)。不要小看這一步跨平臺項目最怕的就是業(yè)務(wù)狀態(tài)分散在多個useState里最后調(diào) UI 時互相打架。4.2 AsyncStorage 存取練習(xí)記錄OpenHarmony 適配層提供了 AsyncStorage 的兼容實現(xiàn)接口和 RN 端一致因此業(yè)務(wù)代碼不需要針對平臺寫差異。我用它存兩樣?xùn)|西歷史成績列表和錯題記錄。存儲層我做了一個很薄的封裝統(tǒng)一處理讀取、寫入、JSON 序列化import AsyncStorage from react-native-async-storage/async-storage; const RECORD_KEY math_practice_records; export async function loadRecords(): PromisePracticeRecord[] { const raw await AsyncStorage.getItem(RECORD_KEY); if (!raw) return []; try { const parsed JSON.parse(raw); return Array.isArray(parsed) ? parsed : []; } catch (e) { return []; } } export async function appendRecord(record: PracticeRecord): Promisevoid { const list await loadRecords(); list.push(record); // 只保留最近 20 條避免內(nèi)容無限膨脹 const trimmed list.slice(-20); await AsyncStorage.setItem(RECORD_KEY, JSON.stringify(trimmed)); }這里有個容易踩的坑AsyncStorage 的讀寫是異步的如果連續(xù)快速調(diào)用可能出現(xiàn)后寫覆蓋先寫的情況。我在提交本輪結(jié)果時只用appendRecord一次不在循環(huán)里頻繁調(diào)用。另外JSON 解析失敗要返回空數(shù)組不能直接 throw否則歷史記錄一壞整個 App 啟動就崩了。4.3 錯題本不只是記一句“做錯了”錯題本如果只存“3 5 做錯了”后續(xù)重練根本沒有可用的上下文。要保證錯題本有價值每條記錄必須包含題目表達式、正確答案、用戶當(dāng)時的錯誤答案、出錯時間。這樣才能做到錯題重練時按原題復(fù)現(xiàn)。我的錯題記錄結(jié)構(gòu)interface WrongRecord { id: string; expression: string; correctAnswer: number; wrongAnswer: string; timestamp: number; }錯題重練和普通練習(xí)共用一個出題引擎但參數(shù)不同直接給定expression和答案不再走隨機生成器。這個復(fù)用比單獨寫一套“錯題練習(xí)”邏輯省事得多新功能也就一個頁面的事。有一點要特別提醒錯題本存儲必須做去重。孩子同一道題錯三次不要存三條而是更新原記錄的timestamp和wrongAnswer。我判斷重復(fù)的標(biāo)準(zhǔn)是expression correctAnswer相同就合并。否則跑一個月后錯題本會膨脹到?jīng)]法看。5. 原生能力銜接與性能優(yōu)化5.1 可以接入的原生能力與接入方式數(shù)學(xué)練習(xí)不依賴攝像頭、定位這類敏感權(quán)限但還是有兩個原生能力值得接入震動和語音報題。答對時輕輕震一下答錯時長震動這個反饋對低齡用戶非常友好語音報題則可以讓孩子不用盯著屏幕讀題聽題作答。震動在 OpenHarmony 適配層里可以通過橋接調(diào)用原生模塊實現(xiàn)但 RN 原生模塊在 OpenHarmony 上的注冊方式和 Android 不完全一樣需要在harmony/entry/src/main/ets里寫一個原生模塊再在 JS 側(cè)調(diào)用。這部分代碼量不大我建議放到業(yè)務(wù)功能全部跑通之后再做前期別讓原生橋接干擾主流程。老實說如果你只做單機練習(xí)不接語音和震動也完全夠用。但如果你也想驗證一下 RN 原生橋接在 OpenHarmony 上的完整鏈路那值得試一次畢竟這是跨平臺方案繞不開的話題。5.2 bundle 打包與首屏優(yōu)化RN 應(yīng)用在 OpenHarmony 上有兩種加載 bundle 的模式開發(fā)模式連 Metro運行時按需拉取發(fā)布模式把 bundle 打包進 hap 包安裝后離線加載。首屏優(yōu)化最關(guān)鍵的是把發(fā)布模式的預(yù)打包做對。我的做法是先在工程里配置好assets/index.bundle的輸出路徑然后在構(gòu)建 hap 之前先執(zhí)行打包腳本讓 JS bundle 進到entry/src/main/resources/rawfile下。這個過程有點繞如果發(fā)現(xiàn)真機上長時間白屏優(yōu)先懷疑 bundle 文件有沒有真的打進包里。另一個優(yōu)化點是拆分“首屏等待”。數(shù)學(xué)練習(xí)首頁啟動時需要讀本地設(shè)置如果每次都在頁面上 await用戶會看到白屏。我改成先渲染默認設(shè)置再異步加載本地配置并更新界面。雖然差了零點幾秒但啟動感知差別明顯。這套思路在 RN for OpenHarmony 上同樣有效因為它不涉及原生性能調(diào)優(yōu)而是從用戶體驗出發(fā)做取舍。5.3 列表卡頓與渲染優(yōu)化歷史成績和錯題本都用 FlatList 實現(xiàn)。FlatList 在 RN on OpenHarmony 上能用但需要注意三件事第一每條列表項的數(shù)據(jù)結(jié)構(gòu)要盡量扁平不要在 renderItem 里做復(fù)雜計算。比如歷史成績里的“正確率”和“時長”字段我是在寫入記錄時就算好存下來渲染時直接讀字段不對源數(shù)據(jù)做二次計算。第二必須設(shè)置穩(wěn)定且唯一的keyExtractor。我用的是記錄里的id字段不用數(shù)組 index。原因很簡單列表項如果只是 index插入或刪除時會引發(fā)整頁重渲染OpenHarmony 上的表現(xiàn)會更敏感。第三避免在renderItem里使用內(nèi)聯(lián)箭頭函數(shù)。寫成renderRecord{this.renderRecord}而不是renderItem{() Item/}這樣列表復(fù)用節(jié)點時才能命中優(yōu)化路徑。這個點在 Android 上不明顯但我在 OpenHarmony 上測試時內(nèi)聯(lián)函數(shù)和高頻刷新同時出現(xiàn)滾動確實會掉幀明顯。6. 真機調(diào)試與問題排查6.1 從白屏到跑起來調(diào)試基礎(chǔ)操作RN on OpenHarmony 的調(diào)試思路和 Android 類似但也有自己的怪脾氣。真機連接 DevEco Studio 后先確認設(shè)備能安裝 hap 包再確認 Metro 服務(wù)是否正常啟動。我碰到過的情況是Metro 沒開應(yīng)用裝上了也白屏日志里一堆“bundle load failed”。開發(fā)模式下手機和電腦必須在同一局域網(wǎng)而且 Metro 的監(jiān)聽地址不要用localhost否則真機訪問不到。我直接在命令行里跑react-native start --host 0.0.0.0這樣手機可以通過電腦的局域網(wǎng) IP 訪問 Metro。日志這塊也有坑。OpenHarmony 的 hilog 和 RN 的 console 不一定完全打通有時候業(yè)務(wù)代碼報錯了DevEco 控制臺看不到信息。我的做法是給關(guān)鍵流程加一份本地日志寫入比如記錄“bundle 加載完成”“題目生成成功”“存儲執(zhí)行成功”這些打點真機出問題時先看這些打點再回頭查 hilog。6.2 常見問題速查表現(xiàn)象可能原因排查思路真機安裝失敗包簽名不對在 DevEco Studio 里重新配置簽名確認 bundleName 和證書匹配白屏且無報錯bundle 未打進 hap 或 Metro 未啟動檢查 rawfile 下有沒有 bundle 文件確認 Metro 可用中文輸入法無法調(diào)起TextInput 的鍵盤類型與系統(tǒng)沖突用keyboardTypenumeric并測試不同輸入法版本樣式不一致部分 RN 樣式屬性未完全映射查看適配層支持的樣式列表用boxShadow等屬性時預(yù)留降級方案AsyncStorage 讀不到數(shù)據(jù)bundleName 或應(yīng)用被卸載確認包名未改變卸載重裝會清空本地數(shù)據(jù)滾動列表卡頓renderItem 內(nèi)聯(lián)函數(shù)或 key 不穩(wěn)定設(shè)置穩(wěn)定 key抽離 renderItem 為獨立方法這張表不是從文檔里抄的都是我自己在真機上一個一個試出來的。尤其是樣式不一致那條boxShadow在 Android 上已經(jīng)支持得不錯但 OpenHarmony 適配層早期版本并不支持代碼里不寫降級界面上就是干巴巴一個平面。我最后統(tǒng)一抽了個Card組件內(nèi)部做平臺兼容判斷業(yè)務(wù)層不用關(guān)心細節(jié)。6.3 我踩過的兩個關(guān)鍵坑第一個坑是數(shù)字鍵盤調(diào)起后中文輸入法還是能輸入字母。孩子做題時切到手寫輸入法TextInput 拿到的是“三”而不是“3”Number(三)返回NaN校驗直接判錯。這個問題的根子在鍵盤類型只影響“彈出的面板”不影響輸入法引擎。我最后的處理是校驗時只接受數(shù)字字符串檢測到非數(shù)字字符立即清空并提示“請輸入數(shù)字”。這個方法看著暴力但對低齡用戶其實最有效因為錯誤輸入被即時糾正了。第二個坑是提交按鈕快速點擊導(dǎo)致一次答題被記錄兩次。原因很簡單點擊提交后 setState 還沒觸發(fā)重渲染按鈕一直處于可點擊狀態(tài)第二次點擊進來時當(dāng)前題還是同一道答案被重復(fù)記錄。解決辦法是在提交入口加一個短鎖提交后立刻把按鈕disabled置為 true同時用一個 ref 記錄isSubmitting直到當(dāng)前題索引變化后再恢復(fù)。這個問題的排查過程也提醒我RN on OpenHarmony 的重渲染時機和 Android / iOS 略有差異不要完全依賴 UI 狀態(tài)做并發(fā)控制。最后再說一個個人心得數(shù)字和引號之類的小問題在 iOS / Android 上可能影響不大但在新適配平臺上都會更容易暴露。不要假設(shè)某個交互在當(dāng)前平臺表現(xiàn)正常一定要在真機上按真實用戶操作路徑完整跑一遍尤其是鍵盤、觸摸、焦點這類和系統(tǒng)強相關(guān)的交互。數(shù)學(xué)練習(xí)這個項目做完我最大的收獲不是 RN 代碼本身而是學(xué)會了把所有“跨平臺想當(dāng)然”都當(dāng)成風(fēng)險來處理。這個項目后續(xù)還可以繼續(xù)擴展的方向很多比如把錯題本導(dǎo)出成本地報告、增加多用戶切換或者加入語音聽題功能。如果你們也在做類似遷移建議保持耐心先把最小功能完整跑通一遍再逐步疊加能力。