一、圖形渲染技術(shù)棧
下圖所示為 iOS App 的圖形渲染技術(shù)棧,App 使用 Core Graphics、Core Animation、Core Image 等框架來繪制可視化內(nèi)容,這些軟件框架相互之間也有著依賴關(guān)系。這些框架都需要通過 OpenGL 來調(diào)用 GPU 進(jìn)行繪制,最終將內(nèi)容顯示到屏幕之上。
iOS 渲染框架
UIKit
UIKit 是 iOS 開發(fā)者最常用的框架,可以通過設(shè)置 UIKit 組件的布局以及相關(guān)屬性來繪制界面。
事實(shí)上, UIKit 自身并不具備在屏幕成像的能力,其主要負(fù)責(zé)對(duì)用戶操作事件的響應(yīng)(UIView 繼承自 UIResponder),事件響應(yīng)的傳遞大體是經(jīng)過逐層的 視圖樹 遍歷實(shí)現(xiàn)的。
Core Graphics
Core Graphics 基于 Quartz 高級(jí)繪圖引擎,主要用于運(yùn)行時(shí)繪制圖像。開發(fā)者可以使用此框架來處理基于路徑的繪圖,轉(zhuǎn)換,顏色管理,離屏渲染,圖案,漸變和陰影,圖像數(shù)據(jù)管理,圖像創(chuàng)建和圖像遮罩以及 PDF 文檔創(chuàng)建,顯示和分析。
當(dāng)開發(fā)者需要在 運(yùn)行時(shí)創(chuàng)建圖像 時(shí),可以使用 Core Graphics 去繪制。與之相對(duì)的是 運(yùn)行前創(chuàng)建圖像,例如用 Photoshop 提前做好圖片素材直接導(dǎo)入應(yīng)用。相比之下,我們更需要 Core Graphics 去在運(yùn)行時(shí)實(shí)時(shí)計(jì)算、繪制一系列圖像幀來實(shí)現(xiàn)動(dòng)畫。
Core Image
Core Image 與 Core Graphics 恰恰相反,Core Graphics 用于在 運(yùn)行時(shí)創(chuàng)建圖像,而 Core Image 是用來處理 運(yùn)行前創(chuàng)建的圖像 的。Core Image 框架擁有一系列現(xiàn)成的圖像過濾器,能對(duì)已存在的圖像進(jìn)行高效的處理。
大部分情況下,Core Image 會(huì)在 GPU 中完成工作,但如果 GPU 忙,會(huì)使用 CPU 進(jìn)行處理。
OpenGL ES
OpenGL ES(OpenGL for Embedded Systems,簡稱 GLES),是 OpenGL 的子集。在前面的 圖形渲染原理綜述 一文中提到過 OpenGL 是一套第三方標(biāo)準(zhǔn),函數(shù)的內(nèi)部實(shí)現(xiàn)由對(duì)應(yīng)的 GPU 廠商開發(fā)實(shí)現(xiàn)。
Metal
Metal 類似于 OpenGL ES,也是一套第三方標(biāo)準(zhǔn),具體實(shí)現(xiàn)由蘋果實(shí)現(xiàn)。大多數(shù)開發(fā)者都沒有直接使用過 Metal,但其實(shí)所有開發(fā)者都在間接地使用 Metal。Core Animation、Core Image、SceneKit、SpriteKit 等等渲染框架都是構(gòu)建于 Metal 之上的。
當(dāng)在真機(jī)上調(diào)試 OpenGL 程序時(shí),控制臺(tái)會(huì)打印出啟用 Metal 的日志。根據(jù)這一點(diǎn)可以猜測,Apple 已經(jīng)實(shí)現(xiàn)了一套機(jī)制將 OpenGL 命令無縫橋接到 Metal 上,由 Metal 擔(dān)任真正于硬件交互的工作。
UIView 與 CALayer 的關(guān)系
在前面的 Core Animation 簡介中提到 CALayer 事實(shí)上是用戶所能在屏幕上看見的一切的基礎(chǔ)。為什么 UIKit 中的視圖能夠呈現(xiàn)可視化內(nèi)容?就是因?yàn)?UIKit 中的每一個(gè) UI 視圖控件其實(shí)內(nèi)部都有一個(gè)關(guān)聯(lián)的 CALayer,即 backing layer。
由于這種一一對(duì)應(yīng)的關(guān)系,視圖層級(jí)擁有 視圖樹 的樹形結(jié)構(gòu),對(duì)應(yīng) CALayer 層級(jí)也擁有 圖層樹 的樹形結(jié)構(gòu)。
那么為什么 iOS 要基于 UIView 和 CALayer 提供兩個(gè)平行的層級(jí)關(guān)系呢?
其原因在于要做 職責(zé)分離,這樣也能避免很多重復(fù)代碼。在 iOS 和 Mac OS X 兩個(gè)平臺(tái)上,事件和用戶交互有很多地方的不同,基于多點(diǎn)觸控的用戶界面和基于鼠標(biāo)鍵盤的交互有著本質(zhì)的區(qū)別,這就是為什么 iOS 有 UIKit 和 UIView,對(duì)應(yīng) Mac OS X 有 AppKit 和 NSView 的原因。它們?cè)诠δ苌虾芟嗨?,但是在?shí)現(xiàn)上有著顯著的區(qū)別。
實(shí)際上,這里并不是兩個(gè)層級(jí)關(guān)系,而是四個(gè)。每一個(gè)都扮演著不同的角色。除了 視圖樹 和 圖層樹,還有 呈現(xiàn)樹 和 渲染樹。
CALayer
那么為什么 CALayer
可以呈現(xiàn)可視化內(nèi)容呢?因?yàn)?CALayer
基本等同于一個(gè) 紋理。紋理是 GPU 進(jìn)行圖像渲染的重要依據(jù)。
在 圖形渲染原理 中提到紋理本質(zhì)上就是一張圖片,因此 CALayer
也包含一個(gè) contents
屬性指向一塊緩存區(qū),稱為 backing store
,可以存放位圖(Bitmap)。iOS 中將該緩存區(qū)保存的圖片稱為 寄宿圖。
圖形渲染流水線支持從頂點(diǎn)開始進(jìn)行繪制(在流水線中,頂點(diǎn)會(huì)被處理生成紋理),也支持直接使用紋理(圖片)進(jìn)行渲染。相應(yīng)地,在實(shí)際開發(fā)中,繪制界面也有兩種方式:一種是 手動(dòng)繪制;另一種是 使用圖片。
對(duì)此,iOS 中也有兩種相應(yīng)的實(shí)現(xiàn)方式:
使用圖片:contents image
手動(dòng)繪制:custom drawing
Contents Image
Contents Image 是指通過 CALayer 的 contents 屬性來配置圖片。然而,contents 屬性的類型為 id。在這種情況下,可以給 contents 屬性賦予任何值,app 仍可以編譯通過。但是在實(shí)踐中,如果 content 的值不是 CGImage ,得到的圖層將是空白的。
既然如此,為什么要將 contents 的屬性類型定義為 id 而非 CGImage。這是因?yàn)樵?Mac OS 系統(tǒng)中,該屬性對(duì) CGImage 和 NSImage 類型的值都起作用,而在 iOS 系統(tǒng)中,該屬性只對(duì) CGImage 起作用。
本質(zhì)上,contents 屬性指向的一塊緩存區(qū)域,稱為 backing store,可以存放 bitmap 數(shù)據(jù)。
Custom Drawing
Custom Drawing 是指使用 Core Graphics 直接繪制寄宿圖。實(shí)際開發(fā)中,一般通過繼承 UIView 并實(shí)現(xiàn) -drawRect: 方法來自定義繪制。
雖然 -drawRect: 是一個(gè) UIView 方法,但事實(shí)上都是底層的 CALayer 完成了重繪工作并保存了產(chǎn)生的圖片。下圖所示為 -drawRect: 繪制定義寄宿圖的基本原理。
UIView 有一個(gè)關(guān)聯(lián)圖層,即 CALayer。
CALayer 有一個(gè)可選的 delegate 屬性,實(shí)現(xiàn)了 CALayerDelegate 協(xié)議。UIView 作為 CALayer 的代理實(shí)現(xiàn)了 CALayerDelegae 協(xié)議。
當(dāng)需要重繪時(shí),即調(diào)用 -drawRect:,CALayer 請(qǐng)求其代理給予一個(gè)寄宿圖來顯示。
CALayer 首先會(huì)嘗試調(diào)用 -displayLayer: 方法,此時(shí)代理可以直接設(shè)置 contents 屬性。
- (void)displayLayer:(CALayer *)layer;
如果代理沒有實(shí)現(xiàn) -displayLayer: 方法,CALayer 則會(huì)嘗試調(diào)用 -drawLayer:inContext: 方法。在調(diào)用該方法前,CALayer 會(huì)創(chuàng)建一個(gè)空的寄宿圖(尺寸由 bounds 和 contentScale 決定)和一個(gè) Core Graphics 的繪制上下文,為繪制寄宿圖做準(zhǔn)備,作為 ctx 參數(shù)傳入。
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
最后,由 Core Graphics 繪制生成的寄宿圖會(huì)存入 backing store。
二、Core Animation 流水線
通過前面的介紹,我們知道了 CALayer 的本質(zhì),那么它是如何調(diào)用 GPU 并顯示可視化內(nèi)容的呢?下面我們就需要介紹一下 Core Animation 流水線的工作原理。
事實(shí)上,app 本身并不負(fù)責(zé)渲染,渲染則是由一個(gè)獨(dú)立的進(jìn)程負(fù)責(zé),即 Render Server 進(jìn)程。
App 通過 IPC 將渲染任務(wù)及相關(guān)數(shù)據(jù)提交給 Render Server。Render Server 處理完數(shù)據(jù)后,再傳遞至 GPU。最后由 GPU 調(diào)用 iOS 的圖像設(shè)備進(jìn)行顯示。
Core Animation 流水線的詳細(xì)過程如下:
首先,由 app 處理事件(Handle Events),如:用戶的點(diǎn)擊操作,在此過程中 app 可能需要更新 視圖樹,相應(yīng)地,圖層樹 也會(huì)被更新。
其次,app 通過 CPU 完成對(duì)顯示內(nèi)容的計(jì)算,如:視圖的創(chuàng)建、布局計(jì)算、圖片解碼、文本繪制等。在完成對(duì)顯示內(nèi)容的計(jì)算之后,app 對(duì)圖層進(jìn)行打包,并在下一次 RunLoop 時(shí)將其發(fā)送至 Render Server,即完成了一次 Commit Transaction 操作。
Render Server 主要執(zhí)行 Open GL、Core Graphics 相關(guān)程序,并調(diào)用 GPU
GPU 則在物理層上完成了對(duì)圖像的渲染。
最終,GPU 通過 Frame Buffer、視頻控制器等相關(guān)部件,將圖像顯示在屏幕上。
對(duì)上述步驟進(jìn)行串聯(lián),它們執(zhí)行所消耗的時(shí)間遠(yuǎn)遠(yuǎn)超過 16.67 ms,因此為了滿足對(duì)屏幕的 60 FPS 刷新率的支持,需要將這些步驟進(jìn)行分解,通過流水線的方式進(jìn)行并行執(zhí)行,如下圖所示。
Commit Transaction
在 Core Animation 流水線中,app 調(diào)用 Render Server 前的最后一步 Commit Transaction 其實(shí)可以細(xì)分為 4 個(gè)步驟:
Layout
Display
Prepare
Commit
Layout
Layout
階段主要進(jìn)行視圖構(gòu)建,包括:LayoutSubviews
方法的重載,addSubview:
方法填充子視圖等。
Display
Display
階段主要進(jìn)行視圖繪制,這里僅僅是設(shè)置最要成像的圖元數(shù)據(jù)。重載視圖的 drawRect:
方法可以自定義 UIView
的顯示,其原理是在 drawRect:
方法內(nèi)部繪制寄宿圖,該過程使用 CPU 和內(nèi)存。
Prepare
Prepare
階段屬于附加步驟,一般處理圖像的解碼和轉(zhuǎn)換等操作。
Commit
Commit
階段主要將圖層進(jìn)行打包,并將它們發(fā)送至 Render Server
。該過程會(huì)遞歸執(zhí)行,因?yàn)閳D層和視圖都是以樹形結(jié)構(gòu)存在。
三、動(dòng)畫渲染原理
iOS 動(dòng)畫的渲染也是基于上述 Core Animation 流水線完成的。這里我們重點(diǎn)關(guān)注 app 與 Render Server 的執(zhí)行流程。
日常開發(fā)中,如果不是特別復(fù)雜的動(dòng)畫,一般使用 UIView Animation 實(shí)現(xiàn),iOS 將其處理過程分為如下三部階段:
Step 1:調(diào)用 animationWithDuration:animations: 方法
Step 2:在 Animation Block 中進(jìn)行 Layout,Display,Prepare,Commit 等步驟。
Step 3:Render Server 根據(jù) Animation 逐幀進(jìn)行渲染。