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

關(guān)于CPU和GPU都是通過總線連接起來的,在CPU當(dāng)中輸出的往往是一個位圖,再經(jīng)由總線在合適的時機(jī)傳遞個GPU
GPU拿到這個位圖之后,會對這個位圖的圖層進(jìn)行渲染,包括紋理的合成等
之后會把這個結(jié)果放到幀緩沖區(qū)中,然后視頻控制器會按照VSync信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器,達(dá)到最終的顯示效果
那么接下來讓我們看一下CPU和GPU分別做了哪些事情

首先當(dāng)我們創(chuàng)建一個UIView控件的時候,其中負(fù)責(zé)顯示的CALayer
CALayer中有一個contents屬性,就是我們最終要繪制到屏幕上的一個位圖,比如說我們創(chuàng)建了一個UILabel,那么在contents里面就放了一個關(guān)于Hello world的文字位圖
然后系統(tǒng)會在一個合適的時機(jī)回調(diào)給我們一個drawRect:的方法,這個方法中我們可以去繪制一些自定義的內(nèi)容
繪制好了之后,最終會由Core Animation這個框架提交給GPU部分的OpenGL渲染管線,進(jìn)行最終的位圖的渲染,包括紋理合成等,然后顯示在屏幕上
那么CPU和GPU具體做了哪些工作承擔(dān)呢
CPU
具體分為四個階段
Layout:這里主要涉及到一些UI布局,文本計(jì)算等,例如一個label的size
Display:繪制階段,例如drawRect方法就在這一步驟中
Prepare:圖片的編解碼等操作在此步驟中
Commit:提交位圖
GPU渲染管線
頂點(diǎn)著色
圖元裝配
光柵化
片段著色
片段處理
UI卡頓、掉幀的原因

在顯示器中是固定的頻率,比如iOS中是每秒60幀(60FPS),即每幀16.7ms
從上圖中可以看出,每兩個VSync信號之間有時間間隔(16.7ms),在這個時間內(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)出一幀緩沖,那么這一幀會被丟棄,顯示器就會保持不變,繼續(xù)顯示上一幀內(nèi)容,這就將導(dǎo)致導(dǎo)致畫面卡頓
所以無論CPU,GPU,哪個消耗時間過長,都會導(dǎo)致在16.7ms內(nèi)無法生成一幀緩存
卡頓、掉幀優(yōu)化方案切入點(diǎn)
CPU
CPU在準(zhǔn)備下一幀的所做的工作非常多導(dǎo)致耗時,基于減輕CPU工作時長和壓力來達(dá)到一個優(yōu)化效果
1、部分對象的創(chuàng)建、調(diào)整和銷毀可以放到子線程去做
2、預(yù)排版( 布局計(jì)算、文本計(jì)算),這些計(jì)算也可以放到子線程去做,這樣主線程也可以有更多的時間去響應(yīng)用戶的交互
3、預(yù)渲染(文本等異步繪制、圖片編解碼等)-
GPU
1、紋理渲染:假如說我們觸發(fā)了離屏渲染,例如我們設(shè)置圓角時對maskToBounds的設(shè)置,包括一些陰影、蒙層等都會觸發(fā)GPU層面的離屏渲染,對于這種情況下,GPU對于紋理渲染的工作量就會非常的大,我們可以基于此對GPU進(jìn)行優(yōu)化,就是盡量減少離屏渲染,我們也可以通過CPU的異步繪制來減輕GPU的壓力2、視圖混合: 比如說我們視圖層級比較復(fù)雜,視圖之間層層疊加,那么GPU就要做每一個視圖的合成,合成每一個像素點(diǎn)的像素值,如果我們可以減少視圖的層級,也是可以減輕GPU的壓力,我們也可以通過CPU的異步繪制機(jī)制來達(dá)到一個提交的位圖本身就是一個層級比較少的位圖
UIView的繪制原理
流程圖

當(dāng)我們調(diào)用[UIView setNeedsDisplay]這個方法時,其實(shí)并沒有立即進(jìn)行繪制工作,系統(tǒng)會立刻調(diào)用CALayer的同名方法,并且會在當(dāng)前l(fā)ayer上打上一個標(biāo)記,然后會在當(dāng)前runloop將要結(jié)束的時候調(diào)用[CALayer display]這個方法,然后進(jìn)入我們視圖的真正繪制過程
而在[CALayer display]這個方法的內(nèi)部實(shí)現(xiàn)中會判斷這個layer的delegate是否響應(yīng)displayLayer:這個方法,如果不響應(yīng)這個方法,就會進(jìn)入到系統(tǒng)繪制流程中;如果響應(yīng)這個方法,那么就會為我們提供異步繪制的入口
上面就是UIView的繪制原理,接下來我們看一下系統(tǒng)繪制流程是怎樣的
老規(guī)矩,先上流程圖

在CALayer內(nèi)部會先創(chuàng)建backing store,我可以理解為CGContext,我們一般在drawRect:方法中通過上下文堆棧當(dāng)中取出棧頂?shù)腸ontext,也就是上下文
然后這個layer會判斷是否有代理,如果沒有代理,那么就會調(diào)用[CALayer drawInCotext:];如果有代理,會調(diào)用代理的drawLayer:inContext:方法,然后做當(dāng)前視圖的繪制工作(這一步是發(fā)生在系統(tǒng)內(nèi)部的),然后在一個合適的時機(jī)給與我們這個十分熟悉的[UIView drawRect:]方法的回調(diào),[UIView drawRect:]這個方法默認(rèn)是什么都不做,,系統(tǒng)給我們開這個口子是為了讓我們可以再做一些其他的繪制工作
然后無論是哪個分支,最終都會由CALayer上傳對應(yīng)的backing store(可以理解為位圖)給GPU,然后就結(jié)束了系統(tǒng)默認(rèn)的繪制流程
那么問題來了,我們?nèi)绾芜M(jìn)行異步繪制呢
實(shí)際上我們就需要借用系統(tǒng)給開的這個口子,即[layer.delegate displayLayer:]
在這個異步繪制過程中就需要代理負(fù)責(zé)生成對應(yīng)的bitmap(位圖)
同時設(shè)置bitmap作為layer.contents屬性的值
國際慣例,流程圖走一波(原諒我畫圖能力實(shí)在有限TT)

假如說我們在某一個時機(jī)調(diào)用了[view setNeedsDisplay]這個方法,系統(tǒng)會在當(dāng)前runloop將要結(jié)束的時候調(diào)用[CALyer display]方法,然后如果我們這個layer的代理實(shí)現(xiàn)了[view displayLayer]這個方法
然后會通過子線程的切換,我們在子線程中去做一個位圖的繪制,主線程可以去做一些其他的操作
在子線程中第一步先通過CGBitmapContextCreate()方法來創(chuàng)建一個位圖的上下文,然后我們通過CoreGraphic API可以做當(dāng)前UI控件的一些繪制工作,最后我們再通過CGBitmapContextCreateImage()這個函數(shù)來根據(jù)當(dāng)前所繪制的上下文來生成一張CGImage圖片
最后回到主線程來提交這個位圖,設(shè)置layer的contents屬性,這樣就完成了一個UI控件的異步繪制過程
離屏渲染 (便于理解視圖卡頓、掉幀中對GPU的開銷)
離屏渲染指的是GPU在當(dāng)前屏幕緩沖區(qū)以外開辟了一個緩沖區(qū)進(jìn)行渲染操作
當(dāng)前屏幕渲染不需要額外創(chuàng)建新的緩存,也不需要開啟新的上下文,相對于離屏渲染性能更好。但是受當(dāng)前屏幕渲染的局限因素限制(只有自身上下文、屏幕緩存有限等),當(dāng)前屏幕渲染有些情況下的渲染解決不了的,就使用到離屏渲染
離屏渲染對性能的的代價是很高的,主要體現(xiàn)在:
創(chuàng)建了新的緩沖區(qū)
上下文的頻繁切換
導(dǎo)致產(chǎn)生離屏渲染的原因:
shouldRasterize(光柵化)
masks(遮罩)
shadows(陰影)
edge antialiasing(抗鋸齒)
group opacity(不透明)
復(fù)雜形狀設(shè)置圓角等
漸變