又卡了~從王者榮耀看Android屏幕刷新機(jī)制
前言
正在帶妹子上分的我,團(tuán)戰(zhàn)又卡了,我該怎么向妹子解釋?在線等。
“卡”的意思
不管是端游還是手游,我們都會(huì)時(shí)不時(shí)遇到“卡”的時(shí)候,一般這個(gè)卡有兩種含義:
- 掉幀
- 畫面撕裂
那么問題來了,這些情況到底是什么原因?qū)е碌?又該怎么解決?
掉幀
首先,要知道幀是什么,幀率又是什么。
幀,就是影像動(dòng)畫中最小單位的單幅影像畫面,相當(dāng)于電影膠片上的每一格鏡頭。一幀就是一幅靜止的畫面,連續(xù)的幀就形成動(dòng)畫,如電視圖象等。
幀率(每秒幀數(shù)),簡單地說,就是在1秒鐘時(shí)間里傳輸?shù)膱D片的幀數(shù),也可以理解為圖形處理器每秒鐘能夠刷新幾次,通常用fps(Frames Per Second)表示
這下大家應(yīng)該知道了,幀就是一個(gè)靜止畫面,很多個(gè)幀一起就組成了視頻、電影、游戲畫面。
而幀率就是我們游戲常見到的fps,指一秒鐘繪制出現(xiàn)的幀數(shù),單位為“赫茲”(Hz)。
這里簡單科普下,一般要求連貫性的話,幀數(shù)至少要高于每秒約10至12幀的時(shí)候,人眼才會(huì)認(rèn)為是連貫的,此現(xiàn)象叫做“視覺暫留現(xiàn)象”,是由人眼的生物構(gòu)造決定的。通過這個(gè)現(xiàn)象,早期的無聲電影通過手搖驅(qū)動(dòng),將畫面快速播放,就能讓人感覺在播放完整連續(xù)的視頻。
按照我們的認(rèn)知,這個(gè)幀率一般是越大越連貫,就越不卡。但同時(shí),帶來的消耗也就越多,比如電影需要更多的膠卷,電腦需要更好的硬件支持。所以電影一般通用的幀率為24Hz,而電腦、手機(jī)一般幀率為60Hz,這樣就能保證正常條件下能讓人舒服得觀看和使用。
那掉幀的意思就很明顯了,本來游戲的fps為60,突然降到了20,也就是一秒只有20幀了,那能不卡嗎?
那么,掉幀原因到底是啥呢?
其實(shí)原因大家都知道,不信你繼續(xù)看...
硬件原因
“我這個(gè)手機(jī)玩游戲卡死了”
“你那啥破手機(jī)啊,趕快換一個(gè)~”
這個(gè)對(duì)話應(yīng)該時(shí)常發(fā)生,所以大家也都清楚,硬件的好壞一定程度上決定了玩游戲“卡不卡”,配置高的硬件玩游戲就能保證游戲的流暢。
軟件原因
“你這啥App啊,做的啥游戲啊,這么卡,我這手機(jī)配置這么高,就玩你這個(gè)卡”
“額,可能是游戲優(yōu)化沒做好,”
第二個(gè)原因,就是因?yàn)橛螒?軟件自身的優(yōu)化就沒做好,圖片弄的很大,布局嵌套太深,那么幀 的計(jì)算和渲染就更耗時(shí)間,就會(huì)有可能導(dǎo)致掉幀。
網(wǎng)絡(luò)原因
“不行了,太卡了,我這ping都快1000了,怎么玩啊”
“快換流量啊,團(tuán)戰(zhàn)要輸了,少個(gè)人怎么打”
對(duì)了,第三個(gè)原因就是網(wǎng)絡(luò)原因,這也是最常發(fā)生的原因了,網(wǎng)絡(luò)的波動(dòng)會(huì)影響 畫面 的傳輸,所以就會(huì)掉幀。
屏幕刷新機(jī)制
上述三個(gè)原因,其實(shí)都涉及到屏幕刷新的基本機(jī)制。
在典型的顯示系統(tǒng)中,不管是手機(jī)還是電腦,一般都涉及到三個(gè)部分:
- CPU,中央處理器。用于計(jì)算數(shù)據(jù),信息處理。
- GPU,圖形處理器。用于處理圖像圖形,也就是俗稱的顯卡。
- display,顯示屏幕。用于展示畫面,也就是我們的手機(jī)屏幕、電腦顯示器。
整個(gè)顯示過程就是:
- CPU計(jì)算屏幕需要的數(shù)據(jù),然后交給GPU。
- GPU對(duì)圖像進(jìn)行處理繪制,然后存到緩存區(qū)。
- display再從這個(gè)緩存區(qū)讀取數(shù)據(jù),顯示出來。
每一幀都是重復(fù)這個(gè)工作,也就是1秒中需要60次這樣循環(huán)操作,每次操作需要的時(shí)間就約等于16.6ms。也就是我們常說的Android系統(tǒng)中,會(huì)每隔16.6ms刷新一次屏幕。
關(guān)于屏幕刷新機(jī)制,有一張很經(jīng)典的圖片:
可以看到,16.6ms一到,系統(tǒng)就發(fā)送了VSync信號(hào),然后屏幕會(huì)從緩存區(qū)獲取了新的一幀圖像并顯示出來,與此同時(shí),CPU也開始了下一幀數(shù)據(jù)的計(jì)算,然后計(jì)算好交給GPU,最后放到緩存區(qū),等待下一次VSync信號(hào)。
VSync信號(hào)是啥呢?我們暫且按下不表,待會(huì)再說,可以先理解它為一種同步刷新信號(hào),同步CPU和屏幕。當(dāng)信號(hào)來的時(shí)候,屏幕開始切換畫面,CPU開始下一幀計(jì)算。
為了方便理解,我做了個(gè)小動(dòng)畫:
通過上面的解釋,我們知道了一幀顯示的時(shí)間是16.6ms,在這個(gè)時(shí)間內(nèi),CPU和GPU必須把數(shù)據(jù)處理好并放到緩存區(qū)(buffer)中。
如果在某次的16.6ms中,CPU和GPU沒有繪制好下一幀數(shù)據(jù),那么display就無法更新下一幀數(shù)據(jù)了,這就是掉幀。
所以才有了以上三個(gè)原因會(huì)導(dǎo)致掉幀,再來回顧下:
- 1、硬件原因。硬件比較差也就是CPU和GPU計(jì)算不給力,導(dǎo)致一幀的數(shù)據(jù)沒準(zhǔn)備好,所以掉幀。
- 2、軟件原因。在硬件夠用的情況下,App或者游戲的一幀數(shù)據(jù)計(jì)算繁雜,嵌套太多或者圖太大,也會(huì)導(dǎo)致下一幀數(shù)據(jù)不能在16.6ms中被準(zhǔn)備好,導(dǎo)致掉幀。
- 3、網(wǎng)絡(luò)原因。在硬件軟件都正常情況下,由于網(wǎng)絡(luò)波動(dòng),CPU的計(jì)算數(shù)據(jù)都沒有從網(wǎng)絡(luò)上獲取到,那么肯定會(huì)導(dǎo)致CPU數(shù)據(jù)的準(zhǔn)備延遲,最終導(dǎo)致掉幀。
那么掉幀之后,屏幕刷新機(jī)制會(huì)怎么處理后續(xù)的幀呢?
- 如果是游戲的話,因?yàn)榧磿r(shí)性比較重要,所以丟失的幀就不會(huì)再去管了,而是直接準(zhǔn)備當(dāng)前時(shí)間應(yīng)該顯示的內(nèi)容,最終顯示到屏幕。所以這種情況掉的幀就真的掉了。
- 如果是應(yīng)用的話,可能對(duì)數(shù)據(jù)的完整性比較重要,所以如果第2幀掉了,那么屏幕就繼續(xù)顯示第1幀,而CPU和GPU繼續(xù)準(zhǔn)備第2幀的數(shù)據(jù),如果能在下一個(gè)16.6ms內(nèi)完成第2幀數(shù)據(jù),那么屏幕就會(huì)接著顯示第二幀了。比如有時(shí)候手機(jī)卡的時(shí)候,我們?nèi)ゲ僮鰽pp,操作會(huì)延遲,就是掉幀了。這種情況幀并不是真的掉了,而是延遲了。
畫面撕裂
接下來就看看畫面撕裂,為什么一幀中會(huì)出現(xiàn)兩幀的畫面呢?
首先理解一個(gè)概念:「逐行掃描」
「逐行掃描」就是說,顯示器顯示畫面并不是“蹭”一下就打出一張畫面來,而是從上到下一行一行顯示出來的,只不過是顯示得比較快所以肉眼看不出來而已。
之前說了屏幕的數(shù)據(jù)是從緩存區(qū)Buffer中取的,如果在屏幕取數(shù)據(jù)并逐行掃描顯示畫面的過程中,Buffer中的數(shù)據(jù)變了,那么就有可能導(dǎo)致畫面撕裂。
最明顯的例子就是:顯卡的fps是180,而顯示器的fps是60。也就是顯卡一秒鐘能產(chǎn)生180張畫面,而顯示器一秒鐘只能讀取60張畫面。
那么顯示器從Buffer中讀取數(shù)據(jù)逐行掃描的過程中,本來需要1/60 秒顯示完一張畫面,但是在1/180的時(shí)間點(diǎn),顯卡就把下一張畫面的數(shù)據(jù)存到Buffer了,結(jié)果顯示器的下半截就顯示的是第二張畫面的內(nèi)容了。也就造成了畫面撕裂。
再來個(gè)動(dòng)畫解釋下:
所以為了防止這種狀況,一般顯示系統(tǒng)會(huì)加入一個(gè)雙緩存+垂直同步的概念:
- 首先,開啟垂直同步,就會(huì)將GPU的fps限制為和顯示器的fps一樣。
系統(tǒng)會(huì)在顯示器繪制完一幀之后發(fā)送一個(gè)垂直同步信號(hào),然后CPU和GPU就準(zhǔn)備下一幀的內(nèi)容,等待顯示器下一幀繪制完,又會(huì)發(fā)送一個(gè)垂直同步信號(hào)。如此反復(fù),就限制了顯卡的fps,按照顯示器的標(biāo)準(zhǔn)來繪制圖像。
這個(gè)垂直同步信號(hào)就叫做 VSync信號(hào)。
玩游戲的朋友應(yīng)該都知道,很多游戲內(nèi)設(shè)置頁都有 垂直同步 的開啟選項(xiàng),為的就是將顯卡的fps和顯示器的fps適配,防止畫面撕裂。
- 其次,通過雙緩存保證一幀數(shù)據(jù)的連貫性。
1、緩存區(qū)backBuffer用于CPU/GPU圖形處理
2、緩存區(qū)frameBuffer用于顯示器顯示
這樣分工明確之后,屏幕只會(huì)讀取framebuffer的內(nèi)容,是一幀完整的畫面。而CPU/GPU計(jì)算的新一幀內(nèi)容會(huì)放到backbuffer中,不會(huì)影響到framebuffer的內(nèi)容。
只有當(dāng)屏幕繪制完一幀內(nèi)容之后,才會(huì)將CPU/GPU計(jì)算好的新一幀內(nèi)容也就是backbuffer內(nèi)容和framebuffer進(jìn)行交換。
這樣就保證了一幀數(shù)據(jù)的完整連貫。
小結(jié)下就是:當(dāng)屏幕掃描完第1幀畫面之后,系統(tǒng)發(fā)送VSync信號(hào),這時(shí)會(huì)發(fā)生三件事:
- 1、交換兩個(gè)緩存區(qū)(framebuffer、backbuffer)內(nèi)容。
- 2、顯示器開始顯示第2幀內(nèi)容,也就是交換后的framebuffer內(nèi)容。
- 3、CPU/GPU開始計(jì)算處理第三幀的內(nèi)容,并在處理好內(nèi)容后放到backbuffer中。
再來個(gè)動(dòng)畫:
Android Project Butter(黃油計(jì)劃)
問題都解決了嗎?并沒有。
加入VSync信號(hào)之后,掉幀問題變得更嚴(yán)重了:
可以發(fā)現(xiàn),加入了VSync信號(hào)后,雖然統(tǒng)一了CPU處理的時(shí)間點(diǎn),但是掉幀問題可能會(huì)被再一次放大,從掉一幀直接變成后續(xù)一直掉幀。因?yàn)榈诙€(gè)的16.6ms被浪費(fèi)了,CPU必須等到第三個(gè)16.6ms才能開始新一幀的數(shù)據(jù)處理,直接影響后續(xù)的所有幀進(jìn)度。
怎么辦呢?在保留VSync信號(hào)的同時(shí)有可能最大利用上CPU/GPU嗎?
三緩存來了:
1、緩存區(qū)backBuffer用于CPU/GPU圖形處理
2、緩存區(qū)TripleBuffer用于CPU/GPU圖形處理
3、緩存區(qū)frameBuffer用于顯示器顯示
剛才說的情況導(dǎo)致的原因就是因?yàn)樵诘诙€(gè)VSync信號(hào)來的時(shí)候,因?yàn)閎ackBuffer被GPU占用,所以CPU無法去開始新一幀的計(jì)算。
加入了第三個(gè)緩存區(qū),那么在第二個(gè)VSync信號(hào)來的時(shí)候,CPU就可以利用TripleBuffer開始新一幀的計(jì)算,而無視正在被GPU占用的backBuffer。
你可以理解為 CPU、GPU、Display每個(gè)人都有一個(gè)緩存區(qū),這樣三個(gè)就能同時(shí)做自己的事而互不影響,最大化利用每個(gè)模塊。
三緩存和上面說到的Vsync同步信號(hào)都是 Android 4.1 發(fā)布的一個(gè)Project Butter(黃油計(jì)劃)中提出的,為的是就是讓Android能讓黃油/奶油般順滑。
最后貼個(gè)三緩存機(jī)制下的刷新機(jī)制圖:
小結(jié)
今天了解了Android系統(tǒng)的刷新機(jī)制,雖然沒有代碼,但是面試中也是常常被問到的,再次總結(jié)下:
1、為了解決畫面撕裂問題,引入了垂直同步信號(hào)VSync信號(hào)和雙緩存。
每次VSync信號(hào)到達(dá)的時(shí)候,屏幕進(jìn)行畫面切換,CPU/GPU開始準(zhǔn)備下一幀內(nèi)容。
CPU/GPU每次準(zhǔn)備好數(shù)據(jù)后,放到一個(gè)單獨(dú)的緩存區(qū)backBuffer,當(dāng)屏幕準(zhǔn)備好之后,將backBuffer數(shù)據(jù)和frameBuffer數(shù)據(jù)交換,屏幕只讀取frameBuffer緩存區(qū)的數(shù)據(jù),保證了數(shù)據(jù)的完整連續(xù)性。
2、為了解決VSync信號(hào)下CPU/GPU無法最大化利用的問題,引入了三緩存。
當(dāng)VSync信號(hào)來的時(shí)候,即使GPU還沒處理好上一幀數(shù)據(jù),backBuffer還不空閑,但是CPU也可以利用第三個(gè)緩存區(qū)正常開始處理下一幀的數(shù)據(jù),最大化利用CPU/GPU,保證垂直同步機(jī)制的同時(shí)不浪費(fèi)資源。
3、掉幀的根本原因是因?yàn)樵谝粠瑫r(shí)間內(nèi)(一般為16.6ms),CPU/GPU無法把下一幀的數(shù)據(jù)準(zhǔn)備好。
即使引用了三緩存和垂直同步,但是掉幀的情況該發(fā)生還是會(huì)發(fā)生,我們作為App軟件開發(fā)者,能做的就是盡可能優(yōu)化布局,減少嵌套,減少CPU/GPU計(jì)算畫面數(shù)據(jù)的時(shí)間,讓每一幀時(shí)間內(nèi)正常準(zhǔn)備好下一幀圖像數(shù)據(jù)。
至于刷新機(jī)制在Android源碼中到底是怎么實(shí)現(xiàn)的呢?下期會(huì)帶來Choreographer的解析。
參考
https://www.jianshu.com/p/0d00cb85fdf3
https://www.zhihu.com/question/49764664/answer/469342536
https://blog.csdn.net/yangwen123/article/details/16344375