直播卡頓優(yōu)化
希望本文可以帶給大家一個相對全局的視角看待卡頓問題,認識到卡頓是什么、卡頓的成因、卡頓的分類、卡頓的優(yōu)化和一些經(jīng)驗積累,有的放矢地解決 APP 流暢性問題。接下來會從以下五個方面進行講述:
?什么是卡頓
?為什么會發(fā)生卡頓
?如何評價卡頓
?如何優(yōu)化卡頓
1. 什么是卡頓
卡頓,顧名思義就是用戶體感界面不流暢。我們知道手機的屏幕畫面是按照一定頻率來刷新的,理論上講,24 幀的畫面更新就能讓人眼感覺是連貫的。但是實際上,這個只是針對普通的視頻而言。對于一些強交互或者較為敏感的場景來說,比如游戲,起碼需要 60 幀,30 幀的游戲會讓人感覺不適;位移或者大幅度動畫 30 幀會有明顯頓挫感;跟手動畫如果能到 90 幀甚至 120 幀,會讓人感覺十分細膩,這也是近來廠商主打高刷牌的原因。
對于用戶來說,從體感角度大致可以將卡頓分為以下幾類:

這些體驗對于用戶可以說是非常糟糕的,甚至會引起感官的煩躁,進而導致用戶不愿意繼續(xù)停留在我們的 APP。可以說,流暢的體驗對于用戶來說至關重要。
2. 為什么會發(fā)生卡頓
用戶體感的卡頓問題原因很多,且常常是一個復合型的問題,為了聚焦,這里暫只考慮真正意義上的掉幀卡頓。
2.1 繞不開的 VSYNC
我們通常會說,屏幕的刷新率是 60 幀,需要在 16ms 內(nèi)做完所有的操作才不會造成卡頓。但是這里需要明確幾個基本問題:
為什么是 16ms?
16ms 內(nèi)都需要完成什么?
系統(tǒng)如何盡力保證任務在 16ms 內(nèi)完成?
16ms 內(nèi)沒有完成,一定會造成卡頓嗎?
這里先回答第一個問題:為什么是 16ms。早期的 Android 是沒有 vsync 機制的,CPU 和 GPU 的配合也比較混亂,這也造成著名的 tearing 問題,即 CPU/GPU 直接更新正在顯示的屏幕 buffer 造成畫面撕裂。后續(xù) Android 引入了雙緩沖機制,但是 buffer 的切換也需要一個比較合適的時機,也就是屏幕掃描完上一幀后的時機,這也就是引入 vsync 的原因。
早先一般的屏幕刷新率是 60fps,所以每個 vsync 信號的間隔也是 16ms,不過隨著技術的更迭以及廠商對于流暢性的追求,越來越多 90fps 和 120fps 的手機面世,相對應的間隔也就變成了 11ms 和 8ms。
那既然有了 VSYNC,誰在消費 VSYNC?其實 Android 的 VSYNC 消費者有兩個,也就對應兩類 VSYNC 信號,分別是 VSYNC-APP 和 VSYNC-sf,所對應的也是上層 view 繪制和 surfaceFlinger 的合成,具體的我們接下來詳細說。
這里還有一些比較有意思的點,有些廠商會有 vsync offset 的設計,APP 和 sf 的 vsync 信號之間是有偏移量的,這也在一定程度上使得 APP 和 sf 的協(xié)同效應更好。
2.2 View 顛沛流離的一生
在講下一 part 之前先引入一個話題:
一個 view 究竟是如何顯示在屏幕上的?
我們一般都比較了解 view 渲染的三大流程,但是 view 的渲染遠不止于此:
此處以一個通用的硬件加速流程來表征
Vsync 調(diào)度:很多同學的一個認知誤區(qū)在于認為 vsync 是每 16ms 都會有的,但是其實 vsync 是需要調(diào)度的,沒有調(diào)度就不會有回調(diào);
消息調(diào)度:主要是 doframe 的消息調(diào)度,如果消息被阻塞,會直接造成卡頓;
input 處理:觸摸事件的處理;
動畫處理:animator 動畫執(zhí)行和渲染;
view 處理:主要是 view 相關的遍歷和三大流程;
measure、layout、draw:view 三大流程的執(zhí)行;
DisplayList 更新:view 硬件加速后的 draw op;
OpenGL 指令轉(zhuǎn)換:繪制指令轉(zhuǎn)換為 OpenGL 指令;
指令 buffer 交換:OpenGL 的指令交換到 GPU 內(nèi)部執(zhí)行;
GPU 處理:GPU 對數(shù)據(jù)的處理過程;
layer 合成:surface buffer 合成屏幕顯示 buffer 的流程;
光柵化:將矢量圖轉(zhuǎn)換為位圖;
Display:顯示控制;
buffer 切換:切換屏幕顯示的幀 buffer;
Google 將這個過程劃分為:其他時間/VSync 延遲、輸入處理、動畫、測量/布局、繪制、同步和上傳、命令問題、交換緩沖區(qū)。也就是我們常用的 GPU 嚴格模式,其實道理是一樣的。到這里,我們也就回答出來了第二個問題:16ms 內(nèi)都需要完成什么?
準確地說,這里仍可以進一步細化:16ms 內(nèi)完成 APP 側(cè)數(shù)據(jù)的生產(chǎn);16ms 內(nèi)完成 sf layer 的合成
View 的視覺效果正是通過這一整條復雜的鏈路一步步展示出來的,有了這個前提,那就可以得出一個結(jié)論:上述任意鏈路發(fā)生卡頓,均會造成卡頓。
2.3 生產(chǎn)者和消費者

