組件獨(dú)立升級(jí)全解析)
1. 為什么會(huì)有MainlineAndroid碎片化這口鍋搞Android系統(tǒng)定制或整機(jī)方案時(shí)間長了基本都躲不開一個(gè)問題怎么讓系統(tǒng)更新更快、更安全、還不用每次都逼用戶刷全量包。以前我們發(fā)一個(gè)安全補(bǔ)丁從AOSP拉下來、合代碼、過測試、調(diào)CTS、發(fā)OTA一整套流程走完三個(gè)月能落地已經(jīng)算順利。用戶手里的機(jī)器八成還在跑著上一個(gè)版本甚至有人還在用老掉牙的內(nèi)核和系統(tǒng)組件。Mainline項(xiàng)目就是沖著這個(gè)痛點(diǎn)去的。簡單說它把Android系統(tǒng)里的一部分關(guān)鍵組件拆出來做成可以獨(dú)立更新的模塊繞過整機(jī)OTA直接通過系統(tǒng)更新通道推到用戶設(shè)備上。這些模塊以APEX或者APK-in-APEX的形式存在底層原理、打包方式、簽名校驗(yàn)、分區(qū)掛載全都有講究。我第一次拿到APEX包拆開看的時(shí)候第一反應(yīng)是這不就是一個(gè)帶文件映射表的簽名壓縮包嗎后來才發(fā)現(xiàn)它對(duì)系統(tǒng)分區(qū)的掛載策略、回滾機(jī)制、安全校驗(yàn)鏈路都產(chǎn)生了連鎖影響。如果你接觸過Android系統(tǒng)裁剪、ROM定制、設(shè)備認(rèn)證或者只是好奇為什么某些系統(tǒng)組件不升級(jí)整體版本也能更新這篇文章值得看完。我會(huì)把Mainline設(shè)計(jì)思路、APEX打包原理、分區(qū)演進(jìn)、分發(fā)鏈路、廠商適配注意事項(xiàng)以及Debug經(jīng)驗(yàn)一次說清盡量用我們做項(xiàng)目踩坑時(shí)總結(jié)出來的話講。2. Mainline要解決的問題本質(zhì)2.1 系統(tǒng)組件更新的兩頭堵先說說沒有Mainline之前日子怎么過。Android的系統(tǒng)組件大多放在system分區(qū)system分區(qū)在設(shè)備的出廠階段就被寫入之后除了整包OTA幾乎沒有其他合法手段單獨(dú)改它。廠商想要推送一個(gè)新的媒體解碼庫、網(wǎng)絡(luò)協(xié)議棧或運(yùn)行時(shí)更新唯一的正規(guī)路徑就是發(fā)OTA而OTA從生成到灰度到強(qiáng)制推送涉及運(yùn)營商測試、軟件兼容性、海外版本規(guī)范等一系列環(huán)節(jié)周期極長。我問過不少做方案集成的人你們項(xiàng)目里最怕什么答案出奇一致——怕客戶問“能不能只更新某一個(gè)系統(tǒng)組件”。過去真沒法做到因?yàn)閟ystem分區(qū)的完整性和簽名的統(tǒng)一性是綁定死的。你單獨(dú)換掉其中某個(gè)庫整個(gè)分區(qū)的簽名校驗(yàn)就掛了設(shè)備直接進(jìn)recovery。Mainline的思路是把這些組件從system分區(qū)中拆出來單獨(dú)成為“模塊”。模塊所在的邏輯分區(qū)獨(dú)立于system它的更新不需要改動(dòng)system分區(qū)的任何字節(jié)也不需要重啟進(jìn)入OTA流程。相當(dāng)于把之前一個(gè)封閉的系統(tǒng)箱拆成了一排可以獨(dú)立開關(guān)的抽屜每個(gè)抽屜都有自己獨(dú)立的鎖和合頁。2.2 分發(fā)方式的根本變化Mainline的另一個(gè)變化在于分發(fā)通道。傳統(tǒng)OTA必須由設(shè)備廠商發(fā)起從服務(wù)器下發(fā)整包增量包應(yīng)用層配合恢復(fù)系統(tǒng)進(jìn)行安裝。Mainline模塊則交由系統(tǒng)更新服務(wù)統(tǒng)一管理服務(wù)器可直接推送模塊更新不再強(qiáng)制依賴廠商發(fā)布整包OTA。這樣做的好處是即便設(shè)備廠商已經(jīng)停止整機(jī)維護(hù)那些關(guān)鍵模塊仍能持續(xù)獲得修復(fù)在一定程度上延長了設(shè)備的安全生命周期。實(shí)際項(xiàng)目中我們看到的結(jié)果是一臺(tái)兩年前上市的設(shè)備廠商已經(jīng)不再發(fā)大版本OTA但系統(tǒng)自帶的媒體組件、網(wǎng)絡(luò)組件、權(quán)限組件每隔幾個(gè)月還會(huì)更新。用戶感知不到具體哪個(gè)模塊變了但能感覺到部分安全性問題被修補(bǔ)了兼容性問題變少了。這事的本質(zhì)是Google在推進(jìn)一種“服務(wù)化”的系統(tǒng)更新理念——系統(tǒng)不再只是一個(gè)發(fā)行版本更多像一套可持續(xù)升級(jí)的組件集合。Mainline不是某個(gè)功能的更新而是整個(gè)更新策略演進(jìn)的起點(diǎn)。3. APEX與APK-in-APEX模塊拆分的基石3.1 APEX到底是什么APEX格式第一眼看上去像ZIP實(shí)際上也確實(shí)兼容ZIP結(jié)構(gòu)它內(nèi)部有原生庫、可執(zhí)行文件、配置文件、甚至JAR包外層再用簽名保護(hù)。但和普通ZIP最大的區(qū)別是APEX設(shè)計(jì)出來就是要被內(nèi)核掛載為只讀文件系統(tǒng)的掛載后內(nèi)部文件會(huì)直接映射到特定的系統(tǒng)路徑比如/apex/com.android.media。Android系統(tǒng)啟動(dòng)過程中有一個(gè)專門的apexd守護(hù)進(jìn)程負(fù)責(zé)處理這些包。它的工作流程大致是從/system/apex或/data/apex/session目錄掃描合法的APEX文件校驗(yàn)簽名和AVB完整性然后解析包內(nèi)的manifest配置確定這個(gè)包應(yīng)該掛載到哪個(gè)路徑、是否需要激活、激活后哪些SELinux策略要跟著變化。我之前第一次嘗試手寫APEX包的manifest時(shí)犯過一個(gè)錯(cuò)把provideNativeLibs寫漏了一個(gè)平臺(tái)庫名結(jié)果該模塊內(nèi)的組件跑到一半就崩了logcat里全是一堆linker cannot find。后來才明白APEX的manifest不只是簡單描述信息它決定了掛載后的APEX namespace可見范圍寫漏一個(gè)庫意味著這個(gè)作用域里的進(jìn)程全部找不到對(duì)應(yīng)依賴。3.2 APK-in-APEX的存在原因APK-in-APEX聽起來繞口但拆開理解很簡單APEX本身不是給Java層直接跑的Java層的APK需要單獨(dú)的格式。如果一個(gè)模塊既要提供底層native能力又要提供上層應(yīng)用或服務(wù)最簡單的方式就是把APK也封裝進(jìn)APEX里。Android在代碼里區(qū)分了兩種情況APEX里直接放APK文件和普通APK安裝路徑完全不同它不經(jīng)過PackageManager的安裝流程。APK Update則是一種特殊形態(tài)允許更新一個(gè)已預(yù)置在system分區(qū)中的APK而無需整個(gè)APEX替換。這個(gè)機(jī)制對(duì)降低模塊體積很重要。不同廠商在其方案中會(huì)預(yù)置多個(gè)APEX包采用APK-in-APEX方式的好處是可以只更新上層APK那部分底層庫保持不變數(shù)據(jù)量明顯小很多這對(duì)蜂窩網(wǎng)絡(luò)下更新尤其友好。3.3 SDK擴(kuò)展與API穩(wěn)定性Mainline模塊更新對(duì)上層應(yīng)用最大的威脅是模塊版本在變API會(huì)不會(huì)跟著變了Android用了一個(gè)叫SDK擴(kuò)展的機(jī)制來解決這個(gè)問題模塊代碼里使用Build.VERSION.SDK_INT之外多了一個(gè)SDK_INT加擴(kuò)展版本號(hào)的概念。舉個(gè)例子某個(gè)權(quán)限相關(guān)的模塊更新后系統(tǒng)暴露給應(yīng)用的API行為發(fā)生變化但應(yīng)用本身是通過Build.VERSION_CODES里的擴(kuò)展版本號(hào)來判斷能不能用新API的因此即使底層模塊已經(jīng)更新舊應(yīng)用也不會(huì)突然崩潰。模塊更新由服務(wù)器控制但API的變更節(jié)奏受擴(kuò)展版本約束兩者配合才能做到平穩(wěn)演進(jìn)。這條設(shè)計(jì)我認(rèn)為是整個(gè)Mainline框架里最值得稱道的部分它沒有讓模塊更新變成天氣預(yù)報(bào)——誰知哪片云會(huì)下雨。所有行為變更都有版本號(hào)兜底應(yīng)用層可以像以前判斷系統(tǒng)版本一樣判斷模塊能力。4. 啟動(dòng)鏈路與分區(qū)演進(jìn)4.1 從單一system分區(qū)到可疊加分區(qū)Mainline模塊要生效系統(tǒng)光有“更新了模塊”這個(gè)邏輯還不夠必須在啟動(dòng)路由層面就給模塊留位置。假設(shè)模塊都更新到了/userdata或者某個(gè)獨(dú)立分區(qū)系統(tǒng)啟動(dòng)時(shí)如果不掛載它們等于白更新。Android的啟動(dòng)流程從Bootloader到Init再到apexd每一層都需要知道哪些APEX是合法的、在哪里、以什么順序掛載。AOSP里引入了一種叫做“APEX激活”的機(jī)制安裝在用戶數(shù)據(jù)區(qū)的模塊包在每次開機(jī)時(shí)會(huì)被檢查并掛載如果校驗(yàn)失敗則回退到出廠內(nèi)置的系統(tǒng)版本。這意味著系統(tǒng)分區(qū)的頂層多了一層邏輯疊加——出廠的基礎(chǔ)分區(qū)只負(fù)責(zé)提供“初始可用環(huán)境”每次開機(jī)后實(shí)際生效的系統(tǒng)視圖是“出廠版本已激活模塊增量版本”的疊加。這種設(shè)計(jì)對(duì)OTA的影響也很大因?yàn)槟K可能單獨(dú)更新也可能綁定某次OTA一起更新廠商必須設(shè)置好用戶數(shù)據(jù)分區(qū)的大小和預(yù)留空間否則用戶數(shù)據(jù)壓力大的設(shè)備上模塊更新很容易被空間不足卡住。4.2 根分區(qū)驗(yàn)證與可更新分區(qū)的協(xié)調(diào)Android對(duì)系統(tǒng)完整性的保護(hù)一直沒有放松AVBAndroid Verified Boot和dm-verity依然是分區(qū)的安全基石。Mainline引入了一個(gè)問題如果模塊分區(qū)可以動(dòng)態(tài)改變那原本根植于system分區(qū)完整性的信任模型還能不能成立答案是可以的但約束變細(xì)了。AVB的驗(yàn)證鏈條并沒有被打破只是多了一條驗(yàn)證方在啟動(dòng)階段除了驗(yàn)證system分區(qū)還需要驗(yàn)證激活的模塊分區(qū)。模塊分區(qū)有自己獨(dú)立的簽名鏈可以跟系統(tǒng)根密鑰內(nèi)部分離但必須在出廠時(shí)預(yù)置可信的公鑰。這個(gè)邏輯項(xiàng)目中經(jīng)常被誤解。有同事曾問我如果廠商想替換掉某個(gè)Mainline模塊的簽名密鑰是不是只需要在發(fā)布新包時(shí)用新簽名就行答案是否定的。密鑰更新需要單獨(dú)的升級(jí)鏈在老設(shè)備上工廠預(yù)置的信任公鑰不變別的新簽名包根本過不了校驗(yàn)。換簽名這種事只能在出廠的信任根里整體加白名單無法在用戶設(shè)備上自行追加。4.3 動(dòng)態(tài)分區(qū)與Virtual A/B的配合Mainline模塊的獨(dú)立更新如果還走傳統(tǒng)雙分區(qū)切換A/B分區(qū)的老路成本太高。傳統(tǒng)A/B方案意味著設(shè)備里必須留兩套完整系統(tǒng)分區(qū)中低端存儲(chǔ)就吃緊了。Virtual A/B就是為這個(gè)場景設(shè)計(jì)的設(shè)備上只有一份完整屬性和一份表示“更新后數(shù)據(jù)差異”的快照重啟切換時(shí)只更改查看視角而不是真正寫一套系統(tǒng)。和Mainline結(jié)合后模塊更新和系統(tǒng)OTA可以各自獨(dú)立觸發(fā)回滾也能分別處理。有一次我們模擬場景系統(tǒng)OTA包因?yàn)閮?nèi)核模塊兼容問題失敗但此時(shí)某個(gè)Mainline模塊已經(jīng)提前更新到新版本了正常情況下可能造成新模塊與舊系統(tǒng)不兼容。得益于快照機(jī)制模塊和系統(tǒng)的狀態(tài)能夠分別回滾不至于出現(xiàn)一個(gè)進(jìn)得去、一個(gè)退不出的尷尬局面。5. 模塊分發(fā)鏈路與設(shè)備認(rèn)證5.1 誰在管理模塊的更新Mainline模塊的更新通道和系統(tǒng)OTA是有區(qū)別的。在主流方案的實(shí)現(xiàn)中系統(tǒng)更新服務(wù)充當(dāng)了一個(gè)角色你可以把它理解成一個(gè)“模塊更新調(diào)度器”——它定期檢查服務(wù)器端的模塊包是否有新版本如果有且當(dāng)前設(shè)備條件滿足網(wǎng)絡(luò)、電量、存儲(chǔ)空間就會(huì)自動(dòng)下載并安裝。這一步在很多開發(fā)者眼里是個(gè)黑盒服務(wù)器到底推送了什么什么時(shí)候推送廠商能不能控制實(shí)際上廠商是有配置空間的。AOSP里有個(gè)叫apexd的服務(wù)負(fù)責(zé)激活而具體的更新數(shù)據(jù)管理以及授權(quán)策略是落在整個(gè)系統(tǒng)更新框架里的。廠商可以做策略定制比如只在WiFi下自動(dòng)更新或者將所有模塊更新收斂到整機(jī)OTA一起執(zhí)行。我們實(shí)測過兩種模式模塊獨(dú)立更新對(duì)用戶打擾極小后臺(tái)靜默完成幾乎無感但如果廠商把所有模塊更新都綁定到整機(jī)OTA那Mainline的意義就被削弱了因?yàn)橛忠淮伟迅聲r(shí)間拖到了整包OTA的節(jié)奏上。建議在項(xiàng)目和方案里盡量保留模塊獨(dú)立更新的靈活性。5.2 設(shè)備認(rèn)證與模塊兼容性Mainline更新雖然更靈活但也帶來了新的兼容性壓力。模塊更新之后如果和某個(gè)廠商深度定制的底層驅(qū)動(dòng)不兼容用戶會(huì)遇到詭異問題——WiFi打不開、媒體播放異常、或指紋識(shí)別失靈。原因是Mainline模塊的版本更新通常在AOSP主線上驗(yàn)證過但安卓生態(tài)里大量芯片廠商和集成廠商會(huì)改動(dòng)到底層驅(qū)動(dòng)或者擴(kuò)展接口。模塊更新時(shí)如果覆蓋到的代碼路徑恰好和廠商改動(dòng)耦合就會(huì)出現(xiàn)只在這個(gè)設(shè)備上復(fù)現(xiàn)的問題。我建議認(rèn)證流程中建立一個(gè)模塊兼容性子用例重點(diǎn)覆蓋網(wǎng)絡(luò)、媒體和運(yùn)行時(shí)三大塊。設(shè)備認(rèn)證不只包含整機(jī)OTA升級(jí)路徑也包括關(guān)鍵模塊單獨(dú)更新后的穩(wěn)定性驗(yàn)證。項(xiàng)目實(shí)測完成后能明顯減少線上反饋的模塊更新兼容性Bug這是成本低收益高的投入。6. 廠商適配中的真問題與排查辦法6.1 分區(qū)大小與構(gòu)建配置廠商適配Mainline的第一步往往是修改分區(qū)表。因?yàn)槟K分區(qū)不是憑空出現(xiàn)的它需要從system或vendor空間里切出一塊或者直接新建一塊獨(dú)立分區(qū)。不同方案會(huì)選擇不同策略我們見過兩種一是復(fù)用系統(tǒng)自帶的apex目錄所在分區(qū)把增量模塊安裝在數(shù)據(jù)區(qū)緩存中二是單獨(dú)劃分一個(gè)更小的邏輯分區(qū)專門放已激活的模塊包。方案沒有絕對(duì)優(yōu)劣但要求廠商必須計(jì)算清楚出貨策略里要內(nèi)置多少套基礎(chǔ)模塊余量要撐得過未來兩年內(nèi)的模塊版本膨脹。我們早期有個(gè)項(xiàng)目只預(yù)留了300MB給模塊目錄結(jié)果第8個(gè)月就開始頻繁出現(xiàn)空間不足的更新失敗后來不得不把數(shù)據(jù)區(qū)再劃一部分出來做補(bǔ)償?shù)扔谕品貋砹艘淮伟l(fā)布規(guī)劃。6.2 SELinux策略與模塊邊界Mainline模塊激活后它帶來的不只是文件路徑變化還有自己的SELinux標(biāo)簽和策略。AOSP中每個(gè)模塊對(duì)應(yīng)的策略文件都是獨(dú)立維護(hù)的模塊更新時(shí)如果引入新的服務(wù)或訪問新的文件節(jié)點(diǎn)策略不跟著適配就會(huì)出現(xiàn)permission denied。行業(yè)內(nèi)跟進(jìn)這個(gè)問題的標(biāo)準(zhǔn)做法是在集成時(shí)打開SELinux的permissive日志觀察啟動(dòng)階段和模塊激活階段的avc denial信息但要清楚這只是調(diào)試手段正式版本中應(yīng)確保targeted模式無新拒絕日志。常見的陷阱是模塊更新后新增了一個(gè)property或sysfs節(jié)點(diǎn)系統(tǒng)的property_contexts或file_contexts卻沒有補(bǔ)齊。6.3 OTA與模塊更新并發(fā)整機(jī)OTA和Mainline模塊更新同時(shí)觸發(fā)是我們項(xiàng)目里最需要關(guān)注的異常場景。因?yàn)橄到y(tǒng)可能正在升級(jí)system分區(qū)而模塊同時(shí)也在重掛載/apex兩者有極小概率發(fā)生I/O或動(dòng)態(tài)鏈接競爭。從Android系統(tǒng)的調(diào)度設(shè)計(jì)來看OTA本身會(huì)有staging狀態(tài)而模塊更新由另一個(gè)服務(wù)在管理說兩者完全沒有沖突是不可能的。要規(guī)避必須從廠商自定義的升級(jí)策略層面控制整機(jī)OTA觸發(fā)時(shí)先暫停模塊更新等OTA完成重啟后再恢復(fù)。這個(gè)策略之前沒有考慮后來遇到一次升級(jí)失敗的設(shè)備取證日志顯示兩個(gè)更新進(jìn)程進(jìn)行了文件讀寫競爭當(dāng)時(shí)就意識(shí)到必須做全局互斥。6.4 回滾與設(shè)備健康檢查模塊更新失敗后的回滾路徑也是項(xiàng)目上線前容易被忽略的。傳統(tǒng)OTA失敗大多可以靠A/B分區(qū)兜底但模塊更新如果寫壞了回退邏輯是依賴apexd在開機(jī)時(shí)發(fā)現(xiàn)激活校驗(yàn)失敗自動(dòng)使用出廠版本。有一個(gè)要點(diǎn)出廠版本是否存在于system分區(qū)的內(nèi)置APEX目錄中這決定了回滾是否可靠。部分廠商為了省空間在定制時(shí)刪掉了一些系統(tǒng)內(nèi)置APEX的出廠副本只保留用戶數(shù)據(jù)區(qū)的“最新版本”一旦用戶數(shù)據(jù)區(qū)版本損壞或校驗(yàn)失敗設(shè)備就會(huì)陷入模塊缺失的狀態(tài)。這個(gè)坑我們在售后中碰到過處理成本非常高。結(jié)論很明確出廠的“兜底版本”不能省那點(diǎn)空間省不得。7. 實(shí)測多年后的運(yùn)維經(jīng)驗(yàn)做了幾年Mainline項(xiàng)目實(shí)施最深的體會(huì)是這個(gè)框架不是把系統(tǒng)更新變簡單而是把問題拆細(xì)了。傳統(tǒng)OTA是一整鍋粥糊了就是鍋糊了Mainline像是一排抽屜任何一個(gè)抽屜卡住只會(huì)影響局部但排查時(shí)需要你知道哪個(gè)抽屜在哪、誰負(fù)責(zé)打開它、以及鑰匙在誰手里。分享三個(gè)實(shí)戰(zhàn)經(jīng)驗(yàn)第一日志分析要看apexd和update_engine的日志如果模塊更新卡住大概率是空間不足或者DM設(shè)備映射問題這兩個(gè)進(jìn)程的日志比logcat更直接。第二測試適配時(shí)一定要做“出廠版本損壞回滾”的壓力測試。方法很簡單模擬強(qiáng)制修改激活區(qū)數(shù)據(jù)后用重啟腳本觀察是否自動(dòng)回退。很多廠商設(shè)備在普通OTA上過了測試但這道回滾測試沒做好真實(shí)設(shè)備才會(huì)原形畢露。第三模塊更新必須納入整機(jī)版本管理。不要以為模塊是服務(wù)器分發(fā)就不用像固件一樣管理版本號(hào)現(xiàn)場調(diào)試時(shí)我們遇到過模塊版本和系統(tǒng)框架版本不匹配導(dǎo)致上層服務(wù)反復(fù)崩潰的情況。建議項(xiàng)目里建立一個(gè)模塊與系統(tǒng)版本匹配表的自動(dòng)化檢查每次更新前先核對(duì)條目。Mainline帶來的更新策略演進(jìn)本質(zhì)上讓Android從一個(gè)“按版本換代”的系統(tǒng)逐步走向了“按模塊演進(jìn)”的架構(gòu)。這種變化對(duì)用戶是透明的但對(duì)做系統(tǒng)的人來說意味著掌握的知識(shí)面要更寬了——從分區(qū)布局到簽名鏈從SELinux策略到啟動(dòng)順序每一個(gè)環(huán)節(jié)都可能因?yàn)橐粋€(gè)小小的APEX包而牽動(dòng)全局。我在實(shí)際項(xiàng)目里吃過虧之后的最大感受是千萬不要在設(shè)備出貨前才去摸這套機(jī)制那會(huì)兒你大概率已經(jīng)被幾個(gè)更新失敗反饋和一個(gè)“系統(tǒng)空間不足”的紅燈籠搞到加班到凌晨。真正穩(wěn)妥的辦法是在立項(xiàng)和定制階段就把模塊分區(qū)、簽名策略、空間預(yù)留、版本匹配這些前置條件定清楚后面才不會(huì)天天幫線上擦屁股。這個(gè)方向后續(xù)還有不少可以繼續(xù)挖的空間感興趣的可以去研究一下動(dòng)態(tài)系統(tǒng)更新和可組合系統(tǒng)鏡像對(duì)分區(qū)規(guī)劃帶來的進(jìn)一步變化。有一說一在Android這條路上折騰久了你會(huì)發(fā)現(xiàn)系統(tǒng)軟件工程最迷人的地方就在于——每一次你以為已經(jīng)了解全部新的版本就會(huì)冒出來打臉一次。