前言
iOS 最早名為 iPhone OS,是 Apple 公司專門為其硬體裝置開發的作業系統,最初於 2007 年隨第一代 iPhone 推出,後擴充套件為支援 Apple 公司旗下的其他硬體裝置,如 iPod、iPad 等。
作為一名 iOS Developer,相信大多數人都有寫出過造成 iOS 裝置卡頓的程式碼經歷,相應的也有過想方設法優化卡頓程式碼的經驗。
本文將從 OpenGL 的角度結合 Apple 官方給出的部分資料,介紹 iOS Rendering Process 的概念及其整個底層渲染管道的各個流程。
相信在理解了 iOS Rendering Process 的底層各個階段之後,我們可以在平日的開發工作之中寫出效能更高的程式碼,在解決幀率不足的顯示卡頓問題時也可以多一些思路~
索引
- iOS Rendering Process 概念
- iOS Rendering 技術框架
- OpenGL 主要渲染步驟
- OpenGL Render Pipeline
- Core Animation Pipeline
- Commit Transaction
- Animation
- 全文總結
- 擴充套件閱讀
iOS Rendering Process 概念
iOS Rendering Process 譯為 iOS 渲染流程,本文特指 iOS 裝置從設定將要顯示的圖後設資料到最終在裝置螢幕成像的整個過程。
在開始剖析 iOS Rendering Process 之前,我們需要對 iOS 的渲染概念有一個基本的認知:
基於平鋪的渲染
iOS 裝置的螢幕分為 N * N 畫素的圖塊,每個圖塊都適合於 SoC 快取,幾何體在圖塊內被大量拆分,只有在所有幾何體全部提交之後才可以進行光柵化(Rasterization)。
Note: 這裡的光柵化指將螢幕上面被大量拆分出來的幾何體渲染為畫素點的過程。
iOS Rendering 技術框架
事實上 iOS 渲染相關的層級劃分大概如下:
UIKit
嘛~ 作為一名 iOS Developer 來說,應該對 UIKit 都不陌生,我們日常開發中使用的使用者互動元件都來自於 UIKit Framework,我們通過設定 UIKit 元件的 Layout 以及 BackgroundColor 等屬性來完成日常的介面繪畫工作。
其實 UIKit Framework 自身並不具備在螢幕成像的能力,它主要負責對使用者操作事件的響應,事件響應的傳遞大體是經過逐層的檢視樹遍歷實現的。
那麼我們日常寫的 UIKit 元件為什麼可以呈現在 iOS 裝置的螢幕上呢?
Core Animation
Core Animation 其實是一個令人誤解的命名。你可能認為它只是用來做動畫的,但實際上它是從一個叫做 Layer Kit 這麼一個不怎麼和動畫有關的名字演變而來的,所以做動畫僅僅是 Core Animation 特性的冰山一角。
Core Animation 本質上可以理解為是一個複合引擎,旨在儘可能快的組合螢幕上不同的顯示內容。這些顯示內容被分解成獨立的圖層,即 CALayer,CALayer 才是你所能在螢幕上看見的一切的基礎。
其實很多同學都應該知道 CALayer,UIKit 中需要在螢幕呈現的元件內部都有一個對應的 CALayer,也就是所謂的 Backing Layer。正是因為一一對應,所以 CALayer 也是樹形結構的,我們稱之為圖層樹。
檢視的職責就是建立並管理這個圖層,以確保當子檢視在層級關係中新增或者被移除的時候,他們關聯的圖層也同樣對應在層級關係樹當中有相同的操作。
但是為什麼 iOS 要基於 UIView 和 CALayer 提供兩個平行的層級關係呢?為什麼不用一個簡單的層級關係來處理所有事情呢?
原因在於要做職責分離,這樣也能避免很多重複程式碼。在 iOS 和 Mac OS X 兩個平臺上,事件和使用者互動有很多地方的不同,基於多點觸控的使用者介面和基於滑鼠鍵盤的互動有著本質的區別,這就是為什麼 iOS 有 UIKit 和 UIView,而 Mac OS X 有 AppKit 和 NSView 的原因。他們功能上很相似,但是在實現上有著顯著的區別。
Note: 實際上,這裡並不是兩個層級關係,而是四個,每一個都扮演不同的角色,除了檢視樹和圖層樹之外,還存在呈現樹和渲染樹。
OpenGL ES & Core Graphics
OpenGL ES
OpenGL ES 簡稱 GLES,即 OpenGL for Embedded Systems,是 OpenGL 的子集,通常面向**圖形硬體加速處理單元(GPU)**渲染 2D 和 3D 計算機圖形,例如視訊遊戲使用的計算機圖形。
OpenGL ES 專為智慧手機,平板電腦,視訊遊戲機和 PDA 等嵌入式系統而設計 。OpenGL ES 是“歷史上應用最廣泛的 3D 圖形 API”。
Core Graphics
Core Graphics Framework 基於 Quartz 高階繪圖引擎。它提供了具有無與倫比的輸出保真度的低階別輕量級 2D 渲染。您可以使用此框架來處理基於路徑的繪圖,轉換,顏色管理,離屏渲染,圖案,漸變和陰影,影像資料管理,影像建立和影像遮罩以及 PDF 文件建立,顯示和分析。
Note: 在 Mac OS X 中,Core Graphics 還包括用於處理顯示硬體,低階使用者輸入事件和視窗系統的服務。
Graphics Hardware
Graphics Hardware 譯為圖形硬體,iOS 裝置中也有自己的圖形硬體裝置,也就是我們經常提及的 GPU。
圖形處理單元(GPU)是一種專用電子電路,旨在快速操作和改變儲存器,以加速在用於輸出到顯示裝置的幀緩衝器中建立影像。GPU 被用於嵌入式系統,手機,個人電腦,工作站和遊戲控制檯。現代 GPU 在處理計算機圖形和影像方面非常高效,並且 GPU 的高度並行結構使其在大塊資料並行處理的演算法中比通用 CPU 更有效。
OpenGL 主要渲染步驟
OpenGL 全稱 Open Graphics Library,譯為開放圖形庫,是用於渲染 2D 和 3D 向量圖形的跨語言,跨平臺的應用程式程式設計介面(API)。OpenGL 可以直接訪問 GPU,以實現硬體加速渲染。
一個用來渲染影像的 OpenGL 程式主要可以大致分為以下幾個步驟:
- 設定圖後設資料
- 著色器-shader 計算圖後設資料(位置·顏色·其他)
- 光柵化-rasterization 渲染為畫素
- fragment shader,決定最終成像
- 其他操作(顯示·隱藏·融合)
Note: 其實還有一些非必要的步驟,與本文主題不相關,這裡點到為止。
我們日常開發時使用 UIKit 佈局檢視控制元件,設定透明度等等都屬於設定圖後設資料這步,這也是我們日常開發中可以影響 OpenGL 渲染的主要步驟。
OpenGL Render Pipeline
如果有同學看過 WWDC 的一些演講稿或者接觸過一些 OpenGL 知識,應該對 Render Pipeline 這個專業術語並不陌生。
不過 Render Pipeline 實在是一個初次見面不太容易理解的詞,它譯為渲染管道,也有譯為渲染管線的...
其實 Render Pipeline 指的是從應用程式資料轉換到最終渲染的影像之間的一系列資料處理過程。
好比我們上文中提到的 OpenGL 主要渲染步驟一樣,我們開發應用程式時在設定圖後設資料這步為檢視控制元件的設定佈局,背景顏色,透明度以及陰影等等資料。
下面以 OpenGL 4.5 的 Render Pipeline 為例介紹一下:
這些圖後設資料流入 OpenGL 中,傳入頂點著色器(vetex shader),然後頂點著色器對其進行著色器內部的處理後流出。之後可能進入細分著色階段(tessellation shading stage),其中又有可能分為細分控制著色器和細分賦值著色器兩部分處理,還可能會進入幾何著色階段(geometry shading stage),資料從中傳遞。最後都會走片元著色階段(fragment shading stage)。
Note: 圖後設資料是以 copy 的形式流入 shader 的,shader 一般會以特殊的類似全域性變數的形式接收資料。
OpenGL 在最終成像之前還會經歷一個階段名為計算著色階段(compute shaing stage),這個階段 OpenGL 會計算最重要在螢幕中成像的畫素位置以及顏色,如果在之前提交程式碼時用到了 CALayer 會引起 blending 的顯示效果(例如 Shadow)或者檢視顏色或內容圖片的 alpha 通道開啟,都將會加大這個階段 OpenGL 的工作量。
Core Animation Pipeline
上文說到了 iOS 裝置之所以可以成像不是因為 UIKit 而是因為 LayerKit,即 Core Animation。
Core Animation 圖層,即 CALayer 中包含一個屬性 contents,我們可以通過給這個屬性賦值來控制 CALayer 成像的內容。這個屬性的型別定義為 id,在程式編譯時不論我們給 contents 賦予任何型別的值,都是可以編譯通過的。但實踐中,如果 contents 賦值型別不是 CGImage,那麼你將會得到一個空白圖層。
Note: 造成 contents 屬性的奇怪表現的原因是 Mac OS X 的歷史包袱,它之所以被定義為 id 型別是因為在 Mac OS X 中這個屬性對 CGImage 和 NSImage 型別的值都起作用。但是在 iOS 中,如果你賦予一個 UIImage 屬性的值,僅僅會得到一個空白圖層。
說完 Core Animation 的 contents 屬性,下面介紹一下 iOS 中 Core Animation Pipeline:
- 在 Application 中佈局 UIKit 檢視控制元件間接的關聯 Core Animation 圖層
- Core Animation 圖層相關的資料提交到 iOS Render Server,即 OpenGL ES & Core Graphics
- Render Server 將與 GPU 通訊把資料經過處理之後傳遞給 GPU
- GPU 呼叫 iOS 當前裝置渲染相關的圖形裝置 Display
Note: 由於 iOS 裝置目前的螢幕最大支援 60 FPS 的重新整理率,所以每個處理間隔為 16.67 ms。
可以看到從 Commit Transaction 之後我們的圖後設資料就將會在下一次 RunLoop 時被 Application 傳送給底層的 Render Server,底層 Render Server 直接面向 GPU 經過一些列的資料處理將處理完畢的資料傳遞給 GPU,然後 GPU 負責渲染工作,根據當前 iOS 裝置的螢幕計算影像畫素位置以及畫素 alpha 通道混色計算等等最終在當前 iOS 裝置的螢幕中呈現影像。
嘛~ 由於 Core Animation Pipeline 中 Render Server 包含 OpenGL ES & Core Graphics,其中 OpenGL ES 的渲染可以參考上文 OpenGL Render Pipeline 理解。
Commit Transaction
Core Animation Pipeline 的整個管線中 iOS 常規開發一般可以影響到的範圍也就僅僅是在 Application 中佈局 UIKit 檢視控制元件間接的關聯 Core Animation 圖層這一級,即 Commit Transaction 之前的一些操作。
那麼在 Commit Transaction 之前我們一般要做的事情有哪些?
- Layout,構建檢視
- Display,繪製檢視
- Prepare,額外的 Core Animation 工作
- Commit,打包圖層並將它們傳送到 Render Server
Layout
在 Layout 階段我們能做的是把 constraint 寫的儘量高效,iOS 的 Layout Constraint 類似於 Android 的 Relative Layout。
Note: Emmmmm... 據觀察 iOS 的 Layout Constraint 在書寫時應該儘量少的依賴於檢視樹中同層級的兄弟檢視節點,它會拖慢整個檢視樹的 Layout 計算過程。
這個階段的 Layout 計算工作是在 CPU 完成的,包括 layoutSubviews
方法的過載,addSubview:
方法填充子檢視等
Display
其實這裡的 Display 僅僅是我們設定 iOS 裝置要最終成像的圖後設資料而已,過載檢視 drawRect:
方法可以自定義 UIView 的顯示,其原理是在 drawRect:
方法內部繪製 bitmap。
Note: 過載
drawRect:
方法繪製 bitmap 過程使用 CPU 和 記憶體。
所以過載 drawRect:
使用不當會造成 CPU 負載過重,App 記憶體飆升等問題。
Prepare
這個步驟屬於附加步驟,一般處理影像的解碼 & 轉換等操作。
Commit
Commit 步驟指打包圖層並將它們傳送到 Render Server。
Note: Commit 操作會遞迴執行,由於圖層和檢視一樣是以樹形結構存在的,當圖層樹過於複雜時 Commit 操作的開銷也會非常大。
CATransaction
CATransaction 是 Core Animation 中用於將多個圖層樹操作分配到渲染樹的原子更新中的機制,對圖層樹的每個修改都必須是事務的一部分。
CATransaction 類沒有屬性或者例項方法,並且也不能用 +alloc
和 -init
方法建立它,我們只能用類方法 +begin
和 +commit
分別來入棧或者出棧。
事實上任何可動畫化的圖層屬性都會被新增到棧頂的事務,你可以通過 +setAnimationDuration:
方法設定當前事務的動畫時間,或者通過 +animationDuration
方法來獲取時長值(預設 0.25 秒)。
Core Animation 在每個 RunLoop 週期中自動開始一次新的事務,即使你不顯式地使用 [CATransaction begin]
開始一次事務,在一個特定 RunLoop 迴圈中的任何屬性的變化都會被收集起來,然後做一次 0.25 秒的動畫(CALayer 隱式動畫)。
Note: CATransaction 支援巢狀。
Animation
對於 App 使用者互動體驗提升最明顯的工作莫過於使用動畫了,那麼 iOS 是如何處理動畫的渲染過程的呢?
日常開發中如果不是特別複雜的動畫我們一般會使用 UIView Animation 實現,iOS 將 UIView Animation 的處理過程分為以下三個階段:
- 呼叫
animateWithDuration:animations:
方法 - 在 Animation Block 中進行 Layout,Display,Prepare,Commit
- Render Server 根據 Animation 逐幀渲染
Note: 原理是
animateWithDuration:animations:
內部使用了 CATransaction 來將整個 Animation Block 中的程式碼作為原子操作 commit 給了 RunLoop。
基於 CATransaction 實現鏈式動畫
事實上大多數的動畫互動都是有動畫執行順序的,儘管 UIView Animation 很強大,但是在寫一些順序動畫時使用 UIView Animation 只能在 + (void)animateWithDuration:delay:options:animations:completion:
方法的 completion block 中層級巢狀,寫成一坨一坨 block 堆砌而成的程式碼,實在是難以閱讀更別提後期維護了。
在得知 UIView Animation 使用了 CATransaction 時,我們不禁會想到這個 completion block 是不是也是基於 CATransaction 實現的呢?
Bingo!CATransaction 中有 +completionBlock
以及 +setCompletionBlock:
方法可以對應於 UIView Animation 的 completion block 的書寫。
Note: 我的一個開源庫 LSAnimator - 可多鏈式動畫庫 在動畫順序連結時也用到了 CATransaction。
全文總結
結合上下文不難梳理出一個 iOS 最基本的完整渲染經過(Rendering pass)。
效能檢測思路
基於整篇文章的內容歸納一下我們在日常的開發工作中遇到效能問題時檢測問題程式碼的思路:
問題 | 建議 | 檢測工具 |
---|---|---|
目標幀率 | 60 FPS | Core Animation instrument |
CPU or GPU | 降低使用率節約能耗 | Time Profiler instrument |
不必要的 CPU 渲染 | GPU 渲染更理想,但要清楚 CPU 渲染在何時有意義 | Time Profiler instrument |
過多的 offscreen passes | 越少越好 | Core Animation instrument |
過多的 blending | 越少越好 | Core Animation instrument |
奇怪的圖片格式或大小 | 避免實時轉換或調整大小 | Core Animation instrument |
開銷昂貴的檢視或特效 | 理解當前方案的開銷成本 | Xcode View Debugger |
想象不到的層次結構 | 瞭解實際的檢視層次結構 | Xcode View Debugger |
文章寫得比較用心(是我個人的原創文章,轉載請註明 lision.me/),如果發現錯誤會優先… 個人部落格 中更新。如果有任何問題歡迎在我的微博 @Lision 聯絡我~
希望我的文章可以為你帶來價值~
擴充套件閱讀
- WWDC2014-Advanced Graphics and Animations for iOS Apps
- iOS 保持介面流暢的技巧
- iOS-Core-Animation-Advanced-Techniques
補充~ 我建了一個技術交流微信群,想在裡面認識更多的朋友!如果各位同學對文章有什麼疑問或者工作之中遇到一些小問題都可以在群裡找到我或者其他群友交流討論,期待你的加入喲~
Emmmmm..由於微信群人數過百導致不可以掃碼入群,所以請掃描上面的二維碼關注公眾號進群。