我們再回到 Vsync 的話題,消費 Vsync 的雙方分別是 APP 和 sf,其中 APP 代表的是生產(chǎn)者,sf 代表的是消費者,兩者交付的中間產(chǎn)物則是 surface buffer。
再具體一點,生產(chǎn)者大致可以分為兩類,一類是以 window 為代表的頁面,也就是我們平時所看到的 view 樹這一套;另一類是以視頻流為代表的可以直接和 surface 完成數(shù)據(jù)交換的來源,比如相機預覽等。
對于一般的生產(chǎn)者和消費者模式,我們知道會存在相互阻塞的問題。比如生產(chǎn)者速度快但是消費者速度慢,亦或是生產(chǎn)者速度慢消費者速度快,都會導致整體速度慢且造成資源浪費。所以 Vsync 的協(xié)同以及雙緩沖甚至三緩沖的作用就體現(xiàn)出來了。
思考一個問題:是否緩沖的個數(shù)越多越好?過多的緩沖會造成什么問題?
答案是會造成另一個嚴重的問題:lag,響應延遲
這里結(jié)合 view 的一生,我們可以把兩個流程合在一起,讓我們的視角再高一層:

2.4 機制上的保護
這里我們來回答第三個問題,從系統(tǒng)的渲染架構(gòu)上來說,機制上的保護主要有幾方面:
Vsync 機制的協(xié)同;
多緩沖設計;
surface 的提供;
同步屏障的保護;
硬件繪制的支持;
渲染線程的支持;
GPU 合成加速;
這些機制上的保護在系統(tǒng)層面最大程度地保障了 APP 體驗的流暢性,但是并不能幫我們徹底解決卡頓。為了提供更加流暢的體驗,一方面,我們可以加強系統(tǒng)的機制保護,比如 FWatchDog;另一方面,需要我們從 APP 的角度入手,治理應用內(nèi)的卡頓問題。
2.5 再看卡頓的成因
經(jīng)過上面的討論,我們得出一個卡頓分析的核心理論支撐:渲染機制中的任何流轉(zhuǎn)過程發(fā)生異常,均會造成卡頓。
那么接下來,我們逐個分析,看看都會有哪些原因可能造成卡頓。
2.5.1 渲染流程
Vsync 調(diào)度:這個是起始點,但是調(diào)度的過程會經(jīng)過線程切換以及一些委派的邏輯,有可能造成卡頓,但是一般可能性比較小,我們也基本無法介入;
消息調(diào)度:主要是 doframe Message 的調(diào)度,這就是一個普通的 Handler 調(diào)度,如果這個調(diào)度被其他的 Message 阻塞產(chǎn)生了時延,會直接導致后續(xù)的所有流程不會被觸發(fā)。這里直播建立了一個 FWtachDog 機制,可以通過優(yōu)化消息調(diào)度達到插幀的效果,使得界面更加流暢;
input 處理:input 是一次 Vsync 調(diào)度最先執(zhí)行的邏輯,主要處理 input 事件。如果有大量的事件堆積或者在事件分發(fā)邏輯中加入大量耗時業(yè)務邏輯,會造成當前幀的時長被拉大,造成卡頓。抖音基礎技術同學也有嘗試過事件采樣的方案,減少 event 的處理,取得了不錯的效果;
動畫處理:主要是 animator 動畫的更新,同理,動畫數(shù)量過多,或者動畫的更新中有比較耗時的邏輯,也會造成當前幀的渲染卡頓。對動畫的降幀和降復雜度其實解決的就是這個問題;
view 處理:主要是接下來的三大流程,過度繪制、頻繁刷新、復雜的視圖效果都是此處造成卡頓的主要原因。比如我們平時所說的降低頁面層級,主要解決的就是這個問題;
measure/layout/draw:view 渲染的三大流程,因為涉及到遍歷和高頻執(zhí)行,所以這里涉及到的耗時問題均會被放大,比如我們會降不能在 draw 里面調(diào)用耗時函數(shù),不能 new 對象等等;
DisplayList 的更新:這里主要是 canvas 和 displaylist 的映射,一般不會存在卡頓問題,反而可能存在映射失敗導致的顯示問題;
OpenGL 指令轉(zhuǎn)換:這里主要是將 canvas 的命令轉(zhuǎn)換為 OpenGL 的指令,一般不存在問題。不過這里倒是有一個可以探索的點,會不會存在一類特殊的 canvas 指令,轉(zhuǎn)換后的 OpenGL 指令消耗比較大,進而導致 GPU 的損耗?有了解的同學可以探討一下;
buffer 交換:這里主要指 OpenGL 指令集交換給 GPU,這個一般和指令的復雜度有關。一個有意思的事兒是這里一度被我們作為線上采集 GPU 指標的數(shù)據(jù)源,但是由于多緩沖的因素數(shù)據(jù)準確度不夠被放棄了;
GPU 處理:顧名思義,這里是 GPU 對數(shù)據(jù)的處理,耗時主要和任務量和紋理復雜度有關。這也就是我們降低 GPU 負載有助于降低卡頓的原因;
layer 合成:這里主要是 layer 的 compose 的工作,一般接觸不到。偶爾發(fā)現(xiàn) sf 的 vsync 信號被 delay 的情況,造成 buffer 供應不及時,暫時還不清楚原因;
光柵化/Display:這里暫時忽略,底層系統(tǒng)行為;
Buffer 切換:主要是屏幕的顯示,這里 buffer 的數(shù)量也會影響幀的整體延遲,不過是系統(tǒng)行為,不能干預。
2.5.2 視頻流
除了上述的渲染流程引起的卡頓,還有一些其他的因素,典型的就是視頻流。
渲染卡頓:主要是 TextureView 渲染,textureview 跟隨 window 共用一個 surface,每一幀均需要一起協(xié)同渲染并相互影響,UI 卡頓會造成視頻流卡頓,視頻流的卡頓有時候也會造成 UI 的卡頓;
解碼:解碼主要是將數(shù)據(jù)流解碼為 surface 可消費的 buffer 數(shù)據(jù),是除了網(wǎng)絡外最重要的耗時點?,F(xiàn)在我們一般都會采用硬解,比軟解的性能高很多。但是幀的復雜度、編碼算法的復雜度、分辨率等也會直接導致解碼耗時被拉長;
OpenGL 處理:有時會對解碼完成的數(shù)據(jù)做二次處理,這個如果比較耗時會直接導致渲染卡頓;
網(wǎng)絡:這個就不再贅述了,包括 DNS 節(jié)點優(yōu)選、cdn 服務、GOP 配置等;
推流異常:這個屬于數(shù)據(jù)源出了問題,這里暫時以用戶側(cè)的視角為主,暫不討論。
2.5.3 系統(tǒng)負載
內(nèi)存:內(nèi)存的吃緊會直接導致 GC 的增加甚至 ANR,是造成卡頓的一個不可忽視的因素;
CPU:CPU 對卡頓的影響主要在于線程調(diào)度慢、任務執(zhí)行的慢和資源競爭,比如降頻會直接導致應用卡頓;
GPU:GPU 的影響見渲染流程,但是其實還會間接影響到功耗和發(fā)熱;
功耗/發(fā)熱:功耗和發(fā)熱一般是不分家的,高功耗會引起高發(fā)熱,進而會引起系統(tǒng)保護,比如降頻、熱緩解等,間接的導致卡頓。
2.6 卡頓的分類
我們此處再整體整理并歸類,為了更完備一些,這里將推流也放了上來。在一定程度上,我們遇到的所有卡頓問題,均能在這里找到理論依據(jù),這也是指導我們優(yōu)化卡頓問題的理論支撐。
3. 如何評價卡頓
3.1 線上指標

