對于iOS的性能優化,最能體現在用戶端的就是界面的流暢,如何保持界面的流暢是我們作為開發要追求的,本章節先來介紹一下界面展示的相關原理。
1.硬件顯示原理
屏幕基礎渲染原理
首先從過去的 CRT
顯示器原理說起。CRT 的電子槍按照上面方式,從上到下一行行掃描,掃描完成后顯示器就呈現一幀畫面,隨后電子槍回到初始位置繼續下一次掃描。為了把顯示器的顯示過程和系統的視頻控制器進行同步,顯示器(或者其他硬件)會用硬件時鐘產生一系列的定時信號。當電子槍換到新的一行,準備進行掃描時,顯示器會發出一個水平同步信號(horizonal synchronization
),簡稱 HSync
;而當一幀畫面繪制完成后,電子槍回復到原位,準備畫下一幀前,顯示器會發出一個垂直同步信號(vertical synchronization
),簡稱 VSync
。顯示器通常以固定頻率進行刷新,這個刷新率就是VSync
信號產生的頻率。盡管現在的設備大都是液晶顯示屏了,但原理仍然沒有變。
- 垂直同步信號(
VSync
):
屏幕發出VSync之后,就表示將要進行新一幀畫面的顯示,于是開始從幀緩存里面讀取經過GPU
渲染好的用于顯示的數據 - 水平同步信號(
HSync
):
顯示器從幀緩存里拿到數據之后,是從上到下一行一行的刷新的,刷新完一行,就發出一個HSync,直到最下面一層顯示出來,這樣,一幀的畫面就完成了顯示。
iOS中的渲染過程
在iOS的界面渲染中,也是需要遵循上述的屏幕渲染原理的,這是一系列復雜過程,主要使用了CPU
,GPU
和對應的雙緩存機制
- CPU(Central Processing Unit):
- 中央處理器,在iOS程序中,負責對象的創建和銷毀、對象屬性的調整、布局的計算、文本的計算和排版規格、圖片的格式轉碼和解碼、圖像的繪制(Core Graphic)
- GPU(Graphics Processing Unit):
- 圖形處理器,負責紋理的渲染。如果沒有接觸過OpenGL的朋友,可能不太好理解紋理渲染這個概念,我們知道,屏幕上面的物理元件是像素,我們在屏幕上面看到的圖片,文字,視頻,就是由屏幕上的所有像素,通過控制色值變化而呈現出來的。那么像素的色值數據,就是由GPU計算得出的,然后將這些數據提交給視頻控制器,由它負責顯示到屏幕上。
- 比CPU使用更少的電來完成工作并且GPU的浮點計算能力要超出CPU很多。
- GPU的渲染性能要比CPU高效很多,同時對系統的負載和消耗也更低一些,所以在開發中,我們應該盡量讓CPU負責主線程的UI調動,把圖形顯示相關的工作交給GPU來處理,當涉及到光柵化等一些工作時,CPU也會參與進來
- 雙緩沖機制:
- iOS中采用的是雙緩沖機制,分為前幀緩存和后幀緩存。
- GPU會預先渲染好一幀放入一個緩沖區內(前幀緩存),讓視頻控制器讀取,當下一幀渲染好后,GPU會直接把視頻控制器的指針指向第二個緩沖器(后幀緩存)
- 當你視頻控制器已經讀完一幀,準備讀下一幀的時候,GPU會等待顯示器的VSync信號發出后,前幀緩存和后幀緩存會瞬間切換,后幀緩存會變成新的前幀緩存,同時舊的前幀緩存會變成新的后幀緩存。
2.卡頓原因
我們手機屏幕的刷幀率是60FPS
(Frame per Second 幀/秒),也就是會所1秒鐘的時間,屏幕可以刷新60幀(次)。完成一幀刷新的用時是16.6毫秒。因此垂直同步信號VSync就是每16.6
毫秒發出一次。
通過對上面界面渲染原理的探究,可以總結出造成卡頓的主要原因就是:
- 在一個 VSync 時間段內,CPU 或者 GPU 沒有完成內容提交,阻礙了顯示流程,造成掉了幀現象
-
Vsync 與雙緩沖的意義:強制同步屏幕刷新,以掉幀為代價解決屏幕撕裂問題。
卡頓原因展示
3.iOS 中的渲染框架
通過上面基礎原理的探究,對屏幕的渲染有了一定的了解。那么在iOS中的整體渲染流程,基本如上圖所示。在硬件基礎之上,iOS 中有
Core Graphics
、Core Animation
、Core Image
、OpenGL
、Metal
等多種軟件框架來繪制內容,在 CPU 與 GPU 之間進行了更高層地封裝。
-
GPU Driver
:上述軟件框架相互之間也有著依賴關系,不過所有框架最終都會通過 OpenGL 連接到 GPU Driver,GPU Driver 是直接和 GPU 交流的代碼塊,直接與 GPU 連接。 -
OpenGL
:是一個提供了 2D 和 3D 圖形渲染的 API,它能和 GPU 密切的配合,最高效地利用 GPU 的能力,實現硬件加速渲染。OpenGL的高效實現(利用了圖形加速硬件)一般由顯示設備廠商提供,而且非常依賴于該廠商提供的硬件。OpenGL 之上擴展出很多東西,如 Core Graphics 等最終都依賴于 OpenGL,有些情況下為了更高的效率,比如游戲程序,甚至會直接調用 OpenGL 的接口。 -
Metal
:Metal 類似于 OpenGL ES,也是一套第三方標準,具體實現由蘋果實現。Core Animation、Core Image、SceneKit、SpriteKit 等等渲染框架都是構建于 Metal 之上的。 -
Core Graphics
:Core Graphics 是一個強大的二維圖像繪制引擎,是 iOS 的核心圖形庫,常用的比如 CGRect 就定義在這個框架下。 -
Core Image
:Core Image 是一個高性能的圖像處理分析的框架,它擁有一系列現成的圖像濾鏡,能對已存在的圖像進行高效的處理。 -
Core Animation
:在 iOS 上,幾乎所有的東西都是通過 Core Animation 繪制出來,它的自由度更高,使用范圍也更廣。
4.CoreAnimation渲染原理
CoreAnimation初探
Core Animation
,它本質上可以理解為一個復合引擎,主要職責包含:渲染、構建和實現動畫
Core Animation
是 AppKit 和 UIKit 完美的底層支持,同時也被整合進入 Cocoa 和 Cocoa Touch 的工作流之中,它是 app 界面渲染和構建的最基礎架構。 Core Animation 的職責就是盡可能快地組合屏幕上不同的可視內容,這個內容是被分解成獨立的 layer(iOS 中具體而言就是 CALayer
),并且被存儲為樹狀層級結構。這個樹也形成了 UIKit 以及在 iOS 應用程序當中你所能在屏幕上看見的一切的基礎。
UIView和CALayer關系
在CoreAnimation的渲染中,與開發者關系最大的就是UIView
和CALayer
了,為了能更好的理解CoreAnimation的渲染流程,我們必須明確這兩者之間的關系。
UIView
UIView - Apple
Views are the fundamental building blocks of your app's user interface, and theUIView
class defines the behaviors that are common to all views. A view object renders content within its bounds rectangle and handles any interactions with that content.
根據 Apple 的官方文檔,UIView 是 app 中的基本組成結構,定義了一些統一的規范。它會負責內容的渲染以及,處理交互事件。具體而言,它負責的事情可以歸為下面三類
-
Drawing and animation
:繪制與動畫 -
Layout and subview management
:布局與子 view 的管理 -
Event handling
:點擊事件處理
CALayer
CALayer - Apple
Layers are often used to provide the backing store for views but can also be used without a view to display content. A layer’s main job is to manage the visual content that you provide...
If the layer object was created by a view, the view typically assigns itself as the layer’s delegate automatically, and you should not change that relationship.
CALayer 的官方文檔中我們可以看出,CALayer 的主要職責是管理內部的可視內容。當我們創建一個 UIView 的時候,UIView 會自動創建一個 CALayer,為自身提供存儲 bitmap
的地方,并將自身固定設置為 CALayer 的代理
。
那么CALayer
是如何展示bitmap
視圖的呢?
/** Layer content properties and methods. **/
/* An object providing the contents of the layer, typically a CGImageRef,
* but may be something else. (For example, NSImage objects are
* supported on Mac OS X 10.6 and later.) Default value is nil.
* Animatable. */
@property(nullable, strong) id contents;
在CALayer的源碼中,我們發現了contents 提供了 layer 的內容,是一個指針類型,在 iOS 中的類型就是 CGImageRef
(在 OS X 中還可以是 NSImage)。而我們進一步查到,Apple 對 CGImageRef
的定義是:
A bitmap image or image mask.
看到 bitmap,這下我們就可以和之前講的的渲染流水線聯系起來了:實際上,CALayer 中的 contents 屬性保存了由設備渲染流水線渲染好的位圖 bitmap(通常也被稱為 backing store
),而當設備屏幕進行刷新時,會從 CALayer 中讀取生成好的 bitmap,進而呈現到屏幕上。
正因為每次要被渲染的內容是被靜態的存儲起來的,所以每次渲染時,Core Animation 會觸發調用 drawRect: 方法,使用存儲好的 bitmap 進行新一輪的展示。
// 注意 CGImage 和 CGImageRef 的關系:
// typedef struct CGImage CGImageRef;
layer.contents = (__bridge id)image.CGImage;
兩者關系
- 每個 UIView 內部都有一個 CALayer 在背后提供內容的繪制和顯示,并且 UIView 的尺寸樣式都由內部的 Layer 所提供。兩者都有樹狀層級結構,layer 內部有 SubLayers,View 內部有 SubViews.但是 Layer 比 View 多了個AnchorPoint
- 在 View顯示的時候,UIView 做為 Layer 的 CALayerDelegate,View 的顯示內容由內部的 CALayer 的 display
- CALayer 是默認修改屬性支持隱式動畫的,在給 UIView 的 Layer 做動畫的時候,View 作為 Layer 的代理,Layer 通過 actionForLayer:forKey:向 View請求相應的 action(動畫行為)
- layer 內部維護著三分 layer tree,分別是 presentLayer Tree(動畫樹),modeLayer Tree(模型樹), Render Tree (渲染樹),在做 iOS動畫的時候,我們修改動畫的屬性,在動畫的其實是 Layer 的 presentLayer的屬性值,而最終展示在界面上的其實是提供 View的modelLayer
兩者主要的關系如下:
- CALayer 是 UIView 的屬性之一,負責渲染和動畫,提供可視內容的呈現。
- UIView 提供了對 CALayer 部分功能的封裝,同時也另外負責了交互事件的處理。
兩者主要的異同點如下:
相同的層級結構:我們對 UIView 的層級結構非常熟悉,由于每個 UIView 都對應 CALayer 負責頁面的繪制,所以 CALayer 也具有相應的層級結構。
部分效果的設置:因為 UIView 只對 CALayer 的部分功能進行了封裝,而另一部分如圓角、陰影、邊框等特效都需要通過調用 layer 屬性來設置。
是否響應點擊事件:CALayer 不負責點擊事件,所以不響應點擊事件,而 UIView 會響應。
不同繼承關系:CALayer 繼承自
NSObject
,UIView 由于要負責交互事件,所以繼承自UIResponder
。
當然還剩最后一個問題,為什么要將 CALayer 獨立出來,直接使用 UIView 統一管理不行嗎?為什么不用一個統一的對象來處理所有事情呢?
這樣設計的主要原因就是為了職責分離,拆分功能,方便代碼的復用
。
通過 Core Animation 框架來負責可視內容的呈現,這樣在 iOS 和 OS X 上都可以使用 Core Animation 進行渲染。與此同時,兩個系統還可以根據交互規則的不同來進一步封裝統一的控件,比如 iOS 有 UIKit 和 UIView,OS X 則是AppKit 和 NSView。
CoreAnimation渲染流程
關于CoreAnimation的渲染流程,通過上圖可以比較清晰的看出,主要可以總結為以下步驟:
-
Handle Events
:這個過程中會先處理點擊事件,這個過程中有可能會需要改變頁面的布局和界面層次。 -
Commit Transaction
:此時 app 會通過 CPU 處理顯示內容的前置計算,比如布局計算、圖片解碼等任務。之后將計算好的圖層進行打包發給 Render Server。 -
Decode
:打包好的圖層被傳輸到 Render Server 之后,首先會進行解碼。注意完成解碼之后需要等待下一個RunLoop
才會執行下一步 Draw Calls。 -
Draw Calls
:解碼完成后,Core Animation 會調用下層渲染框架(比如 OpenGL 或者 Metal)的方法進行繪制,進而調用到 GPU。 -
Render
:這一階段主要由 GPU 進行渲染。 -
Display
:顯示階段,需要等 render 結束的下一個 RunLoop 觸發顯示。
Commit Transaction 渲染原理
在日常的開發中,作為開發者能影響到的就是 Handle Events 和 Commit Transaction 這兩個階段,這也是開發者接觸最多的部分。Handle Events 就是處理觸摸事件,而 Commit Transaction 這部分中主要進行的是:Layout
、Display
、Prepare
、Commit
等四個具體的操作。
Layout
這個階段主要是構建視圖,遍歷的操作[UIView layerSubview]
,[CALayer layoutSubLayers]
- 調用重載的
layoutSubviews
方法 - 創建視圖,并通過
addSubview
方法添加子視圖 - 計算視圖布局,即所有的
Layout Constraint
由于這個階段是在 CPU
中進行,通常是 CPU 限制或者 IO 限制,所以我們應該盡量高效輕量地操作,減少這部分的時間,比如減少非必要的視圖創建、簡化布局計算、減少視圖層級等。代碼的主要調用結構如下:
Display
這個階段主要是交給 Core Graphics 進行視圖的繪制,注意不是真正的顯示,而是得到前文所說的contents數據。
根據UIView和CALayer的關系,我們知道,主要是CALayer來負責一個view的展示,并最終將得到的bitmap
賦值給contents
屬性,保存在backing store
中供后續使用。
我們知道view的繪制會在drawRect:
方法中,我們在其中打斷點,可得到一下堆棧信息
通過調用堆棧,可以得到此過程主要如下:
- 根據Layout獲得的數據,進行展示
- 通過CALayer和UIView之間的代理來進行展示,主要實現在的
display
方法中
- CALayer中實現
[self drawInContext:context]
: 傳入上下文進行繪制 - UIView中實現
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
:對界面進行繪制并傳入上下文 - UIView中實現
- (void)displayLayer:(CALayer *)layer
:通過UIGraphicsGetImageFromCurrentImageContext
獲取到當前上下文的圖片,并賦值給layer.contents = (__bridge id)(image.CGImage);
- 最后使用
UIGraphicsEndImageContext
關閉上下文
- 以上是默認的流程,如果自己重寫實現了
drawRect:
,這個方法會直接調用Core Graphics
繪制方法得到bitmap
數據,同時系統會額外申請一塊內存
,用于暫存繪制好的bitmap
。這樣繪制過程從GPU
轉移到了CPU
,這就導致了一定的效率損失。與此同時,這個過程會額外使用 CPU 和內存,因此需要高效繪制,否則容易造成 CPU 卡頓或者內存爆炸。
Prepare
Core Animation 額外的工作,主要圖片解碼和轉換
Commit
打包圖層并將它們發送到 Render Server
。
注意 commit 操作是依賴圖層樹遞歸執行的,所以如果圖層樹過于復雜,commit 的開銷就會很大。這也是我們希望減少視圖層級,從而降低圖層樹復雜度的原因。
Render Server相關
Render Server 通常是 OpenGL
或者是 Metal
。以 OpenGL 為例,那么上圖主要是 GPU 中執行的操作,具體主要包括:
- GPU 收到
Command Buffer
,包含圖元 primitives 信息 -
Tiler
開始工作:先通過頂點著色器Vertex Shader
對頂點進行處理,更新圖元信息 - 平鋪過程:平鋪生成
tile bucket
的幾何圖形,這一步會將圖元信息轉化為像素,之后將結果寫入Parameter Buffer
中 -
Tiler
更新完所有的圖元信息,或者 Parameter Buffer 已滿,則會開始下一步 -
Renderer
工作:將像素信息進行處理得到bitmap
,之后存入Render Buffer
-
Render Buffer
中存儲有渲染好的bitmap
,供之后的Display
操作使用
使用 Instrument 的 OpenGL ES,可以對過程進行監控。OpenGL ES tiler utilization 和 OpenGL ES renderer utilization 可以分別監控 Tiler 和 Renderer 的工作情況
參考
iOS性能優化
iOS保持界面流暢
iOS Rendering 渲染全解析
深入理解 iOS Rendering Process