用內(nèi)存優(yōu)化實(shí)戰(zhàn):從原理到落地)
做鴻蒙應(yīng)用開發(fā)這兩年被問得最多的兩個(gè)問題基本沒變過一是 Flutter 寫的界面到底能不能穩(wěn)穩(wěn)跑在鴻蒙設(shè)備上二是跑起來之后內(nèi)存扛不扛得住。第一個(gè)問題在 Flutter-OH 3.x 早期版本就已經(jīng)解決了個(gè)大概第二個(gè)問題卻長(zhǎng)期困擾著不少團(tuán)隊(duì)尤其是我手頭這個(gè)項(xiàng)目圖片多、頁面層級(jí)深在 Mate 系列和 Nova 這些設(shè)備上反復(fù)調(diào)內(nèi)存始終差點(diǎn)意思。Flutter-OH 3.41 正式版這次發(fā)布官方把這版的核心定位說得很明確內(nèi)存負(fù)載全面優(yōu)化讓鴻蒙應(yīng)用更輕、更流暢。我花了兩個(gè)星期把產(chǎn)品主工程升上去又做了幾輪對(duì)比驗(yàn)證實(shí)測(cè)下來確實(shí)能感受到變化這篇文章就把這次升級(jí)前后的一手經(jīng)驗(yàn)和排查過程完整寫出來。1. 為什么 3.41 非要在“內(nèi)存負(fù)載”上動(dòng)刀1.1 鴻蒙終端的內(nèi)存環(huán)境比想象中更苛刻不少開發(fā)者是在 DevEco Studio 模擬器或者旗艦機(jī)上做開發(fā)內(nèi)存 16G、24G 的機(jī)器跑什么都感覺不到壓力。但真實(shí)用戶手里的鴻蒙設(shè)備跨度非常大有剛過千元的中低端機(jī)有折疊屏還有各種平板內(nèi)存從 6G 到 12G 甚至更高都有人在用。系統(tǒng)本身要跑分布式能力、多個(gè)服務(wù)進(jìn)程常駐留給單個(gè)應(yīng)用的余量并沒有想象中那么大。Flutter-OH 在鴻蒙上的運(yùn)行方式又比較特殊——它要讓 Flutter 的 Dart 代碼和鴻蒙的 ArkUI 框架同時(shí)存活并協(xié)作這本身就比安卓版本多一層橋接開銷。一旦應(yīng)用自身對(duì)內(nèi)存不敏感在旗艦機(jī)上看起來一切正常放到中低端設(shè)備上就會(huì)頻繁觸發(fā)系統(tǒng)低內(nèi)存回收。1.2 “內(nèi)存負(fù)載”不等于“內(nèi)存占用”很多人一聽到內(nèi)存優(yōu)化就以為只是把內(nèi)存占用數(shù)字降下來其實(shí)沒那么簡(jiǎn)單。3.41 官方強(qiáng)調(diào)的是“負(fù)載”這個(gè)詞包含的維度更多內(nèi)存峰值、分配頻率、GC 停頓時(shí)長(zhǎng)、后臺(tái)被系統(tǒng)回收的概率、幀率抖動(dòng)、卡頓率這些都是負(fù)載的表現(xiàn)。說得直白一點(diǎn)一個(gè)應(yīng)用即使總占用不高但如果它在短時(shí)間內(nèi)瘋狂創(chuàng)建小對(duì)象導(dǎo)致 GC 頻繁觸發(fā)用戶依然會(huì)感到掉幀、滑動(dòng)不跟手。反過來有些應(yīng)用總占用偏高但分配平穩(wěn)、回收集中體感反而更順滑。Flutter-OH 3.41 這版做的事核心不是單純壓一個(gè)數(shù)字而是讓內(nèi)存的分配、回收、緩存策略更適配鴻蒙系統(tǒng)的運(yùn)行機(jī)制。1.3 3.41 之前的負(fù)擔(dān)到底出在哪我在升級(jí)前特意用舊版本跑了一遍帶圖片瀑布流 App 的 trace問題集中在幾個(gè)地方。第一Dart 側(cè)通過橋接調(diào)用 ArkTS 組件時(shí)每次調(diào)用都會(huì)創(chuàng)建大量臨時(shí)對(duì)象小對(duì)象頻繁分配導(dǎo)致 GC 壓力偏大。第二圖片解碼后的紋理在內(nèi)存里長(zhǎng)期保留頁面退出后也沒有及時(shí)釋放越滾越漲。第三前后臺(tái)切換時(shí)引擎不會(huì)主動(dòng)清理資源應(yīng)用在后臺(tái)待一會(huì)兒再回來內(nèi)存基線已經(jīng)悄悄抬升了。3.41 的改動(dòng)幾乎都是圍繞這幾個(gè)痛點(diǎn)展開的針對(duì)性非常強(qiáng)。2. 3.41 具體優(yōu)化了哪些東西2.1 圖像紋理從“反復(fù)拷貝”變?yōu)椤耙脧?fù)用”這個(gè)是我認(rèn)為這版改動(dòng)里最值錢的一塊。舊版本處理圖片紋理時(shí)每次圖片上屏都要經(jīng)過一次完整的解碼和上傳流程CPU 解碼出來的位圖數(shù)據(jù)要拷貝給 GPU 使用如果同一張圖在列表里出現(xiàn)多次就會(huì)解碼多遍。3.41 對(duì)紋理緩存做了復(fù)用處理同一個(gè)圖片資源在內(nèi)存里只保留一份引用界面多個(gè)地方使用時(shí)共享同一份紋理數(shù)據(jù)。我自己的項(xiàng)目里頭像、商品主圖、運(yùn)營(yíng) banner 有不少是同一張圖多次出現(xiàn)的場(chǎng)景升級(jí)后這部分內(nèi)存改善非常明顯。這里要注意一個(gè)細(xì)節(jié)紋理復(fù)用不等于不清理。引擎增加了緩存水位的概念當(dāng)紋理內(nèi)存達(dá)到閾值時(shí)會(huì)優(yōu)先淘汰最久未使用的紋理而不是等到內(nèi)存爆了才統(tǒng)一回收。這種做法的好處是內(nèi)存不會(huì)突然劇烈波動(dòng)壞處是如果頁面快速來回切換某些圖片可能需要重新解碼一次視覺上會(huì)有一瞬間的空白。后面我在踩坑部分會(huì)詳細(xì)講怎么處理。2.2 GC 策略從“定期全量”調(diào)整為“分代 水位觸發(fā)”Flutter 的 Dart 虛擬機(jī)本身是有分代 GC 的但在鴻蒙適配版本里之前的 GC 觸發(fā)時(shí)機(jī)比較粗糙基本是定期全量回收。全量 GC 的停頓對(duì)動(dòng)畫和滾動(dòng)的殺傷力極大經(jīng)常是滑到一半突然卡一下看 trace 就發(fā)現(xiàn)是 GC 正在工作。3.41 把 GC 策略調(diào)得更貼近用戶操作的實(shí)際節(jié)奏分配達(dá)到水位閾值時(shí)先做新生代回收老年代內(nèi)存主動(dòng)使用并發(fā)標(biāo)記盡量讓回收和渲染并行執(zhí)行減少停頓感。我實(shí)測(cè)下來的感受是頁面切換和滾動(dòng)過程中的幀率明顯更穩(wěn)定了以前那種“每過十幾秒卡一下”的規(guī)律性卡頓基本消失。不過這里也有代價(jià)低端機(jī)上更頻繁的新生代回收會(huì)導(dǎo)致 CPU 占用有短時(shí)波動(dòng)這個(gè)我也是升級(jí)后才發(fā)現(xiàn)的后面避坑部分會(huì)展開說。2.3 ArkUI 橋接通道的批量合并Flutter-OH 在鴻蒙上跑繞不開 Dart 側(cè)和 ArkTS 側(cè)之間的橋接調(diào)用。舊版本里Dart 通知 ArkTS 更新組件狀態(tài)時(shí)采用“一次狀態(tài)變化一次調(diào)用”的模式頁面復(fù)雜時(shí)一幀內(nèi)可能產(chǎn)生上百次小調(diào)用每次都涉及對(duì)象創(chuàng)建、參數(shù)序列化、跨運(yùn)行時(shí)傳遞。3.41 把這部分改成了批量消息合并一個(gè)幀周期內(nèi)的多次狀態(tài)變化會(huì)攢到一起發(fā)送大幅減少了小對(duì)象的創(chuàng)建頻率。從性能 trace 里能直接看到變化相同頁面下橋接相關(guān)的 CPU 耗時(shí)和內(nèi)存分配量都有明顯下降。這個(gè)優(yōu)化對(duì)復(fù)雜頁面的收益尤其大特別是那種一個(gè)頁面有十幾個(gè)自定義組件的場(chǎng)景。2.4 前后臺(tái)切換時(shí)的資源主動(dòng)釋放舊版本在應(yīng)用切到后臺(tái)時(shí)只是暫停了渲染內(nèi)存里的圖片緩存、著色器緩存、動(dòng)畫資源都原封不動(dòng)留著系統(tǒng)如果此時(shí)內(nèi)存吃緊很可能直接把這個(gè)進(jìn)程殺掉再點(diǎn)回來就是冷啟動(dòng)體驗(yàn)非常差。3.41 引入了前臺(tái)后臺(tái)感知的資源回收機(jī)制切后臺(tái)后很快觸發(fā)一輪圖片緩存清理和著色器緩存整理保留核心頁面狀態(tài)但把可重建的資源先釋放掉。這個(gè)改動(dòng)的直接收益是后臺(tái)存活率提高了。我測(cè)試時(shí)故意把應(yīng)用切后臺(tái)然后連續(xù)打開相機(jī)、相冊(cè)等重內(nèi)存應(yīng)用再切回來3.41 版本回到應(yīng)用時(shí)頁面還在舊版本很大概率要重新啟動(dòng)。3.41 核心優(yōu)化項(xiàng)與用戶可感知變化優(yōu)化方向改動(dòng)邏輯用戶可感知的變化圖像紋理復(fù)用同圖共享紋理按水位清理圖片類頁面內(nèi)存下降滾動(dòng)更穩(wěn)GC 分代與水位觸發(fā)減少全量回收并發(fā)標(biāo)記卡頓率下降動(dòng)畫更流暢橋接通道批量合并合并同幀多次調(diào)用復(fù)雜頁面響應(yīng)更快前后臺(tái)資源清理后臺(tái)主動(dòng)釋放可重建資源后臺(tái)被殺概率降低緩存水位管理按需淘汰最久未用緩存內(nèi)存曲線更平穩(wěn)3. 升級(jí)到 3.41 的實(shí)操記錄3.1 升級(jí)前建議先做一次工程體檢不少人拿到新版直接就把 Flutter SDK 換了然后發(fā)現(xiàn)構(gòu)建報(bào)一堆錯(cuò)又開始一步步回退排查浪費(fèi)一整天。我更建議先花半個(gè)小時(shí)確認(rèn)幾件事當(dāng)前工程用的 Flutter-OH 是哪個(gè)版本、Dart SDK 約束范圍、ohos 的最低支持版本、是否接入第三方插件及插件的鴻蒙適配狀態(tài)。我自己的工程之前停在 3.28這次要跳到 3.41中間隔了多個(gè)小版本所以心里提前做了適配準(zhǔn)備。如果你的工程里接入了很多第三方插件建議先去插件倉(cāng)庫(kù)確認(rèn)是否有鴻蒙平臺(tái)的支持聲明有些插件只在安卓和 iOS 上實(shí)現(xiàn)過鴻蒙端實(shí)際上是個(gè)空殼這類插件在升級(jí)后往往會(huì)暴露出隱藏問題。3.2 切換 SDK 與構(gòu)建命令Flutter-OH 的升級(jí)方式和官方 Flutter 略有不同不是簡(jiǎn)單跑一句flutter upgrade就能搞定需要先把本機(jī)的 Flutter SDK 切換到 3.41 對(duì)應(yīng)的 OH 版本。我這邊用的是直接拉取對(duì)應(yīng) release 分支的方式然后重新執(zhí)行依賴?yán)『蜆?gòu)建。# 備份當(dāng)前工程依賴鎖定文件 cp pubspec.lock pubspec.lock.bak cp oh-package.json5 oh-package.json5.bak # 切換到 Flutter-OH 3.41 對(duì)應(yīng) SDK 后清理舊構(gòu)建 flutter clean rm -rf ohos/.cxx rm -rf build # 重新拉取依賴 flutter pub get ohpm install # 構(gòu)建鴻蒙產(chǎn)物 flutter build hap --release --target-platform ohos-arm64第一次構(gòu)建通常不會(huì)太順利我這里遇到的第一個(gè)問題是某個(gè)圖片加載插件沒有適配新版的橋接接口編譯直接報(bào)錯(cuò)。解決辦法是臨時(shí)把它替換成官方默認(rèn)的圖片方案等項(xiàng)目跑通了再根據(jù)實(shí)際需求決定是否保留替代方案。3.3 依賴適配中的幾個(gè)注意點(diǎn)升級(jí)過程中最容易被忽視的部分是oh-package.json5里的鴻蒙原生依賴。很多 Flutter 插件在鴻蒙端依賴特定的 OpenHarmony 組件這些組件的版本號(hào)往往與 Flutter-OH 的版本強(qiáng)相關(guān)不能隨便用安卓那套依賴鎖定的思路。建議升級(jí)后直接把ohos目錄下的依賴都執(zhí)行一遍ohpm outdated看看有沒有需要同步升級(jí)的包。另外如果你的工程里有自定義的 MethodChannel 代碼需要重點(diǎn)檢查通道名和參數(shù)格式。3.41 對(duì)橋接通道做了批量合并優(yōu)化少數(shù)手寫的通道實(shí)現(xiàn)如果依賴“每次調(diào)用都會(huì)在 Dart 和 ArkTS 兩側(cè)立即同步狀態(tài)”的邏輯可能會(huì)遇到狀態(tài)更新延遲一幀的情況。這種問題不算 bug但確實(shí)需要手動(dòng)調(diào)整業(yè)務(wù)邏輯的時(shí)序。3.4 構(gòu)建產(chǎn)物體積的變化3.41 升級(jí)后我特意對(duì)比了 hap 包體積。舊版構(gòu)建出來是 84MB新版是 79MB主要差別來自引擎 so 文件本身的精簡(jiǎn)和重復(fù)符號(hào)的清理以及部分內(nèi)置資源的壓縮算法優(yōu)化。這個(gè)變化不算大但對(duì)用戶側(cè)來說下載流量和安裝時(shí)間都有輕微改善屬于錦上添花的部分。4. 量化驗(yàn)證是“感覺流暢”還是“真的流暢”4.1 用 DevEco Profiler 給應(yīng)用做一次完整內(nèi)存體檢光靠手感說“流暢了”是不夠的尤其是要寫進(jìn)項(xiàng)目周報(bào)或者向團(tuán)隊(duì)匯報(bào)時(shí)必須有可量化的數(shù)據(jù)。我用的工具是 DevEco Studio 自帶的 Profiler配合hdc命令行工具采集內(nèi)存和幀率數(shù)據(jù)。# 查看設(shè)備連接狀態(tài) hdc list targets # 啟動(dòng)應(yīng)用并采集 trace hdc shell aa start -a EntryAbility -b com.example.myapp hdc shell profiler collect --output /data/local/tmp/myapp.trace關(guān)鍵指標(biāo)我建議重點(diǎn)關(guān)注四類PSS按比例分?jǐn)偟奈锢韮?nèi)存占用、Native HeapC/C 側(cè)堆內(nèi)存、GC 頻率與單次暫停時(shí)長(zhǎng)、Frame Duration 的 P90/P95 值。PSS 能反映應(yīng)用在系統(tǒng)層面真實(shí)占用的物理內(nèi)存比單純看 Dart Heap 更接近用戶實(shí)際體驗(yàn)。4.2 我的實(shí)測(cè)數(shù)據(jù)對(duì)比下面是同一臺(tái)榮耀 Magic6 上同一個(gè) App 在 3.28 和 3.41 兩個(gè)版本下的對(duì)比數(shù)據(jù)。測(cè)試場(chǎng)景都是“冷啟動(dòng)后進(jìn)入首頁滑動(dòng)瀑布流 200 個(gè)圖片再切換到詳情頁再返回最后切后臺(tái)再回來”。指標(biāo)3.283.41變化冷啟動(dòng)后 10 秒 PSS823MB661MB下降約 19.7%滑動(dòng) 200 圖后 PSS1.21GB872MB下降約 28%每分鐘 GC 次數(shù)41 次17 次減少約 58%單次 GC 最長(zhǎng)暫停31ms12ms改善約 61%滑動(dòng)場(chǎng)景掉幀率16.6ms 占比2.4%0.7%改善約 70%切后臺(tái)再回前臺(tái)恢復(fù)時(shí)間1.8s冷啟0.4s熱回體驗(yàn)明顯提升這個(gè)結(jié)果相當(dāng)理想尤其是 GC 次數(shù)減少和掉幀率下降直接對(duì)應(yīng)了用戶體感上的“更流暢”。需要說明的是不同應(yīng)用的數(shù)據(jù)會(huì)有差異圖片類應(yīng)用優(yōu)化空間會(huì)更大純文本或列表類應(yīng)用漲幅沒那么夸張但整體方向一定是正向的。4.3 場(chǎng)景化回歸清單性能數(shù)據(jù)都是表象升級(jí)后真正容易出問題的是細(xì)節(jié)場(chǎng)景。我建議不管是什么應(yīng)用升級(jí)后至少回歸這六類場(chǎng)景圖片瀑布流高頻滾動(dòng)、頁面快速前進(jìn)后退、地圖組件的縮放拖拽、WebView 混合頁面、切后臺(tái)再切回、低電量模式下的運(yùn)行表現(xiàn)。其中地圖和 WebView 最容易出現(xiàn)內(nèi)存異常因?yàn)樗鼈兩婕霸晥D和 Flutter 視圖的疊加橋接層的改動(dòng)會(huì)影響疊加繪制。我實(shí)測(cè)中最典型的問題是頁面快速前進(jìn)后退時(shí)某些圖片會(huì)短暫閃爍一下原因是紋理按水位清理后重新解碼的時(shí)間差。后來通過在圖片組件外層加了一個(gè)輕量級(jí)占位狀態(tài)解決視覺上幾乎沒有感知。5. 優(yōu)化帶來的“副作用”與避坑提醒5.1 圖片緩存復(fù)用導(dǎo)致的閃爍問題這個(gè)前面提過紋理復(fù)用和按水位清理會(huì)導(dǎo)致某些圖片在快速往返頁面時(shí)重新解碼進(jìn)而出現(xiàn)短暫空白或閃爍。處理思路有兩個(gè)方向一是利用ImageCache的語義在業(yè)務(wù)層保留更長(zhǎng)時(shí)間的關(guān)鍵圖片引用二是在圖片加載期間顯示占位容器避免黑色或白色閃屏。我自己用的是第二種方案改動(dòng)量小且對(duì)用戶體驗(yàn)沒有副作用。5.2 低端機(jī)上 GC 更“勤快”導(dǎo)致瞬時(shí) CPU 波動(dòng)3.41 的水位觸發(fā) GC 在高性能設(shè)備上非常平滑但在低端機(jī)上由于設(shè)備內(nèi)存本身吃緊水位閾值容易更頻繁地被觸發(fā)表現(xiàn)為 CPU 占用出現(xiàn)短時(shí)尖峰。最直接的辦法是在低端機(jī)型號(hào)上限制頁面層級(jí)深度和減少同時(shí)加載的圖片數(shù)量從源頭降低內(nèi)存分配壓力。不建議關(guān)閉 GC 優(yōu)化那會(huì)因小失大。5.3 部分橋接插件需要同步升級(jí)如果你在用的插件是通過 MethodChannel 和鴻蒙原生頻繁通信的升級(jí)后務(wù)必去檢查插件版本。3.41 把橋接調(diào)用改成了批量合并某些插件如果還依賴舊版的“一次調(diào)用一次返回”時(shí)序就可能會(huì)出現(xiàn)回調(diào)延遲甚至丟失。我在項(xiàng)目中遇到過一個(gè)掃碼插件升級(jí)后掃碼結(jié)果偶爾不返回最終在插件倉(cāng)庫(kù)找到一個(gè)針對(duì)新版優(yōu)化的分支才解決。5.4 不要為了省內(nèi)存把一切都清掉有的同學(xué)看到新版有了資源回收機(jī)制就想在業(yè)務(wù)層把緩存全部關(guān)掉來獲取更低的內(nèi)存占用這個(gè)思路非常危險(xiǎn)。緩存存在的意義是避免重復(fù)計(jì)算的昂貴成本圖片解碼和紋理上傳本身就消耗時(shí)間和電量如果頻繁清理導(dǎo)致每次顯示都要重新解碼內(nèi)存是降下去了但電量和流暢度會(huì)明顯惡化。3.41 的水位策略已經(jīng)是比較科學(xué)的方案業(yè)務(wù)層只需要配合關(guān)鍵場(chǎng)景做微調(diào)不需要也別去推翻它的默認(rèn)行為。6. 升級(jí)后的維護(hù)建議3.41 上了之后我在團(tuán)隊(duì)內(nèi)部定了一個(gè)規(guī)矩所有涉及圖片加載、原生視圖疊加、路由切換的功能改動(dòng)合入前必須跑一遍壓測(cè)用例記錄 PSS 和 GC 數(shù)據(jù)。這個(gè)規(guī)則看起來繁瑣但能防住大多數(shù)內(nèi)存回退問題。實(shí)際做下來發(fā)現(xiàn)最能產(chǎn)生差異的不是那些花哨的性能優(yōu)化手段而是持續(xù)盯住指標(biāo)、定期回歸。出現(xiàn)異常時(shí)用hdc shell抓一段 trace基本就能定位是哪個(gè)模塊在制造大量臨時(shí)對(duì)象或者沒有釋放紋理。另外建議關(guān)注 Flutter-OH 后續(xù)小版本的更新動(dòng)態(tài)像這種引擎級(jí)的優(yōu)化通常會(huì)在幾個(gè)補(bǔ)丁版本內(nèi)收集到更多邊緣場(chǎng)景的問題反饋。如果你已經(jīng)在生產(chǎn)環(huán)境上了 3.41建議保持小版本跟進(jìn)的習(xí)慣修復(fù)合入后能很快平滑升級(jí)。最后分享一個(gè)小技巧升級(jí)后別急著全量發(fā)布先拿一臺(tái)低端機(jī)和一臺(tái)舊版旗艦機(jī)做灰度對(duì)比重點(diǎn)看崩潰率和后臺(tái)存活率。我這次升級(jí)后原先在部分低端機(jī)上偶發(fā)的閃退明顯減少這就是最直觀的收益——用戶不會(huì)在意你用了什么引擎版本但他們一定會(huì)注意到 App 不再頻繁重新加載了。