基本概念
在一個典型的顯示系統(tǒng)中公给,一般包括CPU借帘、GPU、display三個部分淌铐, CPU負(fù)責(zé)計(jì)算數(shù)據(jù)肺然,把計(jì)算好數(shù)據(jù)交給GPU,GPU會對圖形數(shù)據(jù)進(jìn)行渲染,渲染好后放到buffer里存起來腿准,然后display(有的文章也叫屏幕或者顯示器)負(fù)責(zé)把buffer里的數(shù)據(jù)呈現(xiàn)到屏幕上际起。
顯示過程,簡單的說就是CPU/GPU準(zhǔn)備好數(shù)據(jù),存入buffer街望,display每隔一段時間去buffer里取數(shù)據(jù)倦沧,然后顯示出來。display讀取的頻率是固定的它匕,比如每個16ms讀一次展融,但是CPU/GPU寫數(shù)據(jù)是完全無規(guī)律的。
CPU 計(jì)算屏幕數(shù)據(jù)豫柬、GPU 進(jìn)一步處理和緩存告希、最后 display 再將緩存中(buffer)的屏幕數(shù)據(jù)顯示出來。
對于 Android 而言烧给,第一個步驟:CPU 計(jì)算屏幕數(shù)據(jù)指的也就是 View 樹的繪制過程燕偶,也就是 Activity 對應(yīng)的視圖樹從根布局 DecorView 開始層層遍歷每個 View,分別執(zhí)行測量础嫡、布局指么、繪制三個操作的過程。
也就是說榴鼎,我們常說的 Android 每隔 16.6ms 刷新一次屏幕其實(shí)是指:底層以固定的頻率伯诬,比如每 16.6ms 將 buffer 里的屏幕數(shù)據(jù)顯示出來。
CPU 跟 Display 是不同的硬件巫财,它們是可以并行工作的盗似。要理解的一點(diǎn)是,我們寫的代碼平项,只是控制讓 CPU 在接收到屏幕刷新信號的時候開始去計(jì)算下一幀的畫面工作赫舒。而底層在每一次屏幕刷新信號來的時候都會去切換這一幀的畫面,這點(diǎn)我們是控制不了的闽瓢,是底層的工作機(jī)制接癌。之所以要講這點(diǎn),是因?yàn)榭鬯希?dāng)我們的 app 界面沒有必要再刷新時(比如用戶不操作了缺猛,當(dāng)前界面也沒動畫),這個時候届谈,我們 app 是接收不到屏幕刷新信號的枯夜,所以也就不會讓 CPU 去計(jì)算下一幀畫面數(shù)據(jù),但是底層仍然會以固定的頻率來切換每一幀的畫面艰山,只是它后面切換的每一幀畫面都一樣湖雹,所以給我們的感覺就是屏幕沒刷新。
Display 這一行可以理解成屏幕曙搬,所以可以看到摔吏,底層是以固定的頻率發(fā)出 VSync 信號的鸽嫂,而這個固定頻率就是我們常說的每 16.6ms 發(fā)送一個 VSync 信號,至于什么叫 VSync 信號征讲,我們可以不用深入去了解据某,只要清楚這個信號就是屏幕刷新的信號就可以了。
CPU:執(zhí)行應(yīng)用層的measure诗箍、layout癣籽、draw等操作,繪制完成后將數(shù)據(jù)提交給GPU
GPU:進(jìn)一步處理數(shù)據(jù)滤祖,并將數(shù)據(jù)緩存起來
屏幕:由一個個像素點(diǎn)組成筷狼,以固定的頻率(16.6ms,即1秒60幀)從緩沖區(qū)中取出數(shù)據(jù)來填充像素點(diǎn)
雙緩沖機(jī)制
看完上面的流程圖匠童,我們很容易想到一個問題埂材,屏幕是以16.6ms的固定頻率進(jìn)行刷新的,但是我們應(yīng)用層觸發(fā)繪制的時機(jī)是完全隨機(jī)的(比如我們隨時都可以觸摸屏幕觸發(fā)繪制)汤求,如果在GPU向緩沖區(qū)寫入數(shù)據(jù)的同時俏险,屏幕也在向緩沖區(qū)讀取數(shù)據(jù),會發(fā)生什么情況呢扬绪?
有可能屏幕上就會出現(xiàn)一部分是前一幀的畫面竖独,一部分是另一幀的畫面,這顯然是無法接受的勒奇,那怎么解決這個問題呢预鬓?
這個其實(shí)和我們平時使用代碼管理工具Git的一些思路有相似之處巧骚,首先我們有一個master分支赊颠,對應(yīng)線上版本的代碼,當(dāng)有新的需求來的時候劈彪,我們往往不會在master分支上直接進(jìn)行開發(fā)竣蹦,都會拉出一個新的分支,比如develop分支沧奴,在develop分支上開發(fā)新需求痘括,等開發(fā)完成測試通過后才會合并到master分支
所以,在屏幕刷新中滔吠,Android系統(tǒng)引入了雙緩沖機(jī)制纲菌。
- GPU只向Back Buffer中寫入繪制數(shù)據(jù),且GPU會定期交換Back Buffer和Frame Buffer疮绷,也就是讓Back Buffer 變成Frame Buffer交給屏幕進(jìn)行繪制翰舌,讓原先的Frame Buffer變成Back Buffer進(jìn)行數(shù)據(jù)寫入。
- 交換的頻率也是60次/秒冬骚,這就與屏幕的刷新頻率保持了同步椅贱。
雖然我們引入了雙緩沖機(jī)制,但是我們知道,當(dāng)布局比較復(fù)雜凑保,或設(shè)備性能較差的時候晒衩,CPU并不能保證在16.6ms內(nèi)就完成繪制數(shù)據(jù)的計(jì)算,所以這里系統(tǒng)又做了一個處理山橄。
當(dāng)你的應(yīng)用正在往Back Buffer中填充數(shù)據(jù)時垮媒,系統(tǒng)會將Back Buffer鎖定。如果到了GPU交換兩個Buffer的時間點(diǎn)航棱,你的應(yīng)用還在往Back Buffer中填充數(shù)據(jù)涣澡,GPU會發(fā)現(xiàn)Back Buffer被鎖定了,它會放棄這次交換丧诺。
這樣做的后果就是手機(jī)屏幕仍然顯示原先的圖像入桂,這就是我們常常說的丟幀,所以為了避免丟幀的發(fā)生驳阎,我們就要盡量減少布局層級抗愁,減少不必要的View的invalidate調(diào)用,減少大量對象的創(chuàng)建(GC也會占用CPU時間)等等呵晚。
QA
Q1:Android 每隔 16.6 ms 刷新一次屏幕到底指的是什么意思蜘腌?是指每隔 16.6ms 調(diào)用 onDraw() 繪制一次么?
Q2:如果界面一直保持沒變的話饵隙,那么還會每隔 16.6ms 刷新一次屏幕么撮珠?
答:我們常說的 Android 每隔 16.6 ms 刷新一次屏幕其實(shí)是指底層會以這個固定頻率來切換每一幀的畫面,而這個每一幀的畫面數(shù)據(jù)就是我們 app 在接收到屏幕刷新信號之后去執(zhí)行遍歷繪制 View 樹工作所計(jì)算出來的屏幕數(shù)據(jù)金矛。而 app 并不是每隔 16.6ms 的屏幕刷新信號都可以接收到芯急,只有當(dāng) app 向底層注冊監(jiān)聽下一個屏幕刷新信號之后,才能接收到下一個屏幕刷新信號到來的通知驶俊。而只有當(dāng)某個 View 發(fā)起了刷新請求時娶耍,app 才會去向底層注冊監(jiān)聽下一個屏幕刷新信號。
也就是說饼酿,只有當(dāng)界面有刷新的需要時榕酒,我們 app 才會在下一個屏幕刷新信號來時,遍歷繪制 View 樹來重新計(jì)算屏幕數(shù)據(jù)故俐。如果界面沒有刷新的需要想鹰,一直保持不變時,我們 app 就不會去接收每隔 16.6ms 的屏幕刷新信號事件了药版,但底層仍然會以這個固定頻率來切換每一幀的畫面辑舷,只是后面這些幀的畫面都是相同的而已。
Q3:界面的顯示其實(shí)就是一個 Activity 的 View 樹里所有的 View 都進(jìn)行測量刚陡、布局惩妇、繪制操作之后的結(jié)果呈現(xiàn)株汉,那么如果這部分工作都完成后,屏幕會馬上就刷新么歌殃?
答:我們 app 只負(fù)責(zé)計(jì)算屏幕數(shù)據(jù)而已乔妈,接收到屏幕刷新信號就去計(jì)算,計(jì)算完畢就計(jì)算完畢了氓皱。至于屏幕的刷新路召,這些是由底層以固定的頻率來切換屏幕每一幀的畫面。所以即使屏幕數(shù)據(jù)都計(jì)算完畢波材,屏幕會不會馬上刷新就取決于底層是否到了要切換下一幀畫面的時機(jī)了股淡。
Q4:網(wǎng)上都說避免丟幀的方法之一是保證每次繪制界面的操作要在 16.6ms 內(nèi)完成,但如果這個 16.6ms 是一個固定的頻率的話廷区,請求繪制的操作在代碼里被調(diào)用的時機(jī)是不確定的啊唯灵,那么如果某次用戶點(diǎn)擊屏幕導(dǎo)致的界面刷新操作是在某一個 16.6ms 幀快結(jié)束的時候,那么即使這次繪制操作小于 16.6 ms隙轻,按道理不也會造成丟幀么埠帕?這又該如何理解?
之所以提了這個問題玖绿,是因?yàn)橹笆且詾槿绻硞€ View 發(fā)起了刷新請求敛瓷,比如調(diào)用了 invalidte(),那么它的重繪工作就馬上開始執(zhí)行了斑匪,所以以前在看網(wǎng)上那些介紹屏幕刷新機(jī)制的博客時呐籽,經(jīng)常看見下面這張圖:
那個時候就是不大理解蚀瘸,為什么每一次 CPU 計(jì)算的工作都剛剛好是在每一個信號到來的那個瞬間開始的呢狡蝶?畢竟代碼里發(fā)起刷新屏幕的操作是動態(tài)的,不可能每次都剛剛好那么巧苍姜。
梳理完屏幕刷新機(jī)制后就清楚了牢酵,代碼里調(diào)用了某個 View 發(fā)起的刷新請求,這個重繪工作并不會馬上就開始衙猪,而是需要等到下一個屏幕刷新信號來的時候才開始,所以現(xiàn)在回過頭來看這些圖就清楚多了布近。
Q5:大伙都清楚垫释,主線程耗時的操作會導(dǎo)致丟幀,但是耗時的操作為什么會導(dǎo)致丟幀撑瞧?它是如何導(dǎo)致丟幀發(fā)生的棵譬?
答:造成丟幀大體上有兩類原因,一是遍歷繪制 View 樹計(jì)算屏幕數(shù)據(jù)的時間超過了 16.6ms预伺;二是订咸,主線程一直在處理其他耗時的消息曼尊,導(dǎo)致遍歷繪制 View 樹的工作遲遲不能開始,從而超過了 16.6 ms 底層切換下一幀畫面的時機(jī)脏嚷。
第一個原因就是我們寫的布局有問題了骆撇,需要進(jìn)行優(yōu)化了。而第二個原因則是我們常說的避免在主線程中做耗時的任務(wù)父叙。
針對第二個原因神郊,系統(tǒng)已經(jīng)引入了同步屏障消息的機(jī)制,盡可能的保證遍歷繪制 View 樹的工作能夠及時進(jìn)行趾唱,但仍沒辦法完全避免涌乳,所以我們還是得盡可能避免主線程耗時工作。
其實(shí)第二個原因甜癞,可以拿出來細(xì)講的夕晓,比如有這種情況, message 不怎么耗時悠咱,但數(shù)量太多运授,這同樣可能會造成丟幀。如果有使用一些圖片框架的乔煞,它內(nèi)部下載圖片都是開線程去下載吁朦,但當(dāng)下載完成后需要把圖片加載到綁定的 view 上,這個工作就是發(fā)了一個 message 切到主線程來做渡贾,如果一個界面這種 view 特別多的話逗宜,隊(duì)列里就會有非常多的 message,雖然每個都 message 并不怎么耗時空骚,但經(jīng)不起量多啊纺讲。
當(dāng)前作為學(xué)習(xí)筆記
推薦閱讀(大神博客)
現(xiàn)在還不清楚Android屏幕刷新原理,那就過分啦囤屹!
Android 屏幕刷新機(jī)制
破譯Android性能優(yōu)化中的16ms問題
android屏幕刷新顯示機(jī)制
Android Choreographer 源碼分析