解析)
做車載安卓開發(fā)的朋友應(yīng)該都清楚給車機做 OTA 更新和給手機做 OTA 完全不是一個難度等級。手機更新掛了用戶頂多吐槽兩句進 recovery 再刷一次就能救回來車機要是更新到一半斷電、斷網(wǎng)或者引導(dǎo)程序被寫壞用戶面對的是一塊黑屏的中控導(dǎo)航、倒車影像、空調(diào)控制全部失效。這種現(xiàn)場處理成本比手機高一個量級因為車主不會自己拆機刷機也沒人愿意為了一個系統(tǒng)版本跑兩趟維修點。我前兩年接手一個基于安卓系統(tǒng)的車機項目在更新流程上反復(fù)栽跟頭最后把 Chrome OS 的更新機制完整過了一遍才算把思路徹底理順。Chrome OS 的更新流程和安卓系統(tǒng)在底層是同源的核心思想是“雙槽位 原子切換”永遠(yuǎn)不修改正在運行的系統(tǒng)而是把新版本完整寫到另一個槽位分區(qū)塊校驗通過后再切換過去啟動失敗還能自動回退。這篇文章把這條流程從版本檢查到重啟回滾逐段拆開講重點落在怎么把它落地到安卓系統(tǒng)的車載設(shè)備、手機這類場景里。適合看這篇內(nèi)容的有三類人正在設(shè)計車機或智能座艙 OTA 方案的系統(tǒng)工程師想讓安卓設(shè)備更新更可靠的 ROM 開發(fā)者以及單純想弄明白“系統(tǒng)更新在后臺到底做了什么”的進階用戶。我不畫那些漂亮的架構(gòu)圖只講實際跑通這條鏈路需要知道的事以及我踩過的坑。1. 車載更新為什么容易翻車三個繞不開的現(xiàn)實約束1.1 電力不可控更新做到一半用戶可能直接熄火車機沒有“充電寶”這種后備方案。手機的更新策略可以簡單粗暴地要求“電量大于 50%接上充電器再更新”車機不行它依靠整車電源。用戶可能在地庫里待著無聊點了一下“檢查更新”下載完剛進入寫入階段就關(guān)掉電源下車走了。整車斷電意味著 SoC 瞬間失去供電正在寫 eMMC 的進程直接被拍死。更麻煩的是車機系統(tǒng)里往往還掛著儀表聯(lián)動、電源管理、網(wǎng)絡(luò)喚醒等一堆域控制器邏輯。你很難用一個簡單的“電量閾值”判斷能不能寫分區(qū)因為你不知道下一秒用戶會不會啟動發(fā)動機。我的做法是加一個“電源擋位 電壓兜底”雙條件整車電源處于 ON 擋且電瓶電壓高于某個閾值比如 12V才允許進入寫入階段下載階段則不受此限制。這樣至少把“熄火瞬間寫入”的概率降到最低。1.2 網(wǎng)絡(luò)不可靠地庫、隧道、高速移動下的下載體驗車機的網(wǎng)絡(luò)環(huán)境比手機差得多。手機在地庫里沒信號可以等回家連 Wi-Fi車機經(jīng)常就是在地庫或者戶外停車場停著的時候才有人想起更新。再加上很多車機的蜂窩模塊硬件帶寬本身就有限一個幾 GB 的鏡像包下載幾個小時是常態(tài)下載過程中進出隧道、跨基站、信號從滿格變成無服務(wù)都是家常便飯。所以車機 OTA 的第一條鐵律就是下載必須支持?jǐn)帱c續(xù)傳而且進度要持久化。Chrome OS 的更新引擎在這一點上做得很扎實它把已下載數(shù)據(jù)的偏移量記錄在持久分區(qū)里重啟后還能接著下。很多車機項目第一次做 OTA 時沒有意識到這一點把下載緩存放在容易被清掉的內(nèi)存文件系統(tǒng)里結(jié)果斷一次電80% 的進度直接清零用戶心態(tài)直接爆炸。1.3 救援手段缺失車機沒有“刷機自由”手機用戶遇到系統(tǒng)壞了還能自己進 recovery 雙清、線刷、卡刷社區(qū)教程一抓一大把。車機呢大部分車主不知道什么是 recovery中控面板上也不存在“音量鍵 電源鍵”這種組合鍵想進刷機模式得靠診斷儀或者特殊指令。就算工程師能遠(yuǎn)程進入診斷模式車機本身黑屏的情況下用戶連車都開不順體驗極其糟糕。這些約束疊加起來指向同一個結(jié)論車機的更新方案必須默認(rèn)“更新過程中隨時可能被打斷”而且打斷之后系統(tǒng)還要能自己恢復(fù)。這正是 Chrome OS 更新機制的設(shè)計前提也是我把它作為參考藍(lán)本的原因。2. Chrome OS 更新機制的核心骨架雙槽位、狀態(tài)機與更新引擎2.1 雙槽位設(shè)計不是“覆蓋”而是“寫完再切”Chrome OS 的存儲里同時保留兩份系統(tǒng)KERNEL-A/ROOT-A 和 KERNEL-B/ROOT-B這就是所謂的 A/B 雙槽位。系統(tǒng)當(dāng)前從 A 槽啟動時B 槽是空閑的一次更新做的事情就是往 B 槽寫一份完整的新系統(tǒng)而不是去改動 A 槽的任何字節(jié)。寫完并校驗通過之后才把“下次啟動優(yōu)先用 B 槽”這個標(biāo)記寫進引導(dǎo)程序能讀到的位置。這個設(shè)計可以類比成搬家你不是住在正在裝修的房子里而是在隔壁先裝好一套樣板間驗收滿意之后再搬過去舊房子留著作為退路。對車機這種“不允許現(xiàn)場救磚”的場景來說這個“退路”是剛需而不是錦上添花。2.2 更新引擎與狀態(tài)機整個流程的大腦Chrome OS 里的更新動作由一個常駐后臺的守護進程負(fù)責(zé)源碼里通常叫 update_engine。它負(fù)責(zé)跟更新服務(wù)器通信、下載鏡像、寫分區(qū)、觸發(fā)重啟所有操作都有一個清晰的狀態(tài)機空閑、檢查更新、發(fā)現(xiàn)新版本、下載中、校驗中、收尾、等待重啟。每個狀態(tài)都會落日志排查問題的時候第一步永遠(yuǎn)是看它停在哪個狀態(tài)。這一點對移植到安卓非常重要。安卓系統(tǒng)從支持 A/B 無縫更新開始也把同一個更新引擎接進了自己的框架車機 ROM 里能看到這個進程再正常不過。它的狀態(tài)機和 Chrome OS 基本同源你只要理解了這套狀態(tài)機車機 OTA 的調(diào)試工作就成功了一半。2.3 分區(qū)布局內(nèi)核、根文件系統(tǒng)、用戶數(shù)據(jù)各自有不同的待遇Chrome OS 的分區(qū)不是所有內(nèi)容都做雙份。內(nèi)核和根文件系統(tǒng)走 A/B 雙槽位因為它們決定了系統(tǒng)能不能起來用戶數(shù)據(jù)分區(qū)是單份的兩個槽位共用因為它要跨版本保留。這個分工很有講究系統(tǒng)代碼可以隨時替換用戶數(shù)據(jù)不能隨便丟。落到安卓車機上對應(yīng)關(guān)系大概是這樣內(nèi)核對應(yīng) boot 分區(qū)根文件系統(tǒng)對應(yīng) system/vendor 這些系統(tǒng)分區(qū)用戶數(shù)據(jù)對應(yīng) data 分區(qū)。你可以把 Chrome OS 的分區(qū)哲學(xué)理解為“把不可變的東西做成雙份把可變的東西保持單份”這個原則在移植時依然成立。3. 一次完整更新的五步流從版本協(xié)商到失敗回滾3.1 第一步設(shè)備“報家底”服務(wù)器決定給不給包更新開始前設(shè)備要向更新服務(wù)器匯報自己的身份和現(xiàn)狀硬件標(biāo)識哪一款車機、哪個型號的設(shè)備、當(dāng)前系統(tǒng)版本號、所屬更新通道穩(wěn)定版還是測試版、固件版本等。服務(wù)器根據(jù)這些信息判斷這個設(shè)備當(dāng)前版本離最新版本差多遠(yuǎn)該推全量包還是增量包以及要不要限制灰度范圍。這一步經(jīng)常被車機團隊忽略。很多項目直接把更新服務(wù)器做成“有包就發(fā)”完全沒有版本協(xié)商的概念。結(jié)果就是一臺長期斷網(wǎng)的設(shè)備三個月后終于連上網(wǎng)服務(wù)器給它推了一個基于三個月前版本的增量包而它的當(dāng)前版本比增量包的基準(zhǔn)還老寫入時校驗直接失敗。所以版本協(xié)商不是可選項是整個流程的地基。3.2 第二步全量包還是增量包網(wǎng)絡(luò)決定策略更新包分兩種。全量包包含目標(biāo)版本的完整鏡像體積大好處是不依賴設(shè)備當(dāng)前版本寫起來最穩(wěn)妥增量包只包含新舊版本之間的差異體積可能只有全量包的十分之一但要求設(shè)備當(dāng)前版本正好在基準(zhǔn)范圍內(nèi)。兩者的取舍在車機場景里尤其明顯。我給車機項目定的策略很簡單默認(rèn)優(yōu)先增量包前提是設(shè)備當(dāng)前版本距離目標(biāo)版本不超過兩個版本超過兩代或找不到對應(yīng)基準(zhǔn)直接轉(zhuǎn)全量包。寧可下載慢一點也不讓用戶在“基準(zhǔn)版本不匹配”的報錯里打轉(zhuǎn)。對比項全量包增量包體積大完整鏡像小版本間差異適用條件任意舊版本指定舊版本范圍生成復(fù)雜度低高需要基準(zhǔn)版本失敗風(fēng)險低中基準(zhǔn)不匹配則失敗車機弱網(wǎng)場景后備方案首選3.3 第三步下載、斷點續(xù)傳與逐塊校驗下載不是簡單地把一個文件拉到本地。更新引擎會把鏡像包切成很多塊每一塊都有自己的哈希值下載過程中邊下邊驗驗不過的塊直接重下。整個鏡像包下載完成后還會對包簽名和整體哈希再做一次校驗任何一步不通過更新都不會進入寫分區(qū)階段。斷點續(xù)傳的實現(xiàn)方式是把下載進度記錄到持久分區(qū)的偏好文件里分塊信息和偏移量都留在那里。斷電重啟后引擎讀到原來的進度從斷點繼續(xù)下而不是從頭再來。我在測試?yán)锬M過十幾次“下載中拔電源”只要暫存目錄和進度文件放在持久分區(qū)恢復(fù)率是百分之百反過來說只要這兩個東西放錯地方一次斷電就前功盡棄。3.4 第四步寫入非活動槽位寫完才算數(shù)下載校驗通過后才是真正的“安裝”階段。更新引擎把鏡像內(nèi)容按塊寫入非活動槽位比如當(dāng)前從 A 槽啟動就寫 B 槽的內(nèi)核和根文件系統(tǒng)分區(qū)。每一塊寫完之后立刻做哈希比對不一致就中止并把這個槽位標(biāo)記為不可用。全部寫完后還會對目標(biāo)鏡像做一次整體校驗。這一步給車機方案提了個醒寫入階段是最怕斷電的區(qū)間。雖然 A/B 雙槽位能保證“寫一半斷電后 A 槽還能正常啟動”但 B 槽會殘留一個半成品下次更新必須先清掉重寫。所以我在車機上加了“寫入階段禁止整車下電”的提示同時在策略上允許“下載可以斷寫入必須一口氣完成”。3.5 第五步重啟切換、嘗試計數(shù)與自動回退寫完并校驗通過后更新引擎把“優(yōu)先啟動 B 槽”的標(biāo)記寫進引導(dǎo)程序的數(shù)據(jù)區(qū)域然后請求系統(tǒng)重啟。引導(dǎo)程序啟動 B 槽時會維護一個嘗試計數(shù)每啟動失敗一次計數(shù)減一減到零就自動切回 A 槽只有 B 槽真正起來、關(guān)鍵服務(wù)跑通之后才把計數(shù)清零確定性地鎖定 B 槽。這個嘗試計數(shù)機制是整套回滾能否生效的關(guān)鍵。很多移植版在這個環(huán)節(jié)想當(dāng)然認(rèn)為“重啟后能進桌面就算成功”于是提前把計數(shù)清掉。結(jié)果系統(tǒng)在進桌面前其實已經(jīng)悄悄崩潰過兩次計數(shù)被人為清零后下次一崩就連回退的資格都沒有了。4. 把 Chrome OS 思路移植到安卓車機/手機四個必須處理的落差4.1 分區(qū)模型不同一個 kernel/rootfs一個 boot/system/vendorChrome OS 的雙槽位是“內(nèi)核 根文件系統(tǒng)”兩個大分區(qū)。安卓車機/手機要復(fù)雜得多至少包含 boot、system、vendor有些還有 init_boot、odm、product而且這些分區(qū)在較新的安卓里還可能不是物理分區(qū)而是被塞進一個叫 super 的大分區(qū)里的邏輯分區(qū)。移植 A/B 時你不能簡單地把“寫一個 rootfs 鏡像”翻譯成“寫一個 system 鏡像”而是要把構(gòu)成一套完整系統(tǒng)的所有目標(biāo)分區(qū)都納入寫入計劃。好消息是不用自己從零實現(xiàn)。安卓本身已經(jīng)有 A/B 分區(qū)的標(biāo)準(zhǔn)布局boot_a/boot_b、system_a/system_b、vendor_a/vendor_b引導(dǎo)程序通過 misc 分區(qū)里的 bootloader message 記錄當(dāng)前槽位和嘗試次數(shù)。你要做的主要是核對設(shè)備里的分區(qū)表確保這些“雙份”分區(qū)真實存在而不是名字有了、實際空間共用。分區(qū)角色Chrome OS安卓 A/B 對應(yīng)內(nèi)核KERNEL-A/KERNEL-Bboot_a/boot_b根文件系統(tǒng)ROOT-A/ROOT-Bsystem_a/system_b、vendor_a/vendor_b用戶數(shù)據(jù)單份 statefuldata 分區(qū)單份共享槽位標(biāo)記引導(dǎo)程序數(shù)據(jù)區(qū)misc 分區(qū)的 bootloader message4.2 驗證鏈不同從簽名鏡像到完整 verified bootChrome OS 的更新安全性靠的是完整信任鏈引導(dǎo)程序驗證內(nèi)核簽名內(nèi)核驗證根文件系統(tǒng)的 dm-verity 哈希樹每一級都驗過了才放行。安卓也有對應(yīng)的驗證框架但在車機上很多方案只停留在“下載的鏡像校驗一下哈?!边@個層面引導(dǎo)鏈完全沒接。我的建議是既然做了雙槽位就順手把驗證鏈也補齊。系統(tǒng)簽名、dm-verity 表這些在車機上越來越重要。否則你辛苦保證的更新安全可能被一個沒上鎖的引導(dǎo)程序全部繞過去雙槽位再穩(wěn)也架不住入口是敞開的。4.3 供電與網(wǎng)絡(luò)策略不能照抄要按使用場景重設(shè)Chrome OS 是筆記本和迷你主機場景它的電源約束是“插電 有電池”手機是“電池 偶爾連著充電器”車機是“整車電源 可能隨時被拔鑰匙”。三者的更新策略必須分別設(shè)置不能一套邏輯通吃。我列過一張對照表車機的幾個關(guān)鍵門檻和手機完全不同檢查更新頻率車機不需要像手機那樣一天查好幾次設(shè)置成 6 小時一次即可電量門檻手機看電池百分比車機看電源擋位和電瓶電壓網(wǎng)絡(luò)策略車機如果裝了蜂窩模塊必須區(qū)分計費網(wǎng)絡(luò)和免費 Wi-Fi避免一個鏡像包把用戶的流量套餐刷穿。4.4 存儲空間預(yù)算雙槽位意味著鏡像空間翻倍A/B 最直白的代價是存儲開銷系統(tǒng)所有關(guān)鍵分區(qū)要做兩份額外還要留出下載暫存空間。以一套中等規(guī)模的車機安卓系統(tǒng)為例boot 分區(qū)就算 64MBsystem 分區(qū) 4GBvendor 分區(qū) 1.5GB雙份就是約 11GB再加一個全量包暫存 5GB一共 16GB 起步。很多低成本車機的存儲只有 16GB 或 32GB預(yù)算一算就穿幫。應(yīng)對方案有幾條路優(yōu)先用增量包體積能壓到全量的三分之一甚至更小開啟流式下載邊下邊寫省掉完整暫存這一份存儲實在不夠的再考慮虛擬 A/B用快照方式減少完整副本。但虛擬 A/B 對系統(tǒng)分區(qū)布局和啟動校驗的要求更高屬于進階方案團隊沒有足夠測試資源時不要輕易上。5. 實測中最容易翻車的三個場景與完整排查思路5.1 斷電斷網(wǎng)后更新“丟失”一看暫存路徑二看進度存儲位置現(xiàn)象下載到 80%斷了一次電重啟后更新引擎狀態(tài)回到空閑進度清零。先別急著懷疑更新引擎有問題多半是暫存目錄或進度記錄放在了不該放的地方。排查鏈路如下第一步進系統(tǒng)后查看下載緩存目錄掛在哪個文件系統(tǒng)上執(zhí)行 df 確認(rèn)它是持久分區(qū)還是內(nèi)存文件系統(tǒng)。如果掛在內(nèi)存文件系統(tǒng)重啟必丟先把路徑改到持久分區(qū)。第二步查看進度文件的存儲目錄確認(rèn)它在持久分區(qū)并且斷電后沒有被清理腳本刪掉。第三步看更新引擎日志里有沒有明顯的續(xù)傳記錄字樣。有說明續(xù)傳機制在工作沒有說明前兩步有一處沒做到位。5.2 新槽位起不來但回滾失效問題多半在嘗試計數(shù)處理現(xiàn)象B 槽寫入成功重啟后黑屏再重啟還是黑屏本該回滾到 A 槽卻完全沒有動靜。這類問題十有八九出在嘗試計數(shù)的處理順序上。排查鏈路先抓引導(dǎo)程序日志確認(rèn)每次啟動實際選中的是哪個槽位、計數(shù)還剩幾次。如果引導(dǎo)程序每次都在試 B說明“優(yōu)先啟動 B”的標(biāo)記沒被正確清掉但計數(shù)又沒在減少多見于計數(shù)被放在了易失存儲里。再查系統(tǒng)側(cè)負(fù)責(zé)“標(biāo)記成功啟動”的代碼確認(rèn)它是在完全啟動完成之后才執(zhí)行的。很多時候移植團隊為圖省事把這個標(biāo)記提前到內(nèi)核起來就執(zhí)行結(jié)果系統(tǒng)明明沒起來標(biāo)記已經(jīng)糊上去了。5.3 寫入中報空間不足預(yù)算時少算了一個臨時文件現(xiàn)象寫入到 60% 時更新中止日志報空間不足。明明之前算過“A 槽用多少、B 槽用多少、預(yù)留多少”怎么會不夠原因往往出在沒算這幾項下載暫存文件本身可能還在占用空間如果沒開流式下載日志文件在調(diào)試模式下會飛速膨脹還有系統(tǒng)分區(qū)的元數(shù)據(jù)、哈希樹、以及文件系統(tǒng)預(yù)留塊。我的建議是預(yù)算公式別太樂觀系統(tǒng)鏡像雙份 下載暫存 10% 到 15% 的余量余量按實際日志增長和分區(qū)元數(shù)據(jù)估。上線前寫一個自動化腳本把每個分區(qū)的剩余空間打印出來和人肉預(yù)估對一遍。6. 落地車機項目時我自己用的參數(shù)配置與驗收清單6.1 更新策略參數(shù)按“車機不是手機”來設(shè)以下是我在實際車機項目里驗證過的參考參數(shù)可以直接抄作業(yè)但務(wù)必按你們自己的硬件調(diào)整更新檢查周期6 小時一次另外在整車下電前如果檢測到未完成的下載允許延長工作 5 分鐘做一次斷點存檔。電量/電源門檻整車電源處于 ON 擋且電瓶電壓高于 12V 才允許寫入下載不受此限。網(wǎng)絡(luò)策略免費 Wi-Fi 自動下載蜂窩網(wǎng)絡(luò)只在彈窗確認(rèn)后下載且單次流量上限 500MB超出自動暫停。包策略優(yōu)先增量包版本差距超過兩代轉(zhuǎn)全量包。重試策略下載失敗重試 3 次間隔 5/15/30 分鐘指數(shù)退避。灰度策略1% → 5% → 20% → 50% → 100%每檔停留至少一天。6.2 上線前的驗收清單把斷電測試做成自動化我在項目里把下面這些場景寫成了自動化腳本每次打包后必須全綠才能發(fā)版下載過程中任意時刻斷電重啟后能從斷點續(xù)傳。寫入過程中斷電重啟后舊槽位能正常啟動下次更新能清掉半成品重新來。B 槽寫入成功后人為破壞其內(nèi)核重啟應(yīng)觸發(fā)嘗試計數(shù)遞減并回退 A 槽。新槽位啟動成功后嘗試計數(shù)被正確清零不會在若干次重啟后誤回退。電量低于閾值時系統(tǒng)拒絕進入寫入階段但允許完成下載??臻g不足時給出明確提示而不是悄悄失敗。雙槽位來回升級降級循環(huán)十次用戶數(shù)據(jù)不丟。這些用例看著瑣碎但它們才是車機 OTA 真正值錢的部分。功能開發(fā)可以靠代碼評審可靠性只能靠反復(fù)打斷電測出來。6.3 灰度發(fā)布與失敗決策別一口氣推給所有用戶最后一個建議永遠(yuǎn)不要一次性把更新推給全部設(shè)備。雙槽位機制再穩(wěn)也架不住硬件批次差異、外設(shè)兼容性、用戶使用習(xí)慣這些變量。灰度發(fā)布的時候要盯的不是“下載了多少”而是“成功啟動新槽位的比例”和“回滾率”這兩個指標(biāo)。回滾率超過閾值立刻停發(fā)利用舊槽位還保存著的上一版本引導(dǎo)所有設(shè)備回退。我在項目里設(shè)過一個簡單規(guī)則整體更新成功率低于 95%或者回滾率高于 3%立即凍結(jié)灰度并拉通排查。這個數(shù)字不是拍腦袋定的是先用幾十臺內(nèi)部測試車跑了兩周統(tǒng)計出正常波動范圍之后再設(shè)的。最后再分享一點個人體會。做完這個車機 OTA 項目我最大的感悟是更新流程的好壞不在平時跑得多順而在斷電、斷網(wǎng)、存儲寫壞這些最惡劣的條件下能不能兜底。雙槽位、原子切換、嘗試計數(shù)這套東西聽起來很簡單但每一環(huán)都要靠反復(fù)的斷電和壞塊測試才能真正讓人放心。如果你們項目也在做安卓車機或手機的更新方案我建議第一步不是選服務(wù)器也不是寫代碼而是先把這套流程在測試臺架上完整跑通再來談灰度。另外一個小技巧把更新引擎的日志級別調(diào)到最詳細(xì)并且保留最近三份日志。很多線上問題靠日志就能直接定位別等到出事那天才發(fā)現(xiàn)日志已經(jīng)被沖掉了。