圖形渲染技術(shù)棧
下圖所示為 iOS App 的圖形渲染技術(shù)棧,App 使用 Core Graphics
、Core Animation
、Core Image
等框架來(lái)繪制可視化內(nèi)容,這些軟件框架相互之間也有著依賴關(guān)系。這些框架都需要通過(guò) OpenGL 來(lái)調(diào)用 GPU 進(jìn)行繪制,最終將內(nèi)容顯示到屏幕之上。
iOS 渲染框架
UIKit
UIKit
是 iOS 開發(fā)者最常用的框架,可以通過(guò)設(shè)置 UIKit組件的布局以及相關(guān)屬性來(lái)繪制界面。
事實(shí)上, UIKit自身并不具備在屏幕成像的能力,其主要負(fù)責(zé)對(duì)用戶操作事件的響應(yīng)(UIView繼承自UIResponder),事件響應(yīng)的傳遞大體是經(jīng)過(guò)逐層的 視圖樹 遍歷實(shí)現(xiàn)的。
Core Animation
Core Animation
源自于Layer Kit,動(dòng)畫只是Core Animation特性的冰山一角。
Core Animation是一個(gè)復(fù)合引擎,其職責(zé)是 盡可能快地組合屏幕上不同的可視內(nèi)容,這些可視內(nèi)容可被分解成獨(dú)立的圖層(即 CALayer),這些圖層會(huì)被存儲(chǔ)在一個(gè)叫做圖層樹的體系之中。從本質(zhì)上而言,CALayer
是用戶所能在屏幕上看見的一切的基礎(chǔ)。
Core Graphics
Core Graphics
基于 Quartz 高級(jí)繪圖引擎,主要用于運(yùn)行時(shí)繪制圖像。開發(fā)者可以使用此框架來(lái)處理基于路徑的繪圖,轉(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ì)算、繪制一系列圖像幀來(lái)實(shí)現(xiàn)動(dòng)畫。
Core Image
Core Image
與 Core Graphics
恰恰相反,Core Graphics用于在 運(yùn)行時(shí)創(chuàng)建圖像,而Core Image是用來(lái)處理 運(yùn)行前創(chuàng)建的圖像 的。Core Image框架擁有一系列現(xiàn)成的圖像過(guò)濾器,能對(duì)已存在的圖像進(jìn)行高效的處理。
大部分情況下,Core Image會(huì)在 GPU 中完成工作,但如果 GPU 忙,會(huì)使用 CPU 進(jìn)行處理。
OpenGL ES
OpenGL ES
(OpenGL for Embedded Systems,簡(jiǎn)稱 GLES),是 OpenGL 的子集。在前面的 圖形渲染原理綜述 一文中提到過(guò) 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ā)者都沒有直接使用過(guò)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)可以猜測(cè),Apple 已經(jīng)實(shí)現(xiàn)了一套機(jī)制將 OpenGL 命令無(wú)縫橋接到Metal上,由Metal擔(dān)任真正于硬件交互的工作。
UIView 與 CALayer 的關(guān)系
在前面的Core Animation簡(jiǎn)介中提到CALayer事實(shí)上是用戶所能在屏幕上看見的一切的基礎(chǔ)。為什么 UIKit
中的視圖能夠呈現(xiàn)可視化內(nèi)容?就是因?yàn)閁IKit中的每一個(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)。
其中,視圖的職責(zé)是 創(chuàng)建并管理 圖層,以確保當(dāng)子視圖在層級(jí)關(guān)系中 添加或被移除 時(shí),其關(guān)聯(lián)的圖層在圖層樹中也有相同的操作,即保證視圖樹和圖層樹在結(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)镃ALayer基本等同于一個(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 是指通過(guò) CALayer
的 contents
屬性來(lái)配置圖片。然而,contents屬性的類型為 id
。在這種情況下,可以給 contents屬性賦予任何值,app 仍可以編譯通過(guò)。但是在實(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ā)中,一般通過(guò)繼承 UIView
并實(shí)現(xiàn) -drawRect:
方法來(lái)自定義繪制。
雖然 -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è)寄宿圖來(lái)顯示。 -
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 流水線
通過(guò)前面的介紹,我們知道了 CALayer
的本質(zhì),那么它是如何調(diào)用 GPU 并顯示可視化內(nèi)容的呢?下面我們就需要介紹一下 Core Animation 流水線的工作原理。
事實(shí)上,app 本身并不負(fù)責(zé)渲染,渲染則是由一個(gè)獨(dú)立的進(jìn)程負(fù)責(zé),即 Render Server
進(jìn)程。
App 通過(guò) IPC 將渲染任務(wù)及相關(guān)數(shù)據(jù)提交給 Render Server
。Render Server
處理完數(shù)據(jù)后,再傳遞至 GPU。最后由 GPU 調(diào)用 iOS 的圖像設(shè)備進(jìn)行顯示。
Core Animation 流水線的詳細(xì)過(guò)程如下:
- 首先,由 app 處理事件(Handle Events),如:用戶的點(diǎn)擊操作,在此過(guò)程中 app 可能需要更新 視圖樹,相應(yīng)地,圖層樹 也會(huì)被更新。
- 其次,app 通過(guò) 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 通過(guò) Frame Buffer、視頻控制器等相關(guān)部件,將圖像顯示在屏幕上。
對(duì)上述步驟進(jìn)行串聯(lián),它們執(zhí)行所消耗的時(shí)間遠(yuǎn)遠(yuǎn)超過(guò) 16.67 ms,因此為了滿足對(duì)屏幕的 60 FPS 刷新率的支持,需要將這些步驟進(jìn)行分解,通過(guò)流水線的方式進(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)部繪制寄宿圖,該過(guò)程使用 CPU 和內(nèi)存。
Prepare
Prepare
階段屬于附加步驟,一般處理圖像的解碼和轉(zhuǎn)換等操作。
Commit
Commit
階段主要將圖層進(jìn)行打包,并將它們發(fā)送至 Render Server
。該過(guò)程會(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 將其處理過(guò)程分為如下三部階段:
- Step 1:調(diào)用
animationWithDuration:animations:
方法 - Step 2:在 Animation Block 中進(jìn)行
Layout
,Display
,Prepare
,Commit
等步驟。 - Step 3:
Render Server
根據(jù) Animation 逐幀進(jìn)行渲染。