iOS探索:UI視圖之卡頓、掉幀及繪制原理

在開始理解卡頓、掉幀及繪制原理前,首先讓我們先了解下圖像的顯示原理

圖像顯示原理

WX20181206-150708@2x.png
  • 關(guān)于CPU和GPU都是通過總線連接起來的,在CPU當(dāng)中輸出的往往是一個(gè)位圖,再經(jīng)由總線在合適的時(shí)機(jī)傳遞個(gè)GPU

  • GPU拿到這個(gè)位圖之后,會(huì)對(duì)這個(gè)位圖的圖層進(jìn)行渲染,包括紋理的合成等

  • 之后會(huì)把這個(gè)結(jié)果放到幀緩沖區(qū)中,然后視頻控制器會(huì)按照VSync信號(hào)逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器,達(dá)到最終的顯示效果

那么接下來讓我們看一下CPU和GPU分別做了哪些事情

WX20181206-153514@2x.png
  • 首先當(dāng)我們創(chuàng)建一個(gè)UIView控件的時(shí)候,其中負(fù)責(zé)顯示的CALayer

  • CALayer中有一個(gè)contents屬性,就是我們最終要繪制到屏幕上的一個(gè)位圖,比如說我們創(chuàng)建了一個(gè)UILabel,那么在contents里面就放了一個(gè)關(guān)于Hello world的文字位圖

  • 然后系統(tǒng)會(huì)在一個(gè)合適的時(shí)機(jī)回調(diào)給我們一個(gè)drawRect:的方法,這個(gè)方法中我們可以去繪制一些自定義的內(nèi)容

  • 繪制好了之后,最終會(huì)由Core Animation這個(gè)框架提交給GPU部分的OpenGL渲染管線,進(jìn)行最終的位圖的渲染,包括紋理合成等,然后顯示在屏幕上

那么CPU和GPU具體做了哪些工作承擔(dān)呢

CPU

具體分為四個(gè)階段

  • Layout:這里主要涉及到一些UI布局,文本計(jì)算等,例如一個(gè)label的size

  • Display:繪制階段,例如drawRect方法就在這一步驟中

  • Prepare:圖片的編解碼等操作在此步驟中

  • Commit:提交位圖

GPU渲染管線

  • 頂點(diǎn)著色

  • 圖元裝配

  • 光柵化

  • 片段著色

  • 片段處理

UI卡頓、掉幀的原因

WX20181206-160621@2x.png

在顯示器中是固定的頻率,比如iOS中是每秒60幀(60FPS),即每幀16.7ms

從上圖中可以看出,每?jī)蓚€(gè)VSync信號(hào)之間有時(shí)間間隔(16.7ms),在這個(gè)時(shí)間內(nèi),CPU主線程計(jì)算布局,解碼圖片,創(chuàng)建視圖,繪制文本,計(jì)算完成后將內(nèi)容交給GPU,GPU變換,合成,渲染(詳細(xì)可學(xué)習(xí) OpenGL相關(guān)課程),放入幀緩沖區(qū)

假如16.7ms內(nèi),CPU和GPU沒有來得及生產(chǎn)出一幀緩沖,那么這一幀會(huì)被丟棄,顯示器就會(huì)保持不變,繼續(xù)顯示上一幀內(nèi)容,這就將導(dǎo)致導(dǎo)致畫面卡頓

所以無論CPU,GPU,哪個(gè)消耗時(shí)間過長(zhǎng),都會(huì)導(dǎo)致在16.7ms內(nèi)無法生成一幀緩存

卡頓、掉幀優(yōu)化方案切入點(diǎn)

  • CPU
    CPU在準(zhǔn)備下一幀的所做的工作非常多導(dǎo)致耗時(shí),基于減輕CPU工作時(shí)長(zhǎng)和壓力來達(dá)到一個(gè)優(yōu)化效果
    1、部分對(duì)象的創(chuàng)建、調(diào)整和銷毀可以放到子線程去做
    2、預(yù)排版( 布局計(jì)算、文本計(jì)算),這些計(jì)算也可以放到子線程去做,這樣主線程也可以有更多的時(shí)間去響應(yīng)用戶的交互
    3、預(yù)渲染(文本等異步繪制、圖片編解碼等)

  • GPU
    1、紋理渲染:假如說我們觸發(fā)了離屏渲染,例如我們?cè)O(shè)置圓角時(shí)對(duì)maskToBounds的設(shè)置,包括一些陰影、蒙層等都會(huì)觸發(fā)GPU層面的離屏渲染,對(duì)于這種情況下,GPU對(duì)于紋理渲染的工作量就會(huì)非常的大,我們可以基于此對(duì)GPU進(jìn)行優(yōu)化,就是盡量減少離屏渲染,我們也可以通過CPU的異步繪制來減輕GPU的壓力

    2、視圖混合: 比如說我們視圖層級(jí)比較復(fù)雜,視圖之間層層疊加,那么GPU就要做每一個(gè)視圖的合成,合成每一個(gè)像素點(diǎn)的像素值,如果我們可以減少視圖的層級(jí),也是可以減輕GPU的壓力,我們也可以通過CPU的異步繪制機(jī)制來達(dá)到一個(gè)提交的位圖本身就是一個(gè)層級(jí)比較少的位圖

UIView的繪制原理

流程圖
QQ20181206-211905@2x.png
  • 當(dāng)我們調(diào)用[UIView setNeedsDisplay]這個(gè)方法時(shí),其實(shí)并沒有立即進(jìn)行繪制工作,系統(tǒng)會(huì)立刻調(diào)用CALayer的同名方法,并且會(huì)在當(dāng)前l(fā)ayer上打上一個(gè)標(biāo)記,然后會(huì)在當(dāng)前runloop將要結(jié)束的時(shí)候調(diào)用[CALayer display]這個(gè)方法,然后進(jìn)入我們視圖的真正繪制過程

  • 而在[CALayer display]這個(gè)方法的內(nèi)部實(shí)現(xiàn)中會(huì)判斷這個(gè)layer的delegate是否響應(yīng)displayLayer:這個(gè)方法,如果不響應(yīng)這個(gè)方法,就會(huì)進(jìn)入到系統(tǒng)繪制流程中;如果響應(yīng)這個(gè)方法,那么就會(huì)為我們提供異步繪制的入口

上面就是UIView的繪制原理,接下來我們看一下系統(tǒng)繪制流程是怎樣的

老規(guī)矩,先上流程圖
QQ20181206-213639@2x.png
  • 在CALayer內(nèi)部會(huì)先創(chuàng)建backing store,我可以理解為CGContext,我們一般在drawRect:方法中通過上下文堆棧當(dāng)中取出棧頂?shù)腸ontext,也就是上下文

  • 然后這個(gè)layer會(huì)判斷是否有代理,如果沒有代理,那么就會(huì)調(diào)用[CALayer drawInCotext:];如果有代理,會(huì)調(diào)用代理的drawLayer:inContext:方法,然后做當(dāng)前視圖的繪制工作這一步是發(fā)生在系統(tǒng)內(nèi)部的),然后在一個(gè)合適的時(shí)機(jī)給與我們這個(gè)十分熟悉的[UIView drawRect:]方法的回調(diào),[UIView drawRect:]這個(gè)方法默認(rèn)是什么都不做,,系統(tǒng)給我們開這個(gè)口子是為了讓我們可以再做一些其他的繪制工作

  • 然后無論是哪個(gè)分支,最終都會(huì)由CALayer上傳對(duì)應(yīng)的backing store(可以理解為位圖)給GPU,然后就結(jié)束了系統(tǒng)默認(rèn)的繪制流程

那么問題來了,我們?nèi)绾芜M(jìn)行異步繪制呢

實(shí)際上我們就需要借用系統(tǒng)給開的這個(gè)口子,即[layer.delegate displayLayer:]

  • 在這個(gè)異步繪制過程中就需要代理負(fù)責(zé)生成對(duì)應(yīng)的bitmap(位圖)

  • 同時(shí)設(shè)置bitmap作為layer.contents屬性的值

國(guó)際慣例,流程圖走一波(原諒我畫圖能力實(shí)在有限TT)

QQ20181206-220620@2x.png
  • 假如說我們?cè)谀骋粋€(gè)時(shí)機(jī)調(diào)用了[view setNeedsDisplay]這個(gè)方法,系統(tǒng)會(huì)在當(dāng)前runloop將要結(jié)束的時(shí)候調(diào)用[CALyer display]方法,然后如果我們這個(gè)layer的代理實(shí)現(xiàn)了[view displayLayer]這個(gè)方法

  • 然后會(huì)通過子線程的切換,我們?cè)谧泳€程中去做一個(gè)位圖的繪制,主線程可以去做一些其他的操作

  • 在子線程中第一步先通過CGBitmapContextCreate()方法來創(chuàng)建一個(gè)位圖的上下文,然后我們通過CoreGraphic API可以做當(dāng)前UI控件的一些繪制工作,最后我們?cè)偻ㄟ^CGBitmapContextCreateImage()這個(gè)函數(shù)來根據(jù)當(dāng)前所繪制的上下文來生成一張CGImage圖片

  • 最后回到主線程來提交這個(gè)位圖,設(shè)置layer的contents屬性,這樣就完成了一個(gè)UI控件的異步繪制過程

離屏渲染 (便于理解視圖卡頓、掉幀中對(duì)GPU的開銷)

離屏渲染指的是GPU在當(dāng)前屏幕緩沖區(qū)以外開辟了一個(gè)緩沖區(qū)進(jìn)行渲染操作

當(dāng)前屏幕渲染不需要額外創(chuàng)建新的緩存,也不需要開啟新的上下文,相對(duì)于離屏渲染性能更好。但是受當(dāng)前屏幕渲染的局限因素限制(只有自身上下文、屏幕緩存有限等),當(dāng)前屏幕渲染有些情況下的渲染解決不了的,就使用到離屏渲染

離屏渲染對(duì)性能的的代價(jià)是很高的,主要體現(xiàn)在:

  • 創(chuàng)建了新的緩沖區(qū)

  • 上下文的頻繁切換

導(dǎo)致產(chǎn)生離屏渲染的原因:

  • shouldRasterize(光柵化)

  • masks(遮罩)

  • shadows(陰影)

  • edge antialiasing(抗鋸齒)

  • group opacity(不透明)

  • 復(fù)雜形狀設(shè)置圓角等

  • 漸變

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1 CALayer IOS SDK詳解之CALayer(一) http://doc.okbase.net/Hell...
    Kevin_Junbaozi閱讀 5,346評(píng)論 3 23
  • 作為一個(gè)iOS小菜鳥,當(dāng)我們需求做完之后,我們?cè)摳墒裁???dāng)然是學(xué)習(xí)!最近看了很多關(guān)于iOS性能優(yōu)化的文章,為了便于...
    小蝦啦閱讀 3,155評(píng)論 1 4
  • 繪制像素到屏幕上 answer-huang22 Mar 2014 分享文章 一個(gè)像素是如何繪制到屏幕上去的?有很多...
    阿貍旅途T恤閱讀 1,779評(píng)論 0 7
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,689評(píng)論 1 32
  • 卷首語 歡迎來到 objc.io 的第三期! 這一期都是關(guān)于視圖層的。當(dāng)然視圖層有很多方面,我們需要把它們縮小到幾...
    評(píng)評(píng)分分閱讀 1,946評(píng)論 0 18

友情鏈接更多精彩內(nèi)容