Android 屏幕刷新機(jī)制

基本概念

首先,先來過一下一些基本概念,摘抄自網(wǎng)上文章android屏幕刷新顯示機(jī)制

在一個(gè)典型的顯示系統(tǒng)中存捺,一般包括CPU、GPU曙蒸、display三個(gè)部分捌治, CPU負(fù)責(zé)計(jì)算數(shù)據(jù),把計(jì)算好數(shù)據(jù)交給GPU,GPU會(huì)對圖形數(shù)據(jù)進(jìn)行渲染逸爵,渲染好后放到buffer里存起來,然后display(有的文章也叫屏幕或者顯示器)負(fù)責(zé)把buffer里的數(shù)據(jù)呈現(xiàn)到屏幕上趋艘。

顯示過程,簡單的說就是CPU/GPU準(zhǔn)備好數(shù)據(jù)瓷胧,存入buffer杂数,display每隔一段時(shí)間去buffer里取數(shù)據(jù),然后顯示出來那伐。display讀取的頻率是固定的,比如每個(gè)16ms讀一次诉探,但是CPU/GPU寫數(shù)據(jù)是完全無規(guī)律的。

上述內(nèi)容概括一下阳液,大體意思就是說,屏幕的刷新包括三個(gè)步驟:CPU 計(jì)算屏幕數(shù)據(jù)鹰溜、GPU 進(jìn)一步處理和緩存、最后 display 再將緩存中(buffer)的屏幕數(shù)據(jù)顯示出來墓陈。

對于 Android 而言,第一個(gè)步驟:CPU 計(jì)算屏幕數(shù)據(jù)指的也就是 View 樹的繪制過程,也就是 Activity 對應(yīng)的視圖樹從根布局 DecorView 開始層層遍歷每個(gè) View利花,分別執(zhí)行測量兑徘、布局欲侮、繪制三個(gè)操作的過程刁俭。

image.png

Display 這一行可以理解成屏幕如孝,所以可以看到,底層是以固定的頻率發(fā)出 VSync 信號的,而這個(gè)固定頻率就是我們常說的每 16.6ms 發(fā)送一個(gè) VSync 信號,至于什么叫 VSync 信號腔稀,我們可以不用深入去了解窄陡,只要清楚這個(gè)信號就是屏幕刷新的信號就可以了。

看圖润歉,CPU 藍(lán)色的這行,CPU 這塊的耗時(shí)其實(shí)就是我們 app 繪制當(dāng)前 View 樹的時(shí)間颈抚,而這段時(shí)間就跟我們自己寫的代碼有關(guān)系了贩汉,如果你的布局很復(fù)雜匹舞,層次嵌套很多晰绎,每一幀內(nèi)需要刷新的 View 又很多時(shí)锄弱,那么每一幀的繪制耗時(shí)自然就會(huì)多一點(diǎn)祸憋。

看圖会宪,CPU 藍(lán)色這行里也有一些數(shù)字,其實(shí)這些數(shù)字跟 Display 黃色的那一行里的數(shù)字是對應(yīng)的拦赠,在 Display 里這些數(shù)字表示的是每一幀的畫面巍沙,那么在 CPU 這一行里,其實(shí)就是在計(jì)算對應(yīng)幀的畫面數(shù)據(jù)荷鼠,也叫屏幕數(shù)據(jù)句携。也就是說,在當(dāng)前幀內(nèi)允乐,CPU 是在計(jì)算下一幀的屏幕畫面數(shù)據(jù)矮嫉,當(dāng)屏幕刷新信號到的時(shí)候削咆,屏幕就去將 CPU 計(jì)算的屏幕畫面數(shù)據(jù)顯示出來;同時(shí) CPU 也接收到屏幕刷新信號蠢笋,所以也開始去計(jì)算下一幀的屏幕畫面數(shù)據(jù)拨齐。

CPU 跟 Display 是不同的硬件,它們是可以并行工作的昨寞。要理解的一點(diǎn)是瞻惋,我們寫的代碼,只是控制讓 CPU 在接收到屏幕刷新信號的時(shí)候開始去計(jì)算下一幀的畫面工作援岩。而底層在每一次屏幕刷新信號來的時(shí)候都會(huì)去切換這一幀的畫面熟史,這點(diǎn)我們是控制不了的,是底層的工作機(jī)制窄俏。

當(dāng)我們的 app 界面沒有必要再刷新時(shí)(比如用戶不操作了蹂匹,當(dāng)前界面也沒動(dòng)畫),這個(gè)時(shí)候凹蜈,我們 app 是接收不到屏幕刷新信號的限寞,所以也就不會(huì)讓 CPU 去計(jì)算下一幀畫面數(shù)據(jù),但是底層仍然會(huì)以固定的頻率來切換每一幀的畫面仰坦,只是它后面切換的每一幀畫面都一樣履植,所以給我們的感覺就是屏幕沒刷新。

