1. 屏幕顯示圖像的原理
CRT 顯示原理,準備進行掃描時,顯示器會發(fā)出一個水平同步信號(horizonal synchronization),簡稱 HSync。而當一幀畫面繪制完成后,電子槍回復到原位,準備畫下一幀前,顯示器會發(fā)出一個垂直同步信號(vertical synchronization),簡稱 VSync。顯示器通常以固定頻率進行刷新,這個刷新率就是 VSync 信號產生的頻率。

通常來說,計算機系統(tǒng)中 CPU、GPU、顯示器是以上面這種方式協(xié)同工作的。CPU 計算好顯示內容提交到 GPU,GPU 渲染完成后將渲染結果放入幀緩沖區(qū),隨后視頻控制器會按照 VSync 信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經過可能的數(shù)模轉換傳遞給顯示器顯示。
1.1 水平同步信號:HSync
在簡單的情況下,幀緩沖區(qū)只有一個,幀數(shù)據(jù)的讀取和刷新都會有比較大的效率問題,為了解決效率問題,顯示系統(tǒng)通常會引入兩個緩沖區(qū),即雙緩沖機制。
在這種情況下,GPU 會預先渲染好一幀放入一個緩沖區(qū)內,讓視頻控制器讀取,當下一幀渲染好后,GPU 會直接把視頻控制器的指針指向第二個緩沖器。如此一來效率會有很大的提升。
1.2 垂直同步信號:VSync
雙緩沖雖然能解決效率問題,但會引入一個新的問題。當視頻控制器還未讀取完成時,即屏幕內容剛顯示一半時,GPU 將新的一幀內容提交到幀緩沖區(qū)并把兩個緩沖區(qū)進行交換后,視頻控制器就會把新的一幀數(shù)據(jù)的下半段顯示到屏幕上,造成畫面撕裂現(xiàn)象。
為了解決這個問題,GPU 通常有一個機制叫做垂直同步(簡寫也是 V-Sync),當開啟垂直同步后,GPU 會等待顯示器的 VSync 信號發(fā)出后,才進行新的一幀渲染和緩沖區(qū)更新。這樣能解決畫面撕裂現(xiàn)象,也增加了畫面流暢度,但需要消費更多的計算資源,也會帶來部分延遲。
2. 卡頓產生的原因和解決方案