3.2 線下指標
Diggo 是字節(jié)自研的一個開放的開發(fā)調(diào)試工具平臺,是一個集「評價、分析、調(diào)試」為一體的,一站式工具平臺。內(nèi)置性能測評、界面分析、卡頓分析、內(nèi)存分析、崩潰分析、即時調(diào)試等基礎分析能力,可為產(chǎn)品開發(fā)階段提供強大助力。

4. 如何優(yōu)化卡頓
4.1 常用的工具
4.1.1 線上工具

4.1.2 線下工具

4.2 常用的思路
這里主要針對 UI 卡頓和 UI/流相互影響打來的卡頓。
對于 UI 卡頓來說,我們手握卡頓優(yōu)化的 8 板大斧子,所向披靡:
下線代碼;
減少執(zhí)行次數(shù);
異步;
打散;
預熱;
復用;
方案優(yōu)化;
硬件加速;
總體思路就是「能不干就不干、能少干就少干、能早點干就早點兒干、能晚點兒干就晚點兒干、能讓別人干就讓別人干、能干完一次當 10 次就只干一次,實在不行,再考慮自己大干一場」。
這里例舉出一些常見的優(yōu)化思路,注意這一定也不可能是全部,如果有其他好的優(yōu)化思路,我們可以一起交流。


4.3 一些做過的事兒
4.3.1 解決 UI 卡頓引起的流卡頓
直播對于?SurfaceView?的切換是一個長期的專項,分為多期逐步將 SurfaceView 在直播全量落地,場景覆蓋秀場直播、聊天室、游戲直播、電商直播、媒體直播等,業(yè)務上對于滲透率和停留時長有比較顯著的收益,同時功耗的收益也很可觀。
這里是一個權(quán)衡的問題,SurfaceView 的兼容性問題 pk 帶來的收益是否能打平,一般來說,越是復雜的業(yè)務場景,收益約大。
4.3.2 解決 message 調(diào)度
FWatchDog 是基于對 MessageQueue 的調(diào)度策略和同步屏障原理,以均幀耗時為閾值判定丟幀后主動在 MessageQueue 中插入同步屏障,保證渲染異步 message 和 doframe 的優(yōu)先執(zhí)行,達到一種渲染插幀的效果,同時具備 ANR 自動恢復同步屏障的能力,保障打散的有效。
所以?FWatchDog 和打散是好的搭檔,能產(chǎn)生 1+1 大于 2 的效果。
4.3.3 減少執(zhí)行次數(shù)
一個典型的應用場景就是滑動場景的 GC 抑制,能夠顯著提高用戶上下滑的使用體驗。這個場景相信每個業(yè)務都會存在,特別是存在大量遍歷的邏輯,優(yōu)化效果明顯。
4.3.4 代碼下線
一些老的框架、無用的邏輯以及存在性不高的代碼都可以下線,這里基本業(yè)務強相關,就不舉具體的例子了。
4.3.5 解決耗時函數(shù)(打散/異步)
首先是打散,直播做了很多 task 的拆分以及打散,第一可以減輕當前渲染幀的耗時壓力,第二可以和 FWatchDog 結(jié)合達到插幀的效果。這里其實還可以控制 task 的執(zhí)行優(yōu)先級,包括隊列的插隊等,總之 MessageQueue 的合理調(diào)度是很有必要的。
異步的使用也相對比較多,一個埋點日志的框架,以及一些 inflate 的加載等,都可以使用異步來解決卡頓問題。
4.3.6 預熱
直播提供了一個預熱框架,可以讓直播內(nèi)部的一次性成本邏輯得到在宿主側(cè)執(zhí)行的機會,同時提供完備的隊列優(yōu)先級管理、同步異步管理和 task 生命周期管理,降低直播內(nèi)部首次加載的卡頓問題。
4.3.7 硬件加速
拉高硬件的運行性能,比如 CPU 頻率、GPU 頻率、線程綁大核以及網(wǎng)絡相關的調(diào)優(yōu),從底層提高 APP 的運行體驗。