ViewRootImpl 和DecorView的綁定

分析 View#invalidate() 時(shí)悄晃,也可以看到內(nèi)部其實(shí)是有一個(gè) do{}while() 循環(huán)來不斷尋找 mParent玫霎,所以最終才會(huì)走到 ViewRootImpl 里去,那么可能大伙就會(huì)疑問了妈橄,為什么 DecorView 的 mParent 會(huì)是 ViewRootImpl 呢庶近?換個(gè)問法也就是,在什么時(shí)候?qū)?DevorView 和 ViewRootImpl 綁定起來眷蚓?

Activity 的啟動(dòng)是在 ActivityThread 里完成的鼻种,handleLaunchActivity() 會(huì)依次間接的執(zhí)行到 Activity 的 onCreate(), onStart(), onResume()。在執(zhí)行完這些后 ActivityThread 會(huì)調(diào)用 WindowManager#addView()沙热,而這個(gè) addView() 最終其實(shí)是調(diào)用了 WindowManagerGlobal 的 addView() 方法叉钥。

這里初始化了一個(gè) ViewRootImpl,然后調(diào)用了它的 setView() 方法篙贸,將 DevorView 作為參數(shù)傳遞了進(jìn)去投队,這個(gè)方法里還調(diào)用了一個(gè) requestLayout() 方法,進(jìn)而又調(diào)用了一個(gè) scheduleTraversals()爵川。

setView() 方法里調(diào)用了 DecorView 的 assignParent() 方法敷鸦,參數(shù)是 ViewParent,而 ViewRootImpl 是實(shí)現(xiàn)了 ViewParent 接口的,所以在這里就將 DecorView 和 ViewRootImpl 綁定起來了轧膘。

每個(gè)Activity 的根布局都是 DecorView钞螟,而 DecorView 的 parent 又是 ViewRootImpl兔甘,所以在子 View 里執(zhí)行 invalidate() 之類的操作谎碍,循環(huán)找 parent 時(shí),最后都會(huì)走到 ViewRootImpl 里來洞焙。

即使是界面上一個(gè)小小的 View 發(fā)起了重繪請求時(shí)蟆淀,都要層層走到 ViewRootImpl,由它來發(fā)起重繪請求澡匪,然后再由它來開始遍歷 View 樹熔任,一直遍歷到這個(gè)需要重繪的 View 再調(diào)用它的 onDraw()方法進(jìn)行繪制。

總結(jié)一下:其實(shí)打開一個(gè) Activity唁情,當(dāng)它的 onCreate---onResume 生命周期都走完后疑苔,才將它的 DecoView 與新建的一個(gè) ViewRootImpl 對象綁定起來,同時(shí)開始安排一次遍歷 View 任務(wù)也就是繪制 View 樹的操作等待執(zhí)行甸鸟,然后將 DecoView 的 parent 設(shè)置成 ViewRootImpl 對象惦费。