在 VSync 信號到來后,系統(tǒng)圖形服務會通過 CADisplayLink 等機制通知 App,App 主線程開始在 CPU 中計算顯示內容,比如視圖的創(chuàng)建、布局計算、圖片解碼、文本繪制等。隨后 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。隨后 GPU 會把渲染結果提交到幀緩沖區(qū)去,等待下一次 VSync 信號到來時顯示到屏幕上。由于垂直同步的機制,如果在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是界面卡頓的原因。
從上面的圖中可以看到,CPU 和 GPU 不論哪個阻礙了顯示流程,都會造成掉幀現(xiàn)象。所以開發(fā)時,也需要分別對 CPU 和 GPU 壓力進行評估和優(yōu)化。
3. CPU 資源消耗原因和解決方案
3.1 對象創(chuàng)建
對象的創(chuàng)建會分配內存、調整屬性、甚至還有讀取文件等操作,比較消耗 CPU 資源。
- 盡量用輕量的對象代替重量的對象,可以對性能有所優(yōu)化。
- 如果對象不涉及 UI 操作,則盡量放到后臺線程去創(chuàng)建。
- 通過 Storyboard 創(chuàng)建視圖對象時,其資源消耗會比直接通過代碼創(chuàng)建對象要大非常多,在性能敏感的界面里,Storyboard 并不是一個好的技術選擇。
- 盡量推遲對象創(chuàng)建的時間,并把對象的創(chuàng)建分散到多個任務中去。
- 如果對象可以復用,并且復用的代價比釋放、創(chuàng)建新對象要小,那么這類對象應當盡量放到一個緩存池里復用。
3.2 對象調整
對象的調整也經常是消耗 CPU 資源的地方。應該盡量減少不必要的屬性修改,盡量避免調整視圖層次、添加和移除視圖。
3.3 對象銷毀
對象的銷毀雖然消耗資源不多,但累積起來也是不容忽視的。如果對象可以放到后臺線程去釋放,那就挪到后臺線程去。
3.4 布局計算
視圖布局的計算是 App 中最為常見的消耗 CPU 資源的地方。如果能在后臺線程提前計算好視圖布局、并且對視圖布局進行緩存,那么這個地方基本就不會產生性能問題了。
3.5 文本計算
文本計算盡量放在后臺線程進行以避免阻塞主線程。如果你用 CoreText 繪制文本,那就可以先生成 CoreText 排版對象,然后自己計算了,并且 CoreText 對象還能保留以供稍后繪制使用。
3.6 文本渲染
文本渲染建議提前進行計算緩存,異步加載內容。
3.7 圖像解碼和繪制
在后臺線程先把圖片繪制到 CGBitmapContext 中,然后從 Bitmap 直接創(chuàng)建圖片。目前常見的網絡圖片庫都自帶這個功能。
4. GPU 資源消耗原因和解決方案
相對于 CPU 來說,GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點描述(三角形),應用變換(transform)、混合并渲染,然后輸出到屏幕上。通常你所能看到的內容,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類。
4.1 紋理的渲染
避免這種情況的方法只能是盡量減少在短時間內大量圖片的顯示,盡可能將多張圖片合成為一張進行顯示。
目前來說,iPhone 4S 以上機型,紋理尺寸上限都是 4096×4096 ,所以盡量不要讓圖片和視圖的大小超過這個值。
4.2 視圖的混合
如果視圖結構過于復雜,混合的過程也會消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,應用應當盡量減少視圖數(shù)量和層次,并在不透明的視圖里標明 opaque 屬性以避免無用的 Alpha 通道合成。
4.3 圖形的生成
當一個列表視圖中出現(xiàn)大量圓角的 CALayer,并且快速滑動時,可以觀察到 GPU 資源已經占滿,而 CPU 資源消耗很少。為了避免這種情況,可以嘗試開啟 CALayer.shouldRasterize 屬性,但這會把原本離屏渲染的操作轉嫁到 CPU 上去。
4.4 預排版
當獲取到 API JSON 數(shù)據(jù)后,把每條 Cell 需要的數(shù)據(jù)都在后臺線程計算并封裝為一個布局對象 CellLayout。CellLayout 包含所有文本的 CoreText 排版結果、Cell 內部每個控件的高度、Cell 的整體高度。
TableView 在請求各個高度函數(shù)時,不會消耗任何多余計算量;當把 CellLayout 設置到 Cell 內部時,Cell 內部也不用再計算布局了。
4.5 預渲染
對于 TableView 來說,Cell 內容的離屏渲染會帶來較大的 GPU 消耗。而 CPU 卻比較清閑。為了避免離屏渲染,你應當盡量避免使用 layer 的 border、corner、shadow、mask 等技術,而盡量在后臺線程預先繪制好對應內容。
4.6 異步繪制
當 TableView 快速滑動時,會有大量異步繪制任務提交到后臺線程去執(zhí)行。目前有些第三方微博客戶端(比如 VVebo、墨客等),使用了一種方式來避免高速滑動時 Cell 的繪制過程,相關實現(xiàn)見這個項目:VVeboTableViewDemo。它的原理是,當滑動時,松開手指后,立刻計算出滑動停止時 Cell 的位置,并預先繪制那個位置附近的幾個 Cell,而忽略當前滑動中的 Cell。
4.7 全局并發(fā)控制
大量的任務提交到后臺隊列時,某些任務會因為某些原因(此處是 CGFont 鎖)被鎖住導致線程休眠,或者被阻塞,concurrent queue 隨后會創(chuàng)建新的線程來執(zhí)行其他任務。
使用 serial queue 充分利用多核 CPU 的資源,為不同優(yōu)先級創(chuàng)建和 CPU 數(shù)量相同的 serial queue,每次從 pool 中獲取 queue 時,會輪詢返回其中一個 queue。把 App 內所有異步操作,包括圖像解碼、對象釋放、異步繪制等,都按優(yōu)先級不同放入了全局的 serial queue 中執(zhí)行,這樣盡量避免了過多線程導致的性能問題。