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

熊貓超人發表於2018-12-06

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

影像顯示原理

WX20181206-150708@2x.png

  • 關於CPU和GPU都是通過匯流排連線起來的,在CPU當中輸出的往往是一個點陣圖,再經由匯流排在合適的時機傳遞個GPU

  • GPU拿到這個點陣圖之後,會對這個點陣圖的圖層進行渲染,包括紋理的合成等

  • 之後會把這個結果放到幀緩衝區中,然後視訊控制器會按照VSync訊號逐行讀取幀緩衝區的資料,經過可能的數模轉換傳遞給顯示器,達到最終的顯示效果

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

WX20181206-153514@2x.png

  • 首先當我們建立一個UIView控制元件的時候,其中負責顯示的CALayer

  • CALayer中有一個contents屬性,就是我們最終要繪製到螢幕上的一個點陣圖,比如說我們建立了一個UILabel,那麼在contents裡面就放了一個關於Hello world的文字點陣圖

  • 然後系統會在一個合適的時機回撥給我們一個drawRect:的方法,這個方法中我們可以去繪製一些自定義的內容

  • 繪製好了之後,最終會由Core Animation這個框架提交給GPU部分的OpenGL渲染管線,進行最終的點陣圖的渲染,包括紋理合成等,然後顯示在螢幕上

那麼CPU和GPU具體做了哪些工作承擔呢

CPU

具體分為四個階段

  • Layout:這裡主要涉及到一些UI佈局,文字計算等,例如一個label的size

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

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

  • Commit:提交點陣圖

GPU渲染管線

  • 頂點著色

  • 圖元裝配

  • 光柵化

  • 片段著色

  • 片段處理

UI卡頓、掉幀的原因

WX20181206-160621@2x.png

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

從上圖中可以看出,每兩個VSync訊號之間有時間間隔(16.7ms),在這個時間內,CPU主執行緒計算佈局,解碼圖片,建立檢視,繪製文字,計算完成後將內容交給GPU,GPU變換,合成,渲染(詳細可學習 OpenGL相關課程),放入幀緩衝區

假如16.7ms內,CPU和GPU沒有來得及生產出一幀緩衝,那麼這一幀會被丟棄,顯示器就會保持不變,繼續顯示上一幀內容,這就將導致導致畫面卡頓

所以無論CPU,GPU,哪個消耗時間過長,都會導致在16.7ms內無法生成一幀快取

卡頓、掉幀優化方案切入點

  • CPU CPU在準備下一幀的所做的工作非常多導致耗時,基於減輕CPU工作時長和壓力來達到一個優化效果 1、部分物件的建立、調整和銷燬可以放到子執行緒去做 2、預排版( 佈局計算、文字計算),這些計算也可以放到子執行緒去做,這樣主執行緒也可以有更多的時間去響應使用者的互動 3、預渲染(文字等非同步繪製、圖片編解碼等)

  • GPU 1、紋理渲染:假如說我們觸發了離屏渲染,例如我們設定圓角時對maskToBounds的設定,包括一些陰影、蒙層等都會觸發GPU層面的離屏渲染,對於這種情況下,GPU對於紋理渲染的工作量就會非常的大,我們可以基於此對GPU進行優化,就是儘量減少離屏渲染,我們也可以通過CPU的非同步繪製來減輕GPU的壓力

    2、檢視混合: 比如說我們檢視層級比較複雜,檢視之間層層疊加,那麼GPU就要做每一個檢視的合成,合成每一個畫素點的畫素值,如果我們可以減少檢視的層級,也是可以減輕GPU的壓力,我們也可以通過CPU的非同步繪製機制來達到一個提交的點陣圖本身就是一個層級比較少的點陣圖

UIView的繪製原理

流程圖

QQ20181206-211905@2x.png

  • 當我們呼叫[UIView setNeedsDisplay]這個方法時,其實並沒有立即進行繪製工作,系統會立刻呼叫CALayer的同名方法,並且會在當前layer上打上一個標記,然後會在當前runloop將要結束的時候呼叫[CALayer display]這個方法,然後進入我們檢視的真正繪製過程

  • 而在[CALayer display]這個方法的內部實現中會判斷這個layer的delegate是否響應displayLayer:這個方法,如果不響應這個方法,就會進入到系統繪製流程中;如果響應這個方法,那麼就會為我們提供非同步繪製的入口

上面就是UIView的繪製原理,接下來我們看一下系統繪製流程是怎樣的

老規矩,先上流程圖

QQ20181206-213639@2x.png

  • 在CALayer內部會先建立backing store,我可以理解為CGContext,我們一般在drawRect:方法中通過上下文堆疊當中取出棧頂的context,也就是上下文

  • 然後這個layer會判斷是否有代理,如果沒有代理,那麼就會呼叫[CALayer drawInCotext:];如果有代理,會呼叫代理的drawLayer:inContext:方法,然後做當前檢視的繪製工作這一步是發生在系統內部的),然後在一個合適的時機給與我們這個十分熟悉的[UIView drawRect:]方法的回撥,[UIView drawRect:]這個方法預設是什麼都不做,,系統給我們開這個口子是為了讓我們可以再做一些其他的繪製工作

  • 然後無論是哪個分支,最終都會由CALayer上傳對應的backing store(可以理解為點陣圖)給GPU,然後就結束了系統預設的繪製流程

那麼問題來了,我們如何進行非同步繪製呢

實際上我們就需要借用系統給開的這個口子,即[layer.delegate displayLayer:]

  • 在這個非同步繪製過程中就需要代理負責生成對應的bitmap(點陣圖)

  • 同時設定bitmap作為layer.contents屬性的值

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

QQ20181206-220620@2x.png

  • 假如說我們在某一個時機呼叫了[view setNeedsDisplay]這個方法,系統會在當前runloop將要結束的時候呼叫[CALyer display]方法,然後如果我們這個layer的代理實現了[view displayLayer]這個方法

  • 然後會通過子執行緒的切換,我們在子執行緒中去做一個點陣圖的繪製,主執行緒可以去做一些其他的操作

  • 在子執行緒中第一步先通過CGBitmapContextCreate()方法來建立一個點陣圖的上下文,然後我們通過CoreGraphic API可以做當前UI控制元件的一些繪製工作,最後我們再通過CGBitmapContextCreateImage()這個函式來根據當前所繪製的上下文來生成一張CGImage圖片

  • 最後回到主執行緒來提交這個點陣圖,設定layer的contents屬性,這樣就完成了一個UI控制元件的非同步繪製過程

離屏渲染 (便於理解檢視卡頓、掉幀中對GPU的開銷)

離屏渲染指的是GPU在當前螢幕緩衝區以外開闢了一個緩衝區進行渲染操作

當前螢幕渲染不需要額外建立新的快取,也不需要開啟新的上下文,相對於離屏渲染效能更好。但是受當前螢幕渲染的侷限因素限制(只有自身上下文、螢幕快取有限等),當前螢幕渲染有些情況下的渲染解決不了的,就使用到離屏渲染

離屏渲染對效能的的代價是很高的,主要體現在:

  • 建立了新的緩衝區

  • 上下文的頻繁切換

導致產生離屏渲染的原因:

  • shouldRasterize(光柵化)

  • masks(遮罩)

  • shadows(陰影)

  • edge antialiasing(抗鋸齒)

  • group opacity(不透明)

  • 複雜形狀設定圓角等

  • 漸變

相關文章