ViewRootImpl#scheduleTraversals

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();//往主線程的消息隊(duì)列中發(fā)送一個(gè)同步屏障,攔截這個(gè)時(shí)刻之后所有的同步消息的執(zhí)行抢韭,但不會(huì)攔截異步消息薪贫,以此來盡可能的保證當(dāng)接收到屏幕刷新信號時(shí)可以盡可能第一時(shí)間處理遍歷繪制 View 樹的工作;

            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//把 performTraversals() 封裝到 Runnable 里面刻恭,然后調(diào)用 Choreographer 的 postCallback() 方法瞧省;postCallback() 方法會(huì)先將這個(gè) Runnable 任務(wù)以當(dāng)前時(shí)間戳放進(jìn)一個(gè)待執(zhí)行的隊(duì)列里,然后如果當(dāng)前是在主線程就會(huì)直接調(diào)用一個(gè)native 層方法鳍贾,如果不是在主線程鞍匾,會(huì)發(fā)一個(gè)最高優(yōu)先級的 message 到主線程,讓主線程第一時(shí)間調(diào)用這個(gè) native 層的方法骑科;
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
View刷新.png
  1. 界面上任何一個(gè) View 的刷新請求最終都會(huì)走到 ViewRootImpl 中的 scheduleTraversals() 里來安排一次遍歷繪制 View 樹的任務(wù)候学;
  2. scheduleTraversals() 會(huì)先過濾掉同一幀內(nèi)的重復(fù)調(diào)用,在同一幀內(nèi)只需要安排一次遍歷繪制 View 樹的任務(wù)即可纵散,這個(gè)任務(wù)會(huì)在下一個(gè)屏幕刷新信號到來時(shí)調(diào)用 performTraversals() 遍歷 View 樹梳码,遍歷過程中會(huì)將所有需要刷新的 View 進(jìn)行重繪;
  3. 接著 scheduleTraversals() 會(huì)往主線程的消息隊(duì)列中發(fā)送一個(gè)同步屏障伍掀,攔截這個(gè)時(shí)刻之后所有的同步消息的執(zhí)行掰茶,但不會(huì)攔截異步消息,以此來盡可能的保證當(dāng)接收到屏幕刷新信號時(shí)可以盡可能第一時(shí)間處理遍歷繪制 View 樹的工作蜜笤;
  4. 發(fā)完同步屏障后 scheduleTraversals() 才會(huì)開始安排一個(gè)遍歷繪制 View 樹的操作濒蒋,作法是把 performTraversals() 封裝到 Runnable 里面,然后調(diào)用 Choreographer 的 postCallback() 方法;
  5. postCallback() 方法會(huì)先將這個(gè) Runnable 任務(wù)以當(dāng)前時(shí)間戳放進(jìn)一個(gè)待執(zhí)行的隊(duì)列里沪伙,然后如果當(dāng)前是在主線程就會(huì)直接調(diào)用一個(gè)native 層方法瓮顽,如果不是在主線程,會(huì)發(fā)一個(gè)最高優(yōu)先級的 message 到主線程围橡,讓主線程第一時(shí)間調(diào)用這個(gè) native 層的方法暖混;
  6. native 層的這個(gè)方法是用來向底層注冊監(jiān)聽下一個(gè)屏幕刷新信號,當(dāng)下一個(gè)屏幕刷新信號發(fā)出時(shí)翁授,底層就會(huì)回調(diào) Choreographer 的onVsync() 方法來通知上層 app拣播;
  7. onVsync() 方法被回調(diào)時(shí),會(huì)往主線程的消息隊(duì)列中發(fā)送一個(gè)執(zhí)行 doFrame() 方法的消息收擦,這個(gè)消息是異步消息贮配,所以不會(huì)被同步屏障攔截住塞赂;
  8. doFrame() 方法會(huì)去取出之前放進(jìn)待執(zhí)行隊(duì)列里的任務(wù)來執(zhí)行泪勒,取出來的這個(gè)任務(wù)實(shí)際上是 ViewRootImpl 的 doTraversal() 操作;
  9. 上述第4步到第8步涉及到的消息都手動(dòng)設(shè)置成了異步消息宴猾,所以不會(huì)受到同步屏障的攔截圆存;
  10. doTraversal() 方法會(huì)先移除主線程的同步屏障,然后調(diào)用 performTraversals() 開始根據(jù)當(dāng)前狀態(tài)判斷是否需要執(zhí)行performMeasure() 測量鳍置、perfromLayout() 布局辽剧、performDraw() 繪制流程,在這幾個(gè)流程中都會(huì)去遍歷 View 樹來刷新需要更新的View税产;

ref:

Android 屏幕刷新機(jī)制

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末怕轿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子辟拷,更是在濱河造成了極大的恐慌撞羽,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件衫冻,死亡現(xiàn)場離奇詭異诀紊,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)隅俘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進(jìn)店門邻奠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人为居,你說我怎么就攤上這事碌宴。” “怎么了蒙畴?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵贰镣,是天一觀的道長呜象。 經(jīng)常有香客問我,道長碑隆,這世上最難降的妖魔是什么恭陡? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮上煤,結(jié)果婚禮上休玩,老公的妹妹穿的比我還像新娘。我一直安慰自己楼入,他們只是感情好哥捕,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布牧抽。 她就那樣靜靜地躺著嘉熊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪扬舒。 梳的紋絲不亂的頭發(fā)上阐肤,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天,我揣著相機(jī)與錄音讲坎,去河邊找鬼孕惜。 笑死,一個(gè)胖子當(dāng)著我的面吹牛晨炕,可吹牛的內(nèi)容都是我干的衫画。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼瓮栗,長吁一口氣:“原來是場噩夢啊……” “哼削罩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起费奸,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后髓削,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體回挽,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年缨历,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了以蕴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,503評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡辛孵,死狀恐怖丛肮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情觉吭,我是刑警寧澤腾供,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響伴鳖,放射性物質(zhì)發(fā)生泄漏节值。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一榜聂、第九天 我趴在偏房一處隱蔽的房頂上張望搞疗。 院中可真熱鬧,春花似錦须肆、人聲如沸匿乃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽幢炸。三九已至,卻和暖如春拒贱,著一層夾襖步出監(jiān)牢的瞬間宛徊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工逻澳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闸天,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓斜做,卻偏偏與公主長得像苞氮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子瓤逼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容