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

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

圖像顯示原理

WX20181206-150708@2x.png

  • 關於CPU和GPU都是經過總線鏈接起來的,在CPU當中輸出的每每是一個位圖,再經由總線在合適的時機傳遞個GPU框架

  • GPU拿到這個位圖以後,會對這個位圖的圖層進行渲染,包括紋理的合成等異步

  • 以後會把這個結果放到幀緩衝區中,而後視頻控制器會按照VSync信號逐行讀取幀緩衝區的數據,通過可能的數模轉換傳遞給顯示器,達到最終的顯示效果函數

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

WX20181206-153514@2x.png

  • 首先當咱們建立一個UIView控件的時候,其中負責顯示的CALayeroop

  • 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工做時長和壓力來達到一個優化效果 一、部分對象的建立、調整和銷燬能夠放到子線程去作 二、預排版( 佈局計算、文本計算),這些計算也能夠放到子線程去作,這樣主線程也能夠有更多的時間去響應用戶的交互 三、預渲染(文本等異步繪製、圖片編解碼等)

  • GPU 一、紋理渲染:假如說咱們觸發了離屏渲染,例如咱們設置圓角時對maskToBounds的設置,包括一些陰影、蒙層等都會觸發GPU層面的離屏渲染,對於這種狀況下,GPU對於紋理渲染的工做量就會很是的大,咱們能夠基於此對GPU進行優化,就是儘可能減小離屏渲染,咱們也能夠經過CPU的異步繪製來減輕GPU的壓力

    二、視圖混合: 好比說咱們視圖層級比較複雜,視圖之間層層疊加,那麼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(不透明)

  • 複雜形狀設置圓角等

  • 漸變

相關文章
相關標籤/搜索