iOS性能優化 - 界面顯示原理

對于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信號發出后,前幀緩存和后幀緩存會瞬間切換,后幀緩存會變成新的前幀緩存,同時舊的前幀緩存會變成新的后幀緩存。
iOS屏幕渲染機制

2.卡頓原因

我們手機屏幕的刷幀率是60FPS(Frame per Second 幀/秒),也就是會所1秒鐘的時間,屏幕可以刷新60幀(次)。完成一幀刷新的用時是16.6毫秒。因此垂直同步信號VSync就是每16.6毫秒發出一次。

通過對上面界面渲染原理的探究,可以總結出造成卡頓的主要原因就是:

  • 在一個 VSync 時間段內,CPU 或者 GPU 沒有完成內容提交,阻礙了顯示流程,造成掉了幀現象
  • Vsync 與雙緩沖的意義:強制同步屏幕刷新,以掉幀為代價解決屏幕撕裂問題。


    卡頓原因展示

3.iOS 中的渲染框架

iOS渲染框架1

通過上面基礎原理的探究,對屏幕的渲染有了一定的了解。那么在iOS中的整體渲染流程,基本如上圖所示。在硬件基礎之上,iOS 中有 Core GraphicsCore AnimationCore ImageOpenGLMetal 等多種軟件框架來繪制內容,在 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初探

CoreAnimation

Core Animation,它本質上可以理解為一個復合引擎,主要職責包含:渲染、構建和實現動畫

Core Animation 是 AppKit 和 UIKit 完美的底層支持,同時也被整合進入 Cocoa 和 Cocoa Touch 的工作流之中,它是 app 界面渲染和構建的最基礎架構。 Core Animation 的職責就是盡可能快地組合屏幕上不同的可視內容,這個內容是被分解成獨立的 layer(iOS 中具體而言就是 CALayer),并且被存儲為樹狀層級結構。這個樹也形成了 UIKit 以及在 iOS 應用程序當中你所能在屏幕上看見的一切的基礎。

UIView和CALayer關系

在CoreAnimation的渲染中,與開發者關系最大的就是UIViewCALayer了,為了能更好的理解CoreAnimation的渲染流程,我們必須明確這兩者之間的關系。

UIView

UIView - Apple
Views are the fundamental building blocks of your app's user interface, and the UIView 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 內部都有一個 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

兩者主要的關系如下:

  1. CALayer 是 UIView 的屬性之一,負責渲染和動畫,提供可視內容的呈現。
  2. 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渲染流程

關于CoreAnimation的渲染流程,通過上圖可以比較清晰的看出,主要可以總結為以下步驟:

  1. Handle Events:這個過程中會先處理點擊事件,這個過程中有可能會需要改變頁面的布局和界面層次。
  2. Commit Transaction:此時 app 會通過 CPU 處理顯示內容的前置計算,比如布局計算、圖片解碼等任務。之后將計算好的圖層進行打包發給 Render Server。
  3. Decode:打包好的圖層被傳輸到 Render Server 之后,首先會進行解碼。注意完成解碼之后需要等待下一個 RunLoop才會執行下一步 Draw Calls。
  4. Draw Calls:解碼完成后,Core Animation 會調用下層渲染框架(比如 OpenGL 或者 Metal)的方法進行繪制,進而調用到 GPU。
  5. Render:這一階段主要由 GPU 進行渲染。
  6. Display:顯示階段,需要等 render 結束的下一個 RunLoop 觸發顯示。

Commit Transaction 渲染原理

在日常的開發中,作為開發者能影響到的就是 Handle Events 和 Commit Transaction 這兩個階段,這也是開發者接觸最多的部分。Handle Events 就是處理觸摸事件,而 Commit Transaction 這部分中主要進行的是:LayoutDisplayPrepareCommit等四個具體的操作。

Layout

這個階段主要是構建視圖,遍歷的操作[UIView layerSubview][CALayer layoutSubLayers]

  • 調用重載的 layoutSubviews 方法
  • 創建視圖,并通過 addSubview方法添加子視圖
  • 計算視圖布局,即所有的Layout Constraint

由于這個階段是在 CPU 中進行,通常是 CPU 限制或者 IO 限制,所以我們應該盡量高效輕量地操作,減少這部分的時間,比如減少非必要的視圖創建、簡化布局計算、減少視圖層級等。代碼的主要調用結構如下:

Layout調用偽代碼

Display

這個階段主要是交給 Core Graphics 進行視圖的繪制,注意不是真正的顯示,而是得到前文所說的contents數據。

根據UIView和CALayer的關系,我們知道,主要是CALayer來負責一個view的展示,并最終將得到的bitmap賦值給contents屬性,保存在backing store中供后續使用。

我們知道view的繪制會在drawRect:方法中,我們在其中打斷點,可得到一下堆棧信息

drawRect:繪制堆棧

通過調用堆棧,可以得到此過程主要如下:

  1. 根據Layout獲得的數據,進行展示
  2. 通過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關閉上下文
  1. 以上是默認的流程,如果自己重寫實現了drawRect:,這個方法會直接調用 Core Graphics繪制方法得到bitmap數據,同時系統會額外申請一塊內存,用于暫存繪制好的bitmap。這樣繪制過程從 GPU 轉移到了CPU,這就導致了一定的效率損失。與此同時,這個過程會額外使用 CPU 和內存,因此需要高效繪制,否則容易造成 CPU 卡頓或者內存爆炸
Display偽代碼

Prepare

Core Animation 額外的工作,主要圖片解碼和轉換

Commit

打包圖層并將它們發送到 Render Server

注意 commit 操作是依賴圖層樹遞歸執行的,所以如果圖層樹過于復雜,commit 的開銷就會很大。這也是我們希望減少視圖層級,從而降低圖層樹復雜度的原因。

Render Server相關

Render Server 通常是 OpenGL或者是 Metal。以 OpenGL 為例,那么上圖主要是 GPU 中執行的操作,具體主要包括:

  1. GPU 收到Command Buffer,包含圖元 primitives 信息
  2. Tiler 開始工作:先通過頂點著色器 Vertex Shader對頂點進行處理,更新圖元信息
  3. 平鋪過程:平鋪生成 tile bucket的幾何圖形,這一步會將圖元信息轉化為像素,之后將結果寫入Parameter Buffer
  4. Tiler更新完所有的圖元信息,或者 Parameter Buffer 已滿,則會開始下一步
  5. Renderer工作:將像素信息進行處理得到bitmap,之后存入 Render Buffer
  6. Render Buffer 中存儲有渲染好的 bitmap,供之后的 Display 操作使用

使用 Instrument 的 OpenGL ES,可以對過程進行監控。OpenGL ES tiler utilization 和 OpenGL ES renderer utilization 可以分別監控 Tiler 和 Renderer 的工作情況

參考

iOS性能優化
iOS保持界面流暢
iOS Rendering 渲染全解析
深入理解 iOS Rendering Process

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372