渲染與合成鏈路:從App到屏幕的完整流程與性能優(yōu)化)
Android圖形系統(tǒng)這條路我寫到了第三篇。前兩篇我們聊了Window、Activity和View那一層的東西今天把視角拉到系統(tǒng)級專門聊渲染和合成的底層鏈路。這個(gè)系列里這篇是我覺得最值得反復(fù)讀的因?yàn)榫W(wǎng)上講Activity生命周期、講View繪制的文章很多但能把“App畫完一幀之后發(fā)生了什么”這件事講透的確實(shí)不多。簡單說這篇解決三個(gè)問題一幀畫面從App進(jìn)程到屏幕上經(jīng)過哪些進(jìn)程、哪幾道工序?yàn)槭裁从袝r(shí)候明明布局不復(fù)雜卻掉幀以及遇到渲染性能問題時(shí)怎么從原理反推出排查方向。適合已經(jīng)寫過自定義View、看得懂onDraw但對“屏幕為什么卡一下”“SurfaceView為什么快”這類問題還想再往深挖一層的同學(xué)。1. 整體設(shè)計(jì)與核心思路為什么要把渲染和合成拆成兩件事1.1 這是一條跨進(jìn)程的生產(chǎn)線Android的圖形鏈路本質(zhì)上是一條跨進(jìn)程的生產(chǎn)線。你可以把它想象成一家餐廳的后廚和傳菜口應(yīng)用進(jìn)程是廚師負(fù)責(zé)把食材UI狀態(tài)加工成菜品一幀畫面SystemServer里有一個(gè)叫SurfaceFlinger的進(jìn)程是傳菜員負(fù)責(zé)把所有廚師做好的菜各App的畫面按順序擺到一個(gè)盤子里最后端到餐桌上屏幕。為什么必須拆成兩個(gè)階段因?yàn)槠聊恢挥幸粋€(gè)但同時(shí)在“做菜”的App有好幾個(gè)。狀態(tài)欄、導(dǎo)航欄、桌面、你正在聊天的窗口每一層都是獨(dú)立App或獨(dú)立線程畫的。如果每個(gè)App直接去寫屏幕顯存那畫面一定亂套——你蓋住了我我刷掉了你。所以Android把“畫畫”和“上桌”分開App只負(fù)責(zé)往自己的緩沖區(qū)塊Buffer里畫畫完交給SurfaceFlinger統(tǒng)一決策誰在上面、誰在下面、誰需要被合成、誰直接透傳然后輸出到顯示器。這背后是一套典型的生產(chǎn)者-消費(fèi)者模型用類比的思路梳理一下就清楚了。角色對應(yīng)組件職責(zé)生產(chǎn)者App進(jìn)程內(nèi)的View系統(tǒng) HWUI渲染引擎把界面繪制成像素/指令寫入BufferQueue的空閑Buffer緩沖隊(duì)列BufferQueue管理Buffer的生產(chǎn)與消費(fèi)解決生產(chǎn)和消費(fèi)速度不一致的問題消費(fèi)者SurfaceFlinger進(jìn)程從BufferQueue取走已填好的Buffer合成后送顯屏幕Display / Composer最終把像素亮出來1.2 渲染和合成是兩套完全不同的“計(jì)時(shí)器”很多性能問題分析不準(zhǔn)就是因?yàn)闆]搞懂渲染和合成各走各的時(shí)鐘。渲染階段的節(jié)拍器叫Vsync應(yīng)用信號Choreographer合成階段的節(jié)拍器叫Vsync合成信號SurfaceFlinger的調(diào)度器。雖然都源自同一個(gè)硬件VBlank屏幕掃描完一幀后返回起點(diǎn)發(fā)出的脈沖但中間經(jīng)過了不同的分發(fā)路徑所以App畫一幀和系統(tǒng)合一層并不是嚴(yán)格的“你畫完我馬上合”。這也解釋了為什么有時(shí)候你的App明明在60fps穩(wěn)定輸出但用戶感知還是“有點(diǎn)不跟手”——因?yàn)楹铣呻A段如果某一幀耗時(shí)太長SurfaceFlinger來不及在下一輪Vsync之前完成合成就會跳過一幀。所以做性能優(yōu)化不能只盯著App側(cè)的渲染時(shí)間合成側(cè)的耗時(shí)同樣是變量。1.3 先把原理補(bǔ)齊再談?wù){(diào)優(yōu)我見過不少同學(xué)一上來就開“開發(fā)者選項(xiàng)-顯示Surface更新”“調(diào)試GPU過度繪制”一頓操作猛如虎最后也沒定位到根因。原因很簡單工具只能告訴你“這里有掉幀”“這里過度繪制了”但不會告訴你“為什么”。要回答為什么必須知道這條鏈路上每一站的工作方式。這篇博文就是想把這條鏈路從源頭捋到尾把每站的關(guān)鍵機(jī)制和對應(yīng)工具串成一個(gè)整體后面你無論是做應(yīng)用層優(yōu)化還是做系統(tǒng)層定制都有底子可以依靠。2. 應(yīng)用側(cè)渲染鏈路從setContentView到屏幕像素2.1 setContentView只是“布置場地”不是“畫畫”先糾正一個(gè)常見的誤解很多人以為調(diào)用setContentView之后布局就開始繪制了其實(shí)不是。setContentView做的事本質(zhì)上只是把XML布局解析成View樹掛到Window上這時(shí)候屏幕上什么都沒有。真正的第一次繪制要等到Vsync回調(diào)把消息發(fā)到主線程觸發(fā)Choreographer的doFrame才會走“measure量尺寸) - layout擺位置 - draw畫出來”這套流程。關(guān)于Choreographer你可以把它理解成一個(gè)“節(jié)拍器服務(wù)員”它跟硬件Vsync對齊然后一杯一杯地把咖啡回調(diào)端給主線程。主線程接到回調(diào)就開始干活。如果主線程忙著處理別的事物比如超長列表的item測量、一個(gè)巨大的Bitmap解碼沒能在下一杯咖啡送來之前干完那一幀就丟掉了——這就是掉幀的本質(zhì)。我第一次做性能排查時(shí)總以為是draw方法里的path計(jì)算太慢后來在Perfetto里才看到真正吃掉時(shí)間的其實(shí)是上一幀遺留的measure/layout任務(wù)draw本身反而是輕活。這個(gè)認(rèn)知直接影響了我后來的寫法能不動的布局就不動能動局部就不invalidate整棵View樹。2.2 CPU畫指令GPU畫像素DisplayList的由來從Android 5.0開始View的draw方法執(zhí)行時(shí)CPU不再直接往Bitmap上畫像素而是把繪制操作封裝成一個(gè)指令列表這個(gè)列表叫DisplayList。說人話就是你把“畫一個(gè)紅色圓”“貼一張位圖”“在這段文字上加粗”這些操作按順序記錄下來但并不立刻執(zhí)行。這套設(shè)計(jì)的聰明之處在于緩存和復(fù)用。如果某一幀只是View的一個(gè)屬性變化比如平移DisplayList本身沒變那下一幀直接重放指令就行不需要重新measure、layout、遍歷整棵View樹。這也是屬性動畫比invalidate整View高效的原因之一。當(dāng)然DisplayList也不是永遠(yuǎn)有效的一旦View調(diào)用了invalidate或者它的繪制狀態(tài)被標(biāo)記為dirty對應(yīng)節(jié)點(diǎn)的DisplayList就得重建。2.3 RenderThread與GPU執(zhí)行主線程不畫像素的真正原因DisplayList準(zhǔn)備好之后主線程的工作就基本結(jié)束了。接下來一個(gè)叫RenderThread的渲染線程會接管DisplayList把它轉(zhuǎn)成OpenGL ES現(xiàn)在也有Vulkan路徑的繪制命令提交給GPU執(zhí)行。這就是“硬件加速渲染”的默認(rèn)模型CPU負(fù)責(zé)“編劇本”生成DisplayListGPU負(fù)責(zé)“拍電影”渲染像素。為什么Android要專門搞一個(gè)RenderThread因?yàn)槿绻欣L制命令都由主線程提交給GPU那么當(dāng)GPU繁忙時(shí)主線程就會卡在等待GPU返回結(jié)果這一步導(dǎo)致點(diǎn)擊事件、輸入事件全部沒空處理用戶感知就是“點(diǎn)不動”“卡死”。有了RenderThread之后主線程只管生成指令提交和等待GPU是后臺線程的事兩者可以流水線式并行。這在低端機(jī)上表現(xiàn)尤其明顯你給主線程減負(fù)一點(diǎn)點(diǎn)用戶能明顯感覺到界面“活”了很多。關(guān)于渲染引擎這兩年還有一個(gè)很值得關(guān)注的發(fā)展方向Flutter新默認(rèn)引擎Impeller它把Skia的運(yùn)行時(shí)shader編譯很大一部分挪到了離線階段目標(biāo)是解決首幀白屏和shader編譯卡頓問題。這個(gè)思路對Android原生也有借鑒意義——你的App如果大量使用自定義Shader編譯階段是繞不開的隱性成本可以像我后面講的那樣用預(yù)熱方式規(guī)避。2.4 BufferQueue生產(chǎn)者和消費(fèi)者之間的“物流中心”RenderThread渲染完成后GPU把像素寫入一塊Buffer然后通過BufferQueue把這塊Buffer交出去。BufferQueue本質(zhì)上是一個(gè)有狀態(tài)的隊(duì)列經(jīng)典的三板斧操作是dequeueBuffer取一塊空閑Buffer用于繪制、queueBuffer畫完放回隊(duì)列、acquireBuffer消費(fèi)者取走處理。幀率能不能穩(wěn)Buffer數(shù)量是關(guān)鍵。老Android版本只有雙緩沖App在畫幀A時(shí)屏幕正在顯示幀B如果App畫得太快想畫幀C但手里沒Buffer了只能干等屏幕把幀B掃完于是幀率被強(qiáng)制拉低。后來引入三緩沖相當(dāng)于給生產(chǎn)者多一塊后端Buffer緩沖讓生產(chǎn)者在某些時(shí)刻可以提前畫即使屏幕還沒掃完也能繼續(xù)干活。代價(jià)是額外的一層內(nèi)存和顯示延遲硬件條件允許時(shí)Android會自動在三緩沖和雙緩沖之間切換不需要開發(fā)者手動干預(yù)。3. 合成階段SurfaceFlinger是如何把畫面拼出來的3.1 所有App的出口都匯聚到SurfaceFlinger一個(gè)入口應(yīng)用側(cè)畫完之后所有窗口的Buffer都進(jìn)了SurfaceFlinger。SurfaceFlinger是整個(gè)圖形系統(tǒng)的“中央調(diào)度室”它維護(hù)了一張Layer列表每個(gè)Layer對應(yīng)一個(gè)窗口更準(zhǔn)確地說一個(gè)Surface。它要做的事是在每次Vsync合成信號到來時(shí)檢查哪些Layer有新幀到來dirty狀態(tài)然后根據(jù)每個(gè)Layer的顯示參數(shù)位置、大小、透明度、裁剪區(qū)域、Z序決定如何把它們輸出到屏幕這里的“Z序”就是窗口的蓋壓關(guān)系。狀態(tài)欄在頂層、輸入法在需要時(shí)浮起來、Dialog在最上面這些都是由WindowManager通過Transaction事務(wù)告知SurfaceFlinger的比如setLayer、setPosition、setAlpha。如果你看過相關(guān)代碼會發(fā)現(xiàn)WindowManager幾乎每幀都可能發(fā)起事務(wù)SurfaceFlinger要高效合并這些事務(wù)再應(yīng)用也是不小的負(fù)載。3.2 GPU合成與硬件合成誰適合合成誰只做透傳合成這一步得看抬誰來做。如果所有Layer都能交給硬件合成器那SurfaceFlinger就做個(gè)“甩手掌柜”直接把各Layer的Buffer地址、格式、裁剪信息打包發(fā)給顯示控制器讓硬件去完成疊加。這就是HWCHardware Composer模式省電且高效——注意這里“合成”其實(shí)是硬件在顯示掃描過程中完成的不走GPU甚至不額外占內(nèi)存帶寬。但硬件合成器并非全能的。某些Layer如果是GPU繪制的結(jié)果需要做旋轉(zhuǎn)/縮放/混合等復(fù)雜變換、需要特效處理HWC大概率不支持這時(shí)候SurfaceFlinger只能自己上通過OpenGL ES把所有Layer合到一塊新Buffer里再把這塊Buffer送去顯示這就是GLES合成。還有一類更進(jìn)階的東西硬件2D合成器。有些SoC會提供獨(dú)立的2D引擎來做圖層疊加像T113這類方案里提到的G2D就屬于此。這類引擎的核心價(jià)值在于用專門硬件處理2D合成比通用GPU更輕量、更省帶寬特別適合做LVGL這類GUI的渲染加速。但要用好它得清晰區(qū)分哪些操作是“純2D搬移/疊加”哪些操作必須走GPU這個(gè)邊界不把握好反而可能比全走GPU還慢。3.3 兩個(gè)被混淆的概念Layer與Surface很多人會把Layer和Surface當(dāng)成同一個(gè)東西其實(shí)Surface是生產(chǎn)端的Buffer容器Layer是合成端的一個(gè)顯示實(shí)體。一個(gè)Surface被queueBuffer之后SurfaceFlinger會為它建立/更新對應(yīng)的Layer或者Layer更新Buffer內(nèi)容。Layer還承擔(dān)了Buffer的緩存管理如果SurfaceFlinger發(fā)現(xiàn)一個(gè)Layer的內(nèi)容沒有變化即沒有新Buffer入隊(duì)就可以在合成時(shí)直接復(fù)用上一次的Buffer不需要重新合成一遍。這也是為什么靜態(tài)界面的省電效果遠(yuǎn)好于動畫界面。在實(shí)際工作中統(tǒng)計(jì)Surface和Layer的數(shù)量是一項(xiàng)基本功。Layer太多意味著每一幀合成時(shí)要做更多決策、處理更多Buffer再強(qiáng)的合成器也會忙不過來。常見病根是多個(gè)浮窗、多個(gè)透明Activity、頻繁創(chuàng)建的SurfaceView。能用單Surface承載的內(nèi)容就盡量不要拆成多個(gè)窗口。3.4 為什么SurfaceView可以“局部更新”且開銷更低講到這里SurfaceView的優(yōu)勢就很好理解了。普通View的一切都要先經(jīng)過ViewRootImpl走DisplayList渲染到App的Surface上再由SurfaceFlinger合成。SurfaceView則單獨(dú)擁有一塊獨(dú)立的Surface獨(dú)立的BufferQueue而且默認(rèn)情況下它還有個(gè)重要特性它的Layer在Z序上位于宿主窗口的“挖洞”層之下也就是宿主窗口那塊區(qū)域相當(dāng)于被挖空了SurfaceView的內(nèi)容直接從前臺的獨(dú)立Layer透傳顯示。這意味著兩點(diǎn)第一SurfaceView的內(nèi)容更新不需要經(jīng)過App整體RenderThread重繪它能做到獨(dú)立于View樹的刷新第二因?yàn)樗仟?dú)立Layer在合成時(shí)可以直接交給HWC透傳不走GPU合成所以視頻播放場景特別偏愛它。這也是為什么我經(jīng)常建議如果你要高頻刷新一塊區(qū)域視頻、相機(jī)預(yù)覽、游戲畫面優(yōu)先考慮SurfaceView而不是在普通View里瘋狂invalidate。4. 性能優(yōu)化實(shí)戰(zhàn)從原理反推排查方向4.1 掉幀了先分清是誰的鍋經(jīng)驗(yàn)豐富的性能優(yōu)化工程師看到掉幀第一反應(yīng)不是改代碼而是先分類這幀是App側(cè)渲染慢還是合成側(cè)慢還是主線程調(diào)度慢三個(gè)方向?qū)?yīng)不同的證據(jù)來源。用adb shell dumpsys gfxinfo可以看App的繪制統(tǒng)計(jì)數(shù)據(jù)比如Draw、Prepare、Process等各階段耗時(shí)其中如果“Total GPU time”特別高說明GPU負(fù)擔(dān)重如果想看合成側(cè)的情況dumpsys SurfaceFlinger --latency可以輸出各Layer的幀時(shí)間戳信息配合分析是否有掉幀或Buffer不及時(shí)。我自己的習(xí)慣是先開Perfetto或Systrace抓一段場景數(shù)據(jù)找到掉幀的那個(gè)時(shí)刻看主線程上是不是有一個(gè)很長的任務(wù)常見的是setContentView大布局、getView重復(fù)創(chuàng)建、SharedPreferences讀取如果沒有再去RenderThread上找GPU瓶頸去看SurfaceFlinger那一段的時(shí)間線。只要這條鏈路的分析順序固定下來大多數(shù)性能問題都能定位到具體環(huán)節(jié)而不是靠猜。4.2 過度繪制與DisplayList緩存兩個(gè)容易被忽視的重災(zāi)區(qū)過度繪制是GPU壓力的主要來源。開發(fā)者選項(xiàng)里的“調(diào)試GPU過度繪制”用顏色區(qū)分繪制層級每多畫一層就多一層顏色最嚴(yán)重時(shí)整個(gè)屏幕都是紅色的。典型問題包括多層嵌套的背景疊底、帶陰影的CardView大面積堆疊、DrawerLayout/Scrim在桌面層上又畫了一層全屏遮罩。優(yōu)化的本質(zhì)是減少不必要的繪制指令能用一層背景解決的就不要疊三層能用clipRect裁剪掉不可見區(qū)域的就不讓那部分像素白白參與渲染。另一個(gè)重災(zāi)區(qū)是DisplayList緩存失效。很多時(shí)候你只是改了一個(gè)View的屬性但因?yàn)樗幱谝粋€(gè)復(fù)雜的ViewGroup中invalidate一觸發(fā)整個(gè)父容器的DisplayList都得重建。這個(gè)問題在看列表RecyclerView時(shí)尤其明顯如果你的item根布局寫得極其復(fù)雜或者item復(fù)用機(jī)制沒做好很容易一兩幀就把GPU打滿。我處理過一個(gè)實(shí)際案例明明GPU型號不差但列表滑動就是掉幀最后定位到的問題是item的陰影效果在每次滑動時(shí)都觸發(fā)整棵子樹DisplayList重建。把陰影改為預(yù)先畫好的純色陰影圖后幀率立刻回歸平穩(wěn)。4.3 setLayerType與硬件層的取舍View.setLayerType是一個(gè)容易被人濫用、也容易被人忽視的API。LAYER_TYPE_HARDWARE表示這個(gè)View會離屏渲染到一個(gè)Texture上之后系統(tǒng)可以直接復(fù)用這塊Texture而不用每次重新執(zhí)行draw方法。它適合用在動畫頻繁的場景比如旋轉(zhuǎn)、縮放一個(gè)復(fù)雜View但有兩個(gè)坑一是離屏緩沖會額外占用GPU內(nèi)存濫用的話內(nèi)存吃緊反而觸發(fā)更頻繁的GC和掉幀二是如果View在動畫過程中內(nèi)容頻繁變化比如每幀都在更新里面的文本或Bitmap那離屏緩沖還不如不建因?yàn)槊看蝺?nèi)容變化還是要重畫整個(gè)Texture等于白繞一圈。所以我的經(jīng)驗(yàn)是先用工具確認(rèn)動畫期間是不是真的Draw方法重繪開銷很大確認(rèn)了再用LayerType不要一上來就“優(yōu)化”。另外如果你的App確實(shí)在動畫期間遇到了Shader編譯卡頓可以采用一種“預(yù)熱”思路在啟動階段用一個(gè)小區(qū)域、低開銷的方式提前觸發(fā)目標(biāo)Shader的一次編譯把編譯耗時(shí)的尖峰平移到用戶還沒明顯感知的時(shí)刻。這個(gè)技巧在Impeller出現(xiàn)之前是很多引擎做首幀優(yōu)化的妥協(xié)方案。4.4 開發(fā)者選項(xiàng)里的實(shí)用開關(guān)最后補(bǔ)充幾個(gè)開發(fā)者模式里對圖形調(diào)試極其實(shí)用的開關(guān)和它們的原理依據(jù)“顯示Surface更新”讓已提交的Surface在更新時(shí)閃爍能快速發(fā)現(xiàn)哪些Surface在刷幀、哪些在隱藏地偷跑資源。“顯示布局邊界”幫你看清楚每個(gè)控件的真實(shí)邊界Locate布局問題很直觀?!澳M輔助顯示設(shè)備”如果你在做多屏異顯或副屏適配用它模擬多個(gè)邏輯顯示。“Force GPU渲染”讓不支持硬件加速的繪制也強(qiáng)制走GPU路徑可以用來定位某些Canvas操作在軟件渲染下是否成為瓶頸。這些開關(guān)本身不復(fù)雜關(guān)鍵是結(jié)合前面講的鏈路知道它們改的是哪一環(huán)才能用得有意義。5. 常見問題與排查技巧實(shí)錄5.1 問題速查表下面是我在實(shí)際開發(fā)和調(diào)優(yōu)中遇到的一些典型現(xiàn)象整理成一張速查表可以直接對照排查?,F(xiàn)象可能原因排查手段解決方向打開頁面黑屏一閃Activity主題是黑/白背景首幀未完成看看dumpsys gfxinfo首幀時(shí)間、Theme配置設(shè)置啟動背景或減少首幀工作量列表滑動掉幀item布局復(fù)雜、DisplayList頻繁重建Perfetto看RenderThread和主線程耗時(shí)簡化布局、復(fù)用View、減少invalidate范圍視頻播放卡頓/黑塊SurfaceView獨(dú)立時(shí)序與窗口切換沖突看SurfaceFlinger的Layer狀態(tài)檢查SurfaceView的surfaceDestroyed時(shí)序處理好Surface生命周期必要時(shí)用TextureView兜底界面整體白屏/無內(nèi)容合成Buffer異常或OpenGL崩潰看logcat里OpenGL errordumpsys SurfaceFlinger查Layer狀態(tài)檢查硬件加速開關(guān)確認(rèn)是否有關(guān)閉硬件加速的異常動畫過程中掉幀嚴(yán)重動畫導(dǎo)致DisplayList頻繁重建用過度繪制查看繪制層級考慮LAYER_TYPE_HARDWARE或?qū)赢嬕频絉enderThread可獨(dú)立處理的對象上高刷屏上幀率鎖在60系統(tǒng)合成或BufferQueue未匹配高刷模式dumpsys SurfaceFlinger --latency查看幀間隔調(diào)整刷新率切換策略確認(rèn)HWC狀態(tài)5.2 容易被忽略的三個(gè)坑第一個(gè)坑StrictMode和掉幀檢測只是結(jié)果指標(biāo)不是定位工具。我看到很多人卡頓發(fā)生時(shí)只會說“這一幀16.9ms”但根本不知道是哪里的16.9ms。其實(shí)應(yīng)該要抓Perfetto而且抓的時(shí)候一定要包含SurfaceFlinger進(jìn)程不然只能看到App內(nèi)部隔離的一半真相。第二個(gè)坑View.setBackground的顏色疊加。這個(gè)問題很隱蔽給根布局設(shè)了背景色給RecyclerView的item也設(shè)了背景色再給item里某個(gè)控件設(shè)了背景色結(jié)果三層的背景全部參與了繪制把GPU帶寬消耗在了看不見的地方。很多所謂的“復(fù)雜布局導(dǎo)致卡頓”其實(shí)根本原因是背景疊加把不必要的背景改成透明即可大幅改善。第三個(gè)坑Bitmap過大GPU紋理超限。OpenGL ES對紋理尺寸上限有要求部分低端機(jī)超過上限會直接繪制失敗或黑屏。排查時(shí)要檢查Bitmap原始尺寸避免用超大尺寸圖片做撐滿屏幕的背景正確做法是先用BitmapRegionDecoder等工具加載尺寸合適的采樣圖而不是讓GPU去處理一張明顯超限的紋理。寫在最后這條鏈路走下來我覺得最有價(jià)值的一點(diǎn)是當(dāng)你真正理解渲染和合成是兩套獨(dú)立機(jī)制后你在做界面優(yōu)化時(shí)就不會再被“卡頓”這個(gè)詞帶偏而是會冷靜地問是繪制階段慢了還是合成階段慢了是主線程掉幀還是GPU掉幀針對不同環(huán)節(jié)對癥下藥很多之前靠亂試碰運(yùn)氣的優(yōu)化就變成了有依據(jù)的工程決策。我個(gè)人還有一個(gè)習(xí)慣想分享給你遇到難定位的渲染問題先別急著改代碼先用dumpsys命令把當(dāng)前進(jìn)程的Surface、Layer、緩沖情況全部打出來看一眼很多時(shí)候問題會自己浮出水面。比如你懷疑某個(gè)彈窗一直在做動畫一查Layer狀態(tài)發(fā)現(xiàn)它確實(shí)每隔幾幀就在queueBuffer那問題的源頭就清楚了。最后再補(bǔ)一條實(shí)戰(zhàn)經(jīng)驗(yàn)動手優(yōu)化前先在真機(jī)上把“GPU呈現(xiàn)模式分析”打開跑一遍目標(biāo)場景觀察條形圖的高低和顏色分布。如果綠色條密集且高度一致說明渲染管線穩(wěn)定如果出現(xiàn)很多紅色長條再去深挖具體階段。磨刀不誤砍柴工原理和工具配合起來比一上來就寫優(yōu)化方案靠譜得多。