常用框架
iOS系統下通常使用UIKit、Core Animation、Core Graphics、Core Image、Core Video等框架來完成圖形圖像的渲染和顯示:
UIKit:UIKit是iOS系統中用于創建用戶界面的框架。它提供了許多用于顯示文本、圖像、按鈕等控件的類,同時也提供了一些動畫效果的支持。
Core Animation:CoreAnimation是一個動畫框架,可以用于創建基于圖層的動畫。它提供了一些基本的動畫類型,比如位移、旋轉、縮放等,還支持復雜的動畫組合和定時器功能。
Core Graphics:CoreGraphics是一個2D圖形框架,可以用于創建和操作圖像和路徑。它提供了許多繪圖函數,可以用于繪制直線、矩形、橢圓、圓弧等形狀,還支持顏色、漸變、圖案等特效。
Core Image:CoreImage是一個圖像處理框架,可以用于對圖像進行濾鏡、色彩調整、變形等操作。它提供了多種濾鏡效果,可以輕松實現圖片的美化、修復等功能。
Core Video:CoreVideo是一個視頻處理框架,可以用于對視頻進行解碼、編碼、渲染等操作。它提供了多種視頻效果,可以用于實現視頻編輯、特效制作等功能。
這些框架通常被iOS開發者用來實現圖形圖像的渲染和顯示,同時也可以用于創建各種精美的用戶界面和動畫效果。
這些框架仍然遵循圖形圖像渲染pipeline的基本架構,具體的技術棧如圖所示:
Core Animation (核心動畫)
Core Animation
再說說Core Animation,如果你正在編寫iOS App,那么無論你是否知道,你都在使用它。Core Animation字面意思是“核心動畫”,但它本質上可以理解為一個復合引擎,主要職責包含:渲染、構建和實現動畫。
Core Animation最核心的功能是提供給上層框架“UIKit”和“AppKit”使用的Layer對象(iOS系統對應的是CALayer),用來管理和控制屏幕上需要顯示的內容(Content),這些內容被分解成獨立的Layer,存儲在一個叫做Layer Tree(圖層樹)的層級關系體系之中。于是這個Tree就形成了UIKit以及在iOS應用程序當中你所能在屏幕上看見的一切的基礎。
簡單來說就是用戶能看到的屏幕上的內容都由Layer對象進行管理。Layer對象中的Contents屬性保存了由設備渲染流水線渲染好的位圖Bitmap(通常也被稱為 backing store),而當設備屏幕進行刷新時,會從Layer對象中讀取生成好的Bitmap,進而顯示到屏幕上。
UIView 和 Layer
在iOS開發過程中,大量使用的視圖控件實際上是UIView而不是CALayer,UIView能夠呈現可視化內容的原因是UIKit中的每一個UI視圖內部都有一個關聯的CALayer,用來呈現這些內容。
從上面的層次結構圖上來看,UIView處于比Core Animation更高的層級,UIView除了利用Core Animation的CALayer提供可視內容的繪制和動畫(Drawing and Animation)能力外,還有其他兩個功能:
- 布局與子視圖的管理(Layout and subview management)。
- 事件處理(Event handling)。
Tree
在UIkit中所有的視圖都從一個叫做UIVIew的基類派生而來,視圖可以嵌套包含其他視圖-子視圖,子視圖也可以再嵌套包含其他子視圖,經過數層的嵌套,就形成一個由不同視圖組成的層級關系樹,視圖樹(View Tree),如下圖所示。
從上述描述我們知道,每一個UIView都有一個CALayer與之對應,我們把這個對應的Layer叫做UIVIew的Backing Layer。在一個視圖樹中,每一個View的Layer也跟其View一樣組成層級關系樹,這個由Layer組成的樹叫做圖層樹(Layer Tree 或 Model Layer Tree)。
View的職責之一就是創建管理這個與它對應的Layer,以確保當SubViews在視圖樹(View Tree)中添加或者被移除的時候,SubViews關聯的Layer也同樣在對應的圖層樹(Layer Tree)中有著相同的操作。
那么為什么iOS要基于UIView和CALayer提供兩個平行的層級關系呢,為什么要將 CALayer 獨立出來,直接使用 UIView 統一管理不行嗎?為什么不用一個統一的對象來處理所有事情呢?
其實這樣設計的主要原因就是為了職責分離、功能拆分,方便代碼的復用。iOS和MacOS下都可以使用Core Animation框架來負責可視內容的渲染,但是兩個系統交互規則不同,所以要將渲染內容和交互分開設計。所以iOS有UIKit和UIView,MacOS則是AppKit和NSView。
實際上并不是只有視圖樹(View Tree)和圖層樹(Layer Tree)這兩個層級關系樹,還有呈現樹(Presentation Tree)、渲染樹(Render Tree),共四個,每一個Tree都扮演著不同的角色。
渲染樹(Render Tree):當UIView對象需要渲染時,它會將自己對應的CALayer對象提交到渲染樹中。在渲染樹中,CALayer對象會根據自己的層級關系和樣式信息進行布局和繪制。渲染樹在Core Animation中還有一個重要的作用就是支持動畫效果。當View的某個屬性發生改變時,Core Animation會自動更新渲染樹中對應CALayer對象的屬性,并使用硬件加速技術對視圖進行動畫渲染。
呈現樹(Presentation Tree)是渲染樹在動畫過程中的一個快照,用于描述動畫過程中每一幀Layer的狀態。當對一個Layer進行動畫處理時,系統會根據動畫開始和結束時的狀態,生成一系列中間狀態,這些狀態被稱為關鍵幀(Keyframe)。每個關鍵幀都對應著呈現樹中的一個狀態,當動畫播放時,系統會根據關鍵幀逐漸改變Layer的狀態,從而實現流暢的動畫效果。
呈現樹的實現原理非常簡單,當我們對一個Layer進行動畫處理時,系統會自動創建一個呈現樹,用于保存Layer在動畫過程中的狀態。每當動畫的狀態發生改變時,系統就會更新呈現樹中對應CALayer對象的屬性,從而反映出視圖的當前狀態。呈現樹不會影響視圖(UIView)的布局和繪制過程,它僅僅用于保存Layer在動畫過程中的狀態,所以它的計算成本非常小,可以實現高效的動畫渲染。
Core Animation(渲染管線)
Core Animation使用了一個基于GPU的渲染管線來呈現View和Layer,并通過優化和預合成來提高性能,我們把這個過程叫做Core Animation渲染管線。
Core Animation渲染管線是從App開始,App內構建了視圖層次結構,視圖層次結構可以是View Tree,也可以是直接使用Core Animation構建的的Layer Tree。
App不直接通過Core Animation做圖像的渲染工作,而是將上述的視圖層次結構數據打包提交給Render Server(渲染服務器)。Render Server是系統中一個獨立的進程,App使用IPC(進程間通信)的方式與Render Server進行通信。Core Animation框架分為客戶端和服務器版本,App內使用的是Core Animation框架的客戶端版本,Render Server使用的是Core Animation服務器版本。
Render Server收到視圖層次結構數據后,Core Animation的服務端利用OpenGL/Metal渲染視圖層次結構數據,當然渲染工作是在GPU中運行的。
視圖層次結構數據渲染完成后,就可以通過顯示器(Display)將其顯示給用戶。
Render Loop (渲染循環)
Render Loop(渲染循環)是iOS系統中渲染圖形的核心循環,Render Loop不間斷的捕獲用戶屏幕觸摸事件,將事件傳遞給系統分析后,使用Core Animation渲染管線來更新相應場景狀態和渲染顯示相應圖形,確保應用程序的圖形表現能夠實時響應用戶操作,并以流暢的方式呈現在屏幕上。
[圖片上傳失敗...(image-2c3b50-1684293991837)]
一個Core Animation渲染管線主要涉及以下幾個步驟:
Handle Events(事件處理):
應用程序接收到iOS系統傳遞個它的事件(觸摸(Touch)、網絡回調(Networking)、鍵盤操作(Keyboard)、計時器(Timers)),應用程序根據事件類型執行對應操作,比如創建和調整視圖層級、設置視圖的Frame、修改背景顏色、添加一個動畫等。
這些操作最終都會被CALayer標記,并通過Core Animation提交到一個中間狀態中去。注意這時候對視圖或者圖層屬性的修改還沒生效,只是打上標記,意思是需要更新。開發者也可以使用setNeedsLayout來手動標記。
Commit Transaction (提交事務):
在Application的階段的最后一個一步是Commit Transaction,也是將呈現樹(Presentation Tree)打包發送給Render Server前的最后一步。Commit Transcation其實可以細分為 4 個步驟:布局(Layout)、顯示(Display)、準備(Prepare)、提交(Commit)。
布局(Layout):
這個階段主要進行視圖的構建和布局,具體步驟包括
- 調用View里面重載的layoutSubviews方法;
- 創建View,并通過addSubview方法添加SubView;
- 計算View的布局,自動布局約束的計算等。
一旦計算出布局,系統就會調用setNeedsDisplay方法,標記View需要更新。
顯示(Display):
這個階段是交給Core Graphics的CGContext進行視圖的繪制,并不是在顯示器上顯示,主要繪制以下內容:
- 如果開發者重寫了drawRect:方法,那么系統會調用重載的drawRect:方法,在這個方法中繪制圖形,繪制的圖形bitmap數據保存在內存中。
- 使用Core Text或者Core Graphics進行文字的繪制。
這個階段仍然使用CPU和系統內存進行繪制,還沒使用到顯卡。
準備(Prepare):
這階段Core Animation準備將動畫數據發送到渲染服務器,一般進行圖像的解碼和轉換等操作。
- 如果視圖或者子視圖里面包含圖像顯示,將會進行圖像的解碼;
- 如果視圖或者子視圖里面包含圖像顯示,但是這種圖像格式不被GPU不支持,那么執行圖像轉換操作。
提交(Commit):
這階段將圖層進行打包發送給Render Server。該過程會執行遞歸圖層樹,所以如果圖層數太復雜,此階段產生更大的消耗。
以下階段就是在Render Server進程中執行了。
Decode(解碼):
當上述打包好的圖層數據被傳輸到Render Server之后,首先會進行Decode(解碼)操作。就是將打包的圖層數據解碼成Render Server能夠讀取的視圖層次結構。完成解碼之后需要等待下一次垂直同步信號(VSync)后才會執行下一步Draw Calls操作。 這個等待是為了保證上一個幀緩沖區的內容能夠被完整的渲染顯示。
Draw Calls(繪制回調):
當接收到下一次垂直同步信號(VSync)后,Render Server開始對GPU(在底層使用OpenGL 或Metal)發出繪制調用。
Render(渲染)
當收到Render Server繪制調用后,會立即開始渲染階段的操作。渲染階段的操作可以分為兩個部分,準備(Render prepare)和執行(Render execute)。
- 準備渲染(Render prepare):準備圖層樹,準備運行GPU渲染管線的繪制命令。根據事務提交階段的時間通過插值計算頁面上的動畫當前幀的狀態;將圖層和特效分解為一步步簡單操作。
- 執行渲染(Render execute):這階段使用GPU的渲染管線進行一步步的圖形圖像繪制操作,最終生成圖形以供下一步顯示。
理想的狀態是在收到下一個垂直同步信號(VSync)之前,就需要完成當前所有的渲染操作,包括幀緩沖區的交換,因為我們希望在收到下一個垂直同步信號(VSync)后能夠立即展示幀緩沖區中的渲染后的圖像。
Display(顯示)
此處的Display(顯示)和Commit Transaction階段的Display不一樣,這個階段的Display就是顯示器從準備好的幀緩沖區逐步取出像素信息展示在顯示器上。
以上各個階段的操作并不能在一個同步周期(兩個垂直同步信號之間的時間)內一次性完成,蘋果為了優化渲染管線流程,提高圖形圖像的渲染效率,將上述各個階段的操作分散到多個同步周期中,所以Render Loop(渲染循環)中的多個渲染管線的時間是并行的交織在一起的。入下圖,當CPU正在讀取第N幀時,此時GPU正在渲染前一幀(第N-1幀),顯示器正在顯示N-2幀,如此交替往復就形成了Render Loop。
Offscreen Rendering(離屏渲染)
我們都知道,在屏幕中顯示內容時,通常需要使用至少一個幀緩沖區(Frame Buffer)來存儲像素數據,這是GPU用來渲染和存儲最終圖像的地方。GPU會將渲染結果直接寫入幀緩沖區(Frame Buffer),然后顯示在屏幕上。
然而,在某些情況下,可能會有一些限制,使得不能或者不需要直接將渲染結果寫入幀緩沖區(Frame Buffer)。這些限制可能是由于特定的渲染效果、開發者主動指定的光柵化。在這種情況下,渲染結果需要先存儲到一個單獨的內存區域--離屏緩沖區(Offscreen Buffer),然后等到合適的時機再將其寫入幀緩沖區(Frame Buffer)。這個過程就被稱為離屏渲染(Offscreen Rendering)。
離屏渲染可以在不直接顯示在屏幕上的情況下進行圖像處理和渲染操作。通過將渲染結果存儲在離屏緩沖區中(Offscreen Buffer),可以進行后續的處理、讀取或傳輸。一些常見的用途包括圖像后處理、渲染到紋理(Render-to-texture)以及生成圖像數據供后續使用。
但是離屏渲染會增加渲染操作的延遲。一方面離屏渲染執行過程中會使得GPU不斷在幀緩沖區(Frame Buffer)和離屏緩沖區(Offscreen Buffer)之間進行上下文切換,這種切換的代價非常大;另一方面離屏渲染需要額外的內存讀寫操作,也需要更多的內存來存儲渲染結果,所以大量的離屏渲染會內存壓力過大。
離屏渲染的用途
離屏渲染可能帶性能問題,那為什么還要使用離屏渲染技術呢?
- 一些特殊渲染效果(比如Shadows、Masks, Rounded Rectangles、 Visual Effects等)需要使用離屏緩沖區來保存渲染的中間狀態,所以不得不使用離屏渲染。
- 為了效率,開發者可以將內容提前渲染保存在離屏緩沖區中(開啟shouldRasterize光柵化),后續如果使用直接從離屏緩沖區讀取即可,不需要重復渲染。
Hitch(可能出現的性能問題)
通過上述的介紹,我們知道Render Loop的主要工作都是在CPU和GPU中進行。CPU和GPU是并行進行工作的,當CPU正在處理第N幀時,GPU正在渲染前一幀(第N-1),依此類推。所以,CPU和GPU不能分別在一幀時間內完成對應的工作,都會延遲下一幀的處理、渲染和顯示,用戶界面就會出現卡頓現象。
Commit hitch(提交故障)
在Commit Transaction(提交事務)階段應用程序可能需要花費更多時間來處理事務,導致應用程序不得不延遲向Render Server(渲染服務器)提交下一幀的圖層數據,此階段發生的故障叫做Commit hitch(提交故障)。
發生Commit hitch主要是在Commit hitch這個階段主線程任務過重,可以通過以下方法減輕主線程的負擔。
盡可能的保持View輕量:
- 減少drawRect:方法的復雜度,如果CALayer的方法能夠替換自定義繪制的內容盡量使用CALayer方法。
- 如果用不到drawRect:,就不要重寫drawRect:方法。
- 避免頻繁的創建和銷毀視圖,可以使用重用機制復用視圖。
- 可以使用隱藏屬性替代頻繁的移除和插入視圖。
減少負擔重或者冗余的布局:
- 使用setNeedsLayout方法來刷新視圖布局而不是使用layoutIfNeeded方法。
- 盡量減少約束的使用。
- 盡量減少遞歸布局的使用。
Render hitch(渲染故障)
上面提到Render階段有兩部分工作:渲染準備和渲染執行。如果這兩部分的工作中的任一工作都沒有及時的在一幀的時間內完成,都會造成Render hitch(渲染故障)。
其中大量的離屏渲染工作是造成Render hitch的一個重要原因,下面幾種情況會觸發離屏渲染:
- 使用了 mask 的 layer (layer.mask)
- 需要進行裁剪的 layer (layer.masksToBounds / view.clipsToBounds)
- 設置了組透明度為 YES,并且透明度不為 1 的 layer (layer.allowsGroupOpacity/layer.opacity)
- 添加了投影的 layer (layer.shadow*)
- 采用了光柵化的 layer (layer.shouldRasterize)
- 繪制了文字的 layer (UILabel, CATextLayer, Core Text 等)
所以如果開發者要避免發生Render hitch,就要盡量避免觸發主動或者被動離屏渲染操作。
參考文檔:
1、《iOS 頁面渲染 - 流程》
https://juejin.cn/post/7038170936163450910
2、《iOS下的渲染框架》
https://zhuanlan.zhihu.com/p/157556221
3、《iOS Rendering 渲染全解析》
https://github.com/RickeyBoy/Rickey-iOS-Notes/blob/master/%E7%AC%94%E8%AE%B0/iOS%20Rendering.md
4、《Core Animation Basics》
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/CoreAnimationBasics/CoreAnimationBasics.html
5、《Understanding UIKit Rendering》
https://www.wwdcnotes.com/notes/wwdc11/121/
6、《WWDC 2021: Avoid hitches and discover the Render Loop》
https://a11y-guidelines.orange.com/en/mobile/ios/wwdc/nota11y/2021/21hitches/
7、《Оптимизация рендера в iOS: frame buffer, Render Server, FPS, CPU vs GPU》
https://habr.com/ru/post/647177/
8、《Rendering performance of iOS apps》
https://dmytro-anokhin.medium.com/rendering-performance-of-ios-apps-4d09a9228930
9、《Advanced Graphics and Animations for iOS Apps》
https://www.wwdcnotes.com/notes/wwdc14/419/
10、《Core Animation Essentials》
https://www.wwdcnotes.com/notes/wwdc11/421/
11、《iOS Core Animation: The Layer Tree》
https://www.informit.com/articles/article.aspx?p=2128062