圖形兼容層:relinker與SPIR-V轉(zhuǎn)換實(shí)戰(zhàn))
1. 從AnyPS5這個(gè)名字說(shuō)起它到底想解決什么問(wèn)題第一次看到AnyPS5這個(gè)項(xiàng)目名我腦子里冒出來(lái)的第一個(gè)念頭是這大概率跟 PlayStation 5 沒(méi)什么關(guān)系而是某種讓任意平臺(tái)都能跑起來(lái)的通用方案。結(jié)合關(guān)鍵詞里的 Linux、Windows、relinker、SPIR-V基本可以判斷這是一個(gè)跨平臺(tái)的圖形/計(jì)算運(yùn)行時(shí)重定向項(xiàng)目——核心思路是把某一套圖形 API 調(diào)用翻譯或重定向到另一套底層實(shí)現(xiàn)上讓原本綁定特定平臺(tái)的程序能在別的系統(tǒng)上跑起來(lái)。這類(lèi)項(xiàng)目在圈子里其實(shí)不算新鮮但AnyPS5這個(gè)命名方式透露出的野心不小Any 意味著通用性PS5 暗示它最初的目標(biāo)場(chǎng)景可能跟主機(jī)級(jí)圖形負(fù)載有關(guān)。換句話(huà)說(shuō)它不是那種只做小打小鬧兼容層的東西而是沖著高負(fù)載、復(fù)雜渲染管線(xiàn)去的。relinker 這個(gè)詞是關(guān)鍵線(xiàn)索——重鏈接器通常出現(xiàn)在動(dòng)態(tài)庫(kù)符號(hào)重綁定、函數(shù)跳轉(zhuǎn)表重建這類(lèi)場(chǎng)景里說(shuō)明項(xiàng)目在二進(jìn)制層面做了不少手腳而不是簡(jiǎn)單的源碼級(jí)移植。SPIR-V 的出現(xiàn)則把技術(shù)棧釘死在了現(xiàn)代圖形編譯體系上。SPIR-V 是 Khronos 推出的中間表示格式Vulkan、OpenCL 都用它作為著色器和內(nèi)核的交換格式。一個(gè)項(xiàng)目如果圍繞 SPIR-V 做文章那它多半涉及著色器編譯、跨 API 轉(zhuǎn)換、或者運(yùn)行時(shí)管線(xiàn)重建。把 relinker 和 SPIR-V 放在一起看畫(huà)面就清晰了這是一個(gè)在運(yùn)行時(shí)攔截圖形調(diào)用、重定向符號(hào)、并把著色器重新編譯到目標(biāo)平臺(tái)原生格式的中間層。適合讀這篇內(nèi)容的人我大致分三類(lèi)。第一類(lèi)是做跨平臺(tái)移植的工程師手頭有 Windows 上的圖形程序想搬到 Linux或者反過(guò)來(lái)被 API 差異折磨得夠嗆。第二類(lèi)是搞模擬器、兼容層、容器化圖形方案的開(kāi)發(fā)者對(duì)二進(jìn)制重鏈接和著色器轉(zhuǎn)換有實(shí)際需求。第三類(lèi)是對(duì)底層圖形棧好奇的技術(shù)愛(ài)好者想搞清楚一個(gè)程序從發(fā)出繪制命令到屏幕上出現(xiàn)像素中間到底被誰(shuí)動(dòng)了手腳。不管你是哪一類(lèi)下面這些內(nèi)容都會(huì)從原理到實(shí)操給你講透。2. relinker 在 AnyPS5 里扮演的角色不只是改個(gè)鏈接2.1 動(dòng)態(tài)鏈接的本質(zhì)與重鏈接的切入點(diǎn)要理解 relinker 為什么重要得先回到動(dòng)態(tài)鏈接的基本事實(shí)。一個(gè)可執(zhí)行文件在運(yùn)行時(shí)它調(diào)用的外部函數(shù)并不是硬編碼地址而是通過(guò) PLTProcedure Linkage Table和 GOTGlobal Offset Table做間接跳轉(zhuǎn)。程序第一次調(diào)用某個(gè)庫(kù)函數(shù)時(shí)動(dòng)態(tài)鏈接器會(huì)解析符號(hào)、填入真實(shí)地址之后就走緩存。這套機(jī)制本來(lái)是給同一份二進(jìn)制在不同環(huán)境跑設(shè)計(jì)的但它有個(gè)前提目標(biāo)庫(kù)得存在且符號(hào)簽名兼容。AnyPS5 面對(duì)的場(chǎng)景比這苛刻得多。它要處理的不是庫(kù)版本不同而是目標(biāo)平臺(tái)上根本沒(méi)有這套 API。比如某個(gè)程序調(diào)用的是某套專(zhuān)有圖形接口的函數(shù)而 Linux 上只有 Vulkan 或 OpenGL。這時(shí)候光靠 LD_PRELOAD 攔截是不夠的因?yàn)楹瘮?shù)簽名、調(diào)用約定、甚至對(duì)象布局都可能對(duì)不上。relinker 的價(jià)值就在這里它在二進(jìn)制層面重建符號(hào)引用把對(duì)原始 API 的調(diào)用重定向到 AnyPS5 自己實(shí)現(xiàn)的兼容函數(shù)上同時(shí)保證調(diào)用約定和數(shù)據(jù)結(jié)構(gòu)布局的正確性。我實(shí)際做過(guò)類(lèi)似的符號(hào)重綁定實(shí)驗(yàn)最深的體會(huì)是難點(diǎn)從來(lái)不在找到符號(hào)并替換地址而在于替換之后棧幀和寄存器狀態(tài)還得對(duì)得上。x86-64 的 System V ABI 和 Windows x64 調(diào)用約定在參數(shù)傳遞上就有差異前六個(gè)整型參數(shù)走寄存器但具體用哪些寄存器、浮點(diǎn)參數(shù)怎么處理、返回值放哪兩邊規(guī)則不同。relinker 必須精確處理這些差異否則程序跑著跑著就崩而且崩的位置往往離真正的問(wèn)題很遠(yuǎn)排查起來(lái)極其痛苦。2.2 符號(hào)解析的優(yōu)先級(jí)陷阱做重鏈接時(shí)有個(gè)特別容易踩的坑符號(hào)解析順序。動(dòng)態(tài)鏈接器解析符號(hào)時(shí)遵循一套搜索順序通常是可執(zhí)行文件自身、然后按 DT_NEEDED 順序遍歷依賴(lài)庫(kù)、最后是全局符號(hào)表。AnyPS5 注入的兼容層如果放在錯(cuò)誤的位置就會(huì)出現(xiàn)該攔截的沒(méi)攔截到不該攔截的被截了的情況。我的經(jīng)驗(yàn)是兼容層的符號(hào)必須放在搜索順序的靠前位置但又不能無(wú)差別覆蓋所有符號(hào)。比較穩(wěn)妥的做法是只導(dǎo)出你真正要替換的那批符號(hào)其余符號(hào)讓它自然回落到系統(tǒng)庫(kù)。可以用LD_DEBUGbindings觀(guān)察實(shí)際的綁定過(guò)程看每個(gè)符號(hào)最終解析到了哪個(gè)庫(kù)。這個(gè)調(diào)試開(kāi)關(guān)輸出量很大建議配合 grep 過(guò)濾特定符號(hào)名不然屏幕會(huì)被刷爆。還有一個(gè)隱蔽的問(wèn)題弱符號(hào)和強(qiáng)符號(hào)的交互。如果原始程序里某個(gè)符號(hào)是弱符號(hào)而你的兼容層提供了強(qiáng)符號(hào)鏈接器會(huì)優(yōu)先用強(qiáng)的這通常是好事。但如果原始程序依賴(lài)弱符號(hào)的可缺失語(yǔ)義來(lái)做特性探測(cè)你強(qiáng)行提供強(qiáng)符號(hào)反而會(huì)讓它誤判環(huán)境。這種情況在圖形驅(qū)動(dòng)探測(cè)里很常見(jiàn)程序會(huì)嘗試解析某個(gè)擴(kuò)展函數(shù)解析到就認(rèn)為支持該擴(kuò)展解析不到就走降級(jí)路徑。你的兼容層如果無(wú)腦提供所有符號(hào)程序就會(huì)以為所有擴(kuò)展都可用然后調(diào)用時(shí)才發(fā)現(xiàn)你的實(shí)現(xiàn)不完整直接崩。2.3 重鏈接后的驗(yàn)證手段改完鏈接行為怎么確認(rèn)真的生效了我一般分三步驗(yàn)證。第一步是靜態(tài)檢查用readelf -d看動(dòng)態(tài)段確認(rèn)兼容層庫(kù)確實(shí)在依賴(lài)列表里且順序正確。第二步是運(yùn)行時(shí)檢查用LD_DEBUGbindings或ltrace觀(guān)察實(shí)際調(diào)用走了哪個(gè)實(shí)現(xiàn)。第三步是行為驗(yàn)證跑一個(gè)最小化的測(cè)試用例看輸出是否符合預(yù)期。這里有個(gè)細(xì)節(jié)值得說(shuō)ltrace對(duì)圖形程序的干擾很大因?yàn)樗鼤?huì)攔截所有庫(kù)調(diào)用圖形程序調(diào)用極其頻繁加上 ltrace 的開(kāi)銷(xiāo)后幀率會(huì)掉到?jīng)]法看。更好的選擇是用LD_AUDIT機(jī)制寫(xiě)一個(gè)輕量的審計(jì)模塊只記錄你關(guān)心的那幾個(gè)符號(hào)的調(diào)用開(kāi)銷(xiāo)小得多。我自己寫(xiě)過(guò)一個(gè)幾十行的審計(jì) so專(zhuān)門(mén)盯特定符號(hào)實(shí)測(cè)對(duì)性能影響在可接受范圍內(nèi)。提示重鏈接調(diào)試階段建議關(guān)閉編譯器的符號(hào)可見(jiàn)性?xún)?yōu)化確保所有需要攔截的符號(hào)都是默認(rèn)可見(jiàn)的。等驗(yàn)證通過(guò)后再收緊可見(jiàn)性避免導(dǎo)出過(guò)多符號(hào)引發(fā)沖突。3. SPIR-V 轉(zhuǎn)換鏈路著色器怎么從一套 API 跑到另一套3.1 為什么中間表示是跨 API 的關(guān)鍵圖形 API 之間的移植最麻煩的從來(lái)不是繪制調(diào)用本身而是著色器。不同 API 的著色器語(yǔ)言、編譯模型、資源綁定方式都不一樣。如果每次移植都從源碼級(jí)重寫(xiě)著色器工作量巨大且容易出錯(cuò)。SPIR-V 的價(jià)值就在于它提供了一個(gè)統(tǒng)一的中間層只要能把源著色器編譯到 SPIR-V再?gòu)?SPIR-V 翻譯到目標(biāo) API 的原生格式就能實(shí)現(xiàn)一次編寫(xiě)多處運(yùn)行。AnyPS5 圍繞 SPIR-V 做文章說(shuō)明它的轉(zhuǎn)換鏈路大概是這樣的攔截原始 API 的著色器創(chuàng)建調(diào)用拿到著色器字節(jié)碼或源碼轉(zhuǎn)換成 SPIR-V再用目標(biāo)平臺(tái)的工具鏈把 SPIR-V 編譯成原生著色器。這條鏈路里每一步都有坑我逐個(gè)說(shuō)。第一步是拿到原始著色器。如果原始 API 接受的是字節(jié)碼那相對(duì)好辦直接解析。如果接受的是源碼就得先編譯。這里的問(wèn)題是不同 API 的著色器語(yǔ)言方言差異很大有些還帶專(zhuān)有擴(kuò)展。解析器必須足夠?qū)捜莘駝t稍微偏一點(diǎn)的語(yǔ)法就編譯失敗。第二步是轉(zhuǎn)成 SPIR-V。這一步通常借助 SPIRV-Tools 或類(lèi)似的庫(kù)。需要注意的是SPIR-V 有多個(gè)版本和大量擴(kuò)展目標(biāo)平臺(tái)支持哪些版本、哪些擴(kuò)展直接決定了你能用哪些特性。我建議在轉(zhuǎn)換前先查詢(xún)目標(biāo)平臺(tái)的能力然后據(jù)此選擇 SPIR-V 版本和擴(kuò)展集而不是無(wú)腦用最新版本。第三步是從 SPIR-V 編譯到原生格式。這一步依賴(lài)目標(biāo)平臺(tái)的編譯器比如某些平臺(tái)用 glslang 或自研編譯器。編譯結(jié)果的質(zhì)量直接影響運(yùn)行性能有時(shí)候同一個(gè) SPIR-V 用不同優(yōu)化級(jí)別編譯出來(lái)性能能差百分之二三十。3.2 資源綁定的映射難題著色器轉(zhuǎn)換里最容易被低估的是資源綁定。不同 API 對(duì)紋理、緩沖區(qū)、采樣器的綁定模型完全不同。有的用固定槽位有的用描述符集有的用綁定表。把一套模型映射到另一套需要維護(hù)一張映射表而且這張表在運(yùn)行時(shí)可能動(dòng)態(tài)變化。我踩過(guò)的一個(gè)坑是原始 API 允許同一個(gè)資源在不同階段以不同方式綁定而目標(biāo) API 可能要求綁定一致。這時(shí)候就得在轉(zhuǎn)換層做資源復(fù)制或視圖重建。資源復(fù)制有顯存開(kāi)銷(xiāo)視圖重建有兼容性風(fēng)險(xiǎn)選哪個(gè)得看具體場(chǎng)景。我的做法是優(yōu)先視圖重建實(shí)在不行才復(fù)制并且對(duì)復(fù)制做緩存避免每幀重復(fù)。另一個(gè)坑是綁定的生命周期。有些 API 的綁定是設(shè)置后一直有效直到被覆蓋有些是每次繪制都要重新綁定。轉(zhuǎn)換層必須正確跟蹤綁定狀態(tài)否則會(huì)出現(xiàn)上一幀的紋理串到這一幀這種詭異 bug。這類(lèi) bug 特別難查因?yàn)殇秩窘Y(jié)果看起來(lái)只是顏色不對(duì)很容易被誤認(rèn)為是著色器邏輯問(wèn)題。3.3 著色器緩存與熱重載實(shí)際使用中著色器編譯往往是啟動(dòng)階段最耗時(shí)的部分。AnyPS5 這類(lèi)項(xiàng)目如果每次啟動(dòng)都重新編譯所有著色器用戶(hù)體驗(yàn)會(huì)很差。所以著色器緩存幾乎是必備的。緩存的關(guān)鍵是鍵的設(shè)計(jì)鍵必須能唯一標(biāo)識(shí)一份著色器同時(shí)又要足夠穩(wěn)定避免環(huán)境微小變化就導(dǎo)致緩存失效。我的做法是用著色器源碼哈希 目標(biāo)平臺(tái)標(biāo)識(shí) 編譯選項(xiàng)哈希作為緩存鍵。源碼哈希保證內(nèi)容變了緩存失效平臺(tái)標(biāo)識(shí)保證換平臺(tái)不會(huì)誤用緩存編譯選項(xiàng)哈希保證優(yōu)化級(jí)別變了會(huì)重新編譯。緩存文件建議用內(nèi)容尋址的方式存儲(chǔ)文件名就是鍵的哈希這樣天然去重也方便清理。熱重載是另一個(gè)實(shí)用特性。開(kāi)發(fā)階段改著色器后不想重啟程序就需要熱重載。實(shí)現(xiàn)上通常是監(jiān)聽(tīng)文件變化變化后重新編譯并替換運(yùn)行時(shí)的著色器對(duì)象。難點(diǎn)在于替換時(shí)要保證不破壞正在進(jìn)行的繪制通常需要等一幀結(jié)束再替換或者用雙緩沖的方式平滑切換。4. 跨 Windows 與 Linux 的落地環(huán)境差異比想象中大4.1 圖形棧的根本差異Windows 和 Linux 的圖形棧差異是 AnyPS5 這類(lèi)項(xiàng)目必須正面硬剛的問(wèn)題。Windows 上圖形驅(qū)動(dòng)模型相對(duì)統(tǒng)一廠(chǎng)商提供的運(yùn)行時(shí)接口比較一致。Linux 上則碎片化嚴(yán)重Mesa、廠(chǎng)商專(zhuān)有驅(qū)動(dòng)、各種合成器組合起來(lái)行為差異很大。最直接的差異在窗口系統(tǒng)集成。Windows 有 HWNDLinux 有 X11 和 Wayland 兩套。X11 相對(duì)成熟Wayland 更現(xiàn)代但兼容性還在完善中。AnyPS5 如果要在 Linux 上跑必須同時(shí)處理這兩套。我的建議是優(yōu)先支持 X11因?yàn)榇媪砍绦虼蠖喟?X11 模型寫(xiě)的Wayland 可以通過(guò) XWayland 兼容層過(guò)渡。等 X11 路徑穩(wěn)定了再考慮原生 Wayland。另一個(gè)差異是同步機(jī)制。Windows 的圖形同步模型和 Linux 的 fence、semaphore 模型不完全對(duì)應(yīng)??缙脚_(tái)轉(zhuǎn)換時(shí)同步對(duì)象的語(yǔ)義必須仔細(xì)映射否則會(huì)出現(xiàn)畫(huà)面撕裂或者卡死。我遇到過(guò)最詭異的一次是在 Windows 上正常的程序搬到 Linux 后每隔幾秒卡一下查了很久才發(fā)現(xiàn)是 fence 等待的超時(shí)設(shè)置不匹配Windows 默認(rèn)超時(shí)較長(zhǎng)Linux 較短導(dǎo)致偶發(fā)超時(shí)后走了降級(jí)路徑。4.2 文件路徑與依賴(lài)解析跨平臺(tái)還有個(gè)看似簡(jiǎn)單實(shí)則煩人的問(wèn)題路徑。Windows 用反斜杠和盤(pán)符Linux 用正斜杠和掛載點(diǎn)。程序內(nèi)部如果硬編碼了路徑分隔符移植后就會(huì)找不到資源。AnyPS5 作為中間層需要在路徑處理上做歸一化把各種形式的路徑統(tǒng)一成內(nèi)部表示再按目標(biāo)平臺(tái)的習(xí)慣輸出。依賴(lài)解析也是類(lèi)似的問(wèn)題。Windows 上 DLL 搜索路徑有一套規(guī)則Linux 上 so 搜索路徑是另一套。重鏈接時(shí)如果依賴(lài)庫(kù)找不到程序直接起不來(lái)。我的經(jīng)驗(yàn)是在兼容層里顯式指定依賴(lài)庫(kù)的搜索路徑不要依賴(lài)系統(tǒng)的默認(rèn)搜索行為這樣行為更可預(yù)測(cè)??梢杂肦PATH或RUNPATH把庫(kù)路徑寫(xiě)進(jìn)二進(jìn)制避免運(yùn)行時(shí)找不到。注意修改 RPATH 時(shí)優(yōu)先用$ORIGIN相對(duì)路徑這樣整個(gè)目錄搬到哪里都能跑。絕對(duì)路徑在開(kāi)發(fā)機(jī)上沒(méi)問(wèn)題一到用戶(hù)環(huán)境就各種找不到。4.3 性能剖析的跨平臺(tái)方法調(diào)優(yōu)跨平臺(tái)圖形程序性能剖析工具的選擇很關(guān)鍵。Windows 上常用的是廠(chǎng)商提供的圖形調(diào)試器Linux 上則有 RenderDoc、apitrace 這類(lèi)開(kāi)源工具。RenderDoc 跨平臺(tái)支持不錯(cuò)Windows 和 Linux 都能用是我首選的抓幀工具。apitrace 更偏向 API 調(diào)用追蹤適合分析調(diào)用序列問(wèn)題。抓幀分析時(shí)有個(gè)技巧不要一上來(lái)就抓完整幀先抓一個(gè)最小可復(fù)現(xiàn)的場(chǎng)景。完整幀的調(diào)用量可能上萬(wàn)分析起來(lái)眼花繚亂。把場(chǎng)景簡(jiǎn)化到只剩一個(gè)繪制調(diào)用問(wèn)題往往一目了然。我通常的做法是先用程序自帶的調(diào)試選項(xiàng)關(guān)掉大部分渲染只留一個(gè)物體抓幀分析清楚后再逐步加回復(fù)雜度??缙脚_(tái)對(duì)比也很有價(jià)值。同一個(gè)場(chǎng)景在 Windows 和 Linux 上各抓一幀對(duì)比調(diào)用序列和資源狀態(tài)差異點(diǎn)往往就是問(wèn)題所在。我靠這個(gè)方法定位過(guò)好幾個(gè)只在某個(gè)平臺(tái)出現(xiàn)的 bug效率比盲猜高得多。5. 實(shí)操中那些文檔不會(huì)告訴你的坑5.1 線(xiàn)程模型的隱式假設(shè)圖形程序?qū)€(xiàn)程模型往往有隱式假設(shè)。比如渲染線(xiàn)程和主線(xiàn)程是同一個(gè)或者資源創(chuàng)建必須在特定線(xiàn)程。這些假設(shè)在原始平臺(tái)上成立移植后可能就不成立了。AnyPS5 作為中間層如果改變了線(xiàn)程行為程序就可能出問(wèn)題。我遇到過(guò)一個(gè)典型案例程序假設(shè)所有圖形調(diào)用都在主線(xiàn)程所以?xún)?nèi)部狀態(tài)沒(méi)有加鎖。移植后兼容層為了性能把部分調(diào)用放到了工作線(xiàn)程結(jié)果狀態(tài)競(jìng)爭(zhēng)導(dǎo)致偶發(fā)崩潰。修復(fù)方式要么是兼容層保證調(diào)用線(xiàn)程一致要么是給狀態(tài)加鎖。前者性能好但限制多后者通用但有開(kāi)銷(xiāo)。我的選擇是默認(rèn)保證線(xiàn)程一致只在明確安全的地方才做異步。5.2 錯(cuò)誤處理的語(yǔ)義差異不同 API 的錯(cuò)誤處理語(yǔ)義差異很大。有的 API 出錯(cuò)返回錯(cuò)誤碼有的拋異常有的靜默失敗只寫(xiě)日志。轉(zhuǎn)換層必須把這些語(yǔ)義統(tǒng)一否則上層程序無(wú)法正確判斷失敗。更麻煩的是有些 API 的錯(cuò)誤在另一套 API 里根本不算錯(cuò)誤比如某個(gè)資源格式不支持一套 API 直接報(bào)錯(cuò)另一套可能自動(dòng)降級(jí)到相近格式。我的處理原則是能降級(jí)的降級(jí)不能降級(jí)的明確報(bào)錯(cuò)絕不靜默失敗。靜默失敗是最坑的程序以為成功了繼續(xù)跑跑到后面才崩排查成本極高。寧可早期明確報(bào)錯(cuò)讓問(wèn)題暴露在離根因最近的地方。5.3 版本兼容的長(zhǎng)期維護(hù)AnyPS5 這類(lèi)項(xiàng)目要長(zhǎng)期維護(hù)版本兼容是繞不開(kāi)的。目標(biāo)平臺(tái)的 API 在演進(jìn)原始程序的 API 也在演進(jìn)兼容層夾在中間兩邊都得跟。我的建議是建立一套兼容性測(cè)試矩陣覆蓋主要的 API 版本組合每次改動(dòng)都跑一遍。測(cè)試用例不用多但必須覆蓋核心路徑。另外兼容層內(nèi)部要做好版本抽象。不要把某個(gè)版本的 API 細(xì)節(jié)散落在代碼各處而是集中到版本適配層。這樣新版本出來(lái)時(shí)只需要改適配層核心邏輯不動(dòng)。這個(gè)架構(gòu)決策早期做和晚期做成本差好幾倍。我見(jiàn)過(guò)太多項(xiàng)目因?yàn)樵缙跊](méi)做抽象后期每支持一個(gè)新版本就要大改維護(hù)得苦不堪言。6. 從 AnyPS5 延伸出去這類(lèi)方案的通用設(shè)計(jì)思路做 AnyPS5 這類(lèi)跨平臺(tái)圖形兼容層沉淀下來(lái)的設(shè)計(jì)思路其實(shí)可以復(fù)用到很多場(chǎng)景。核心就三條攔截要精準(zhǔn)、轉(zhuǎn)換要無(wú)損、降級(jí)要可控。攔截精準(zhǔn)意味著你只動(dòng)該動(dòng)的部分其余保持原樣。很多兼容層失敗就是因?yàn)閿r截太寬把不該改的也改了引入一堆新問(wèn)題。轉(zhuǎn)換無(wú)損意味著信息在轉(zhuǎn)換過(guò)程中不能丟丟了就得靠猜猜就會(huì)錯(cuò)。降級(jí)可控意味著當(dāng)目標(biāo)平臺(tái)不支持某個(gè)特性時(shí)要有明確的降級(jí)策略而不是直接崩或者靜默出錯(cuò)。這三條說(shuō)起來(lái)簡(jiǎn)單做起來(lái)每一條都需要大量細(xì)節(jié)支撐。但只要你抓住這三條主線(xiàn)遇到具體問(wèn)題時(shí)就有了判斷依據(jù)這個(gè)改動(dòng)是讓攔截更精準(zhǔn)了還是更模糊了這個(gè)轉(zhuǎn)換是有損的還是無(wú)損的這個(gè)降級(jí)路徑是可控的還是失控的用這三把尺子量一量大部分設(shè)計(jì)決策都能想清楚。我自己在做類(lèi)似項(xiàng)目時(shí)最大的體會(huì)是不要追求一步到位支持所有場(chǎng)景。先把一條最核心的路徑打通跑通、跑穩(wěn)再逐步擴(kuò)展。AnyPS5 如果一開(kāi)始就想支持所有 API、所有平臺(tái)、所有特性大概率會(huì)陷入泥潭。聚焦一個(gè)具體場(chǎng)景把它做到能用比做一個(gè)什么都支持但什么都不好用的東西有價(jià)值得多。