性能優化是一個很重要的一部分,我們首先看CPU和GPU的部分,想知道CPU和GPU是怎么優化的,就必須要明白CPU和GPU的原理.
1. 屏幕的成像原理
首先從過去的 CRT 顯示器原理說起。CRT 的電子槍按照上面方式,從上到下一行行掃描,掃描完成后顯示器就呈現(一幀畫面),隨后電子槍回到初始位置繼續下一次掃描。
為了把顯示器的顯示過程和系統的視頻控制器進行同步,顯示器(或者其他硬件)會用硬件時鐘產生一系列的定時信號。當電子槍換到新的一行,準備進行掃描時,顯示器會發出一個水平同步信號(horizonal synchronization),簡稱 HSync;而當一幀畫面繪制完成后,電子槍回復到原位,準備畫下一幀前,顯示器會發出一個垂直同步信號(vertical synchronization),簡稱 VSync。
顯示器通常以固定頻率進行刷新,這個刷新率就是 VSync 信號產生的頻率。盡管現在的設備大都是液晶顯示屏了,但原理仍然沒有變。
通常來說,計算機系統中 CPU、GPU、顯示器是以上面這種方式協同工作的。CPU 計算好顯示內容提交到 GPU,GPU 渲染完成后將渲染結果放入幀緩沖區,隨后視頻控制器會按照 VSync 信號逐行讀取幀緩沖區的數據,經過可能的數模轉換傳遞給顯示器顯示。
在最簡單的情況下,幀緩沖區只有一個,這時幀緩沖區的讀取和刷新都都會有比較大的效率問題。為了解決效率問題,顯示系統通常會引入兩個緩沖區,即雙緩沖機制。在這種情況下,GPU 會預先渲染好一幀放入一個緩沖區內,讓視頻控制器讀取,當下一幀渲染好后,GPU 會直接把視頻控制器的指針指向第二個緩沖器。如此一來效率會有很大的提升。
雙緩沖雖然能解決效率問題,但會引入一個新的問題。當視頻控制器還未讀取完成時,即屏幕內容剛顯示一半時,GPU 將新的一幀內容提交到幀緩沖區并把兩個緩沖區進行交換后,視頻控制器就會把新的一幀數據的下半段顯示到屏幕上,造成畫面撕裂現象。
為了解決這個問題,GPU 通常有一個機制叫做垂直同步(簡寫也是 V-Sync),當開啟垂直同步后,GPU 會等待顯示器的 VSync 信號發出后,才進行新的一幀渲染和緩沖區更新。這樣能解決畫面撕裂現象,也增加了畫面流暢度,但需要消費更多的計算資源,也會帶來部分延遲。
下圖就是iOS應用界面渲染到展示的流程:
Display 的上一層便是圖形處理單元 GPU,GPU 是一個專門為圖形高并發計算而量身定做的處理單元。這也是為什么它能同時更新所有的像素,并呈現到顯示器上。它并發的本性讓它能高效的將不同紋理合成起來。我們將有一小塊內容來更詳細的討論圖形合成。關鍵的是,GPU 是非常專業的,因此在某些工作上非常高效。比如,GPU 非常快,并且比 CPU 使用更少的電來完成工作。通常 CPU 都有一個普遍的目的,它可以做很多不同的事情,但是合成圖像在 CPU 上卻顯得比較慢。
一. 卡頓產生的原因
由于垂直同步的機制,如果在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是界面卡頓的原因。
CPU(紅色)——>GPU(藍色)
1.CPU完成計算,提交給GPU渲染,這是來個垂直同步信號,則會將渲染的內容顯示到屏幕上。
2.CPU計算時間正常,CPU渲染時間短,等待VSync
3.CPU計算時間正常或慢,GPU渲染時間長,這時來了VSync,而這一幀還沒有渲染完,那么就會出現掉幀現象,屏幕回去顯示上一幀的畫面。這樣就產生了卡頓。
4.而當下一幀VSync出現時,丟掉的那一幀畫面才會出現。
因此,我們需要平衡 CPU 和 GPU 的負荷避免一方超負荷運算。為了做到這一點,我們首先得了解 CPU 和 GPU 各自負責哪些內容。
二. CPU和GPU的職責
在 iOS 系統中,圖像內容展示到屏幕的過程需要 CPU 和 GPU 共同參與。
- CPU 負責計算顯示內容,比如視圖的創建、布局計算、圖片解碼、文本繪制等。
- 隨后 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。
- 之后 GPU 會把渲染結果提交到幀緩沖區去,等待下一次 VSync 信號到來時顯示到屏幕上。
A. CPU卡頓分析
a. 布局計算
視圖布局的計算是 App 中最為常見的消耗 CPU 資源的地方。如果能在后臺線程提前計算好視圖布局、并且對視圖布局進行緩存,那么這個地方基本就不會產生性能問題了。
不論通過何種技術對視圖進行布局,其最終都會落到對 UIView.frame/bounds/center 等屬性的調整上。上面也說過,對這些屬性的調整非常消耗資源,所以盡量提前計算好布局,在需要時一次性調整好對應屬性,而不要多次、頻繁的計算和調整這些屬性。
b. 對象創建
對象創建過程伴隨著內存分配、屬性設置、甚至還有讀取文件等操作,比較消耗 CPU 資源。盡量用輕量的對象代替重量的對象,可以對性能有所優化。比如 CALayer 比 UIView 要輕量許多,如果視圖元素不需要響應觸摸事件,用 CALayer 會更加合適。
通過 Storyboard 創建視圖對象還會涉及到文件反序列化操作,其資源消耗會比直接通過代碼創建對象要大非常多,在性能敏感的界面里,Storyboard 并不是一個好的技術選擇。
c. Autolayout
Autolayout 是蘋果本身提倡的技術,在大部分情況下也能很好的提升開發效率,但是 Autolayout 對于復雜視圖來說常常會產生嚴重的性能問題。隨著視圖數量的增長,Autolayout 帶來的 CPU 消耗會呈指數級上升。具體數據可以看這個文章:http://pilky.me/36/。 如果你不想手動調整 frame 等屬性,你可以用一些工具方法替代(比如常見的 left/right/top/bottom/width/height 快捷屬性),或者使用 ComponentKit、AsyncDisplayKit 等框架。
d. 文本計算
如果一個界面中包含大量文本(比如微博、微信朋友圈等),文本的寬高計算會占用很大一部分資源,并且不可避免。
一個比較常見的場景是在 UITableView 中,heightForRowAtIndexPath
這個方法會被頻繁調用。這里的優化就是盡量避免每次都重新進行文本的行高計算,緩存高度即可。
e. 文本渲染
屏幕上能看到的所有文本內容控件,包括 UIWebView,在底層都是通過 CoreText 排版、繪制為 Bitmap 顯示的。常見的文本控件 (UILabel、UITextView 等),其排版和繪制都是在主線程進行的,當顯示大量文本時,CPU 的壓力會非常大。對此解決方案只有一個,那就是自定義文本控件,用 TextKit 或最底層的 CoreText 對文本異步繪制。盡管這實現起來非常麻煩,但其帶來的優勢也非常大,CoreText 對象創建好后,能直接獲取文本的寬高等信息,避免了多次計算(調整 UILabel 大小時算一遍、UILabel 繪制時內部再算一遍);CoreText 對象占用內存較少,可以緩存下來以備稍后多次渲染。
f.圖像的繪制
圖像的繪制通常是指用那些以 CG 開頭的方法把圖像繪制到畫布中,然后從畫布創建圖片并顯示的過程。前面的模塊圖里介紹了 CoreGraphic 是作用在 CPU 之上的,因此調用 CG 開頭的方法消耗的是 CPU 資源。我們可以將繪制過程放到后臺線程,然后在主線程里將結果設置到 layer 的 contents 中。代碼如下:
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
g.圖片的解碼
Once an image file has been loaded, it must then be decompressed. This decompression can be a computationally complex task and take considerable time. The decompressed image will also use substantially more memory than the original.(圖片被加載后需要解碼,圖片的解碼是一個復雜耗時的過程,并且需要占用比原始圖片還多的內存資源)
①.為什么圖片需要解碼
把圖片從PNG或JPEG等格式中解壓出來,得到像素數據。如果GPU不支持這種顏色各式,CPU需要進行格式轉換。
比如應用中有一些從網絡下載的圖片,而GPU恰好不支持這個格式,這就需要CPU預先進行格式轉化。SDwebImageDecoder就是這個作用。
②.默認延遲解碼
當你用 UIImage 或 CGImageSource 的那幾個方法創建圖片時,為了節省內存,圖片數據并不會立刻解碼。圖片設置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的數據才會得到解碼。這一步是發生在主線程的,并且不可避免。
如果想要繞開這個機制,可以使用 ImageIO (怎么使用?)或者提前在后臺線程先把圖片繪制到 CGBitmapContext 中,然后從 Bitmap 直接創建圖片。目前常見的網絡圖片庫都自帶這個功能。
③.不一定是默認延遲解碼
常用的 UIImage 加載方法有 imageNamed和 imageWithContentsOfFile。其中 imageNamed加載圖片后會馬上解碼,并且系統會將解碼后的圖片緩存起來,但是這個緩存策略是不公開的,我們無法知道圖片什么時候會被釋放。因此在一些性能敏感的頁面,我們還可以用 static 變量 hold 住 imageNamed加載到的圖片避免被釋放掉,以空間換時間的方式來提高性能。
imageWithContentsOfFile解碼后的UIImage對象如果作為臨時變量被釋放了,則它下次仍然會解碼。
//圖片解碼的代碼
- (void)image
{
UIImageView *imageView = [[UIImageView alloc] init];
imageView.frame = CGRectMake(100, 100, 100, 56);
[self.view addSubview:imageView];
self.imageView = imageView;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 獲取CGImage
CGImageRef cgImage = [UIImage imageNamed:@"timg"].CGImage;
// alphaInfo
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
// bitmapInfo
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
// size
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
// context
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
// draw
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
// get CGImage
cgImage = CGBitmapContextCreateImage(context);
// into UIImage
UIImage *newImage = [UIImage imageWithCGImage:cgImage];
// release
CGContextRelease(context);
CGImageRelease(cgImage);
// back to the main thread
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = newImage;
});
});
}
我們來總結一下處理CPU的卡頓:
1.盡量用輕量級的對象,比如用不到事件處理的地方,可以考慮使用CAlayer取代UIView;能用基本數據類型,就別用NSNumber類型。
2.不要頻繁地跳用UIVIew的相關屬性,比如frame、bounds、transform等屬性,盡量減少不必要的修改。
3.盡量提前計算好布局,在有需要時一次性調整對應的布局,不要多次修改屬性。
4.Autolayout會比直接設置frame消耗更多的CPU資源。
5.圖片的size最好剛好跟UIImageView的size保持一致。
6.控制一下線程的最大并發數量。
7.盡量把耗時的操作放到子線程。
8.文本處理(尺寸的計算,繪制)。
9.圖片處理(解碼、繪制)。
B. GPU卡頓分析
1.On-SCreen Rendering:當前屏幕渲染,在當前用語顯示的屏幕緩沖區進行渲染操作。
2.Off-Screen Rendring: 離屏渲染,在當前屏幕緩沖區以外新開辟一個緩沖區進行渲染操作。
相對于 CPU 來說,GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點描述(三角形),應用變換(transform)、混合并渲染,然后輸出到屏幕上。寬泛的說,(大多數 CALayer 的屬性都是用 GPU 來繪制)。
a. 以下一些操作會降低 GPU 繪制的性能:
①.大量幾何結構
所有的 Bitmap,包括圖片、文本、柵格化的內容,最終都要由內存提交到顯存,綁定為 GPU Texture。不論是提交到顯存的過程,還是 GPU 調整和渲染 Texture 的過程,都要消耗不少 GPU 資源。當在較短時間顯示大量圖片時(比如 TableView 存在非常多的圖片并且快速滑動時),CPU 占用率很低,GPU 占用非常高,界面仍然會掉幀。
避免這種情況的方法只能是盡量減少在短時間內大量圖片的顯示,盡可能將多張圖片合成為一張進行顯示。
另外當圖片過大,超過 GPU 的最大紋理尺寸時,圖片需要先由 CPU 進行預處理,這對 CPU 和 GPU 都會帶來額外的資源消耗。目前來說,iPhone 4S 以上機型,(紋理尺寸上限都是 4096x4096),更詳細的資料可以看這里:iosres.com。所以,盡量不要讓圖片和視圖的大小超過這個值。
②.視圖以及圖層的混合
屏幕上每一個點都是一個像素,像素有R、G、B三種顏色構成(有時候還帶有alpha值)。如果某一塊區域上覆蓋了多個layer,最后的顯示效果受到這些layer的共同影響。舉個例子,上層是藍色(RGB=0,0,1),透明度為50%,下層是紅色(RGB=1,0,0)。那么最終的顯示效果是紫色(RGB=0.5,0,0.5)。
公式:
0.5 0 0.5
R = S + D * (1 - Sa) = 0 + 0 * (1 - 0.5) = 0
0 1 0.5
當多個視圖(或者說 CALayer)重疊在一起顯示時,GPU 會首先把他們混合到一起。如果視圖結構過于復雜,混合的過程也會消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,(應用應當盡量減少視圖數量和層次,并且減少不必要的透明視圖)
b.離屏渲染
離屏渲染是指圖層在被顯示之前,GPU在當前屏幕緩沖區以外新開辟一個緩沖區進行渲染操作。離屏渲染耗時是發生在離屏這個動作上面,而不是渲染。為什么離屏這么耗時?原因主要有創建緩沖區和上下文切換。創建新的緩沖區代價都不算大,付出最大代價的是上下文切換。
①. 上下文切換
不管是在GPU渲染過程中,還是一直所熟悉的進程切換,上下文切換在哪里都是一個相當耗時的操作。首先我要保存當前屏幕渲染環境,然后切換到一個新的繪制環境,申請繪制資源,初始化環境,然后開始一個繪制,繪制完畢后銷毀這個繪制環境,如需要切換到On-Screen Rendering或者再開始一個新的離屏渲染重復之前的操作。
②. 渲染流程
我們先看看最基本的渲染通道流程:
我們再來看看需要Offscreen Render的渲染通道流程:
一般情況下,OpenGL會將應用提交到Render Server的動畫直接渲染顯示(基本的Tile-Based渲染流程),但對于一些復雜的圖像動畫的渲染并不能直接渲染疊加顯示,而是需要根據Command Buffer分通道進行渲染之后再組合,這一組合過程中,就有些渲染通道是不會直接顯示的;對比基本渲染通道流程和Masking渲染通道流程圖,我們可以看到到Masking渲染需要更多渲染通道和合并的步驟;而這些沒有直接顯示在屏幕的上的通道(如上圖的 Pass 1 和 Pass 2)就是Offscreen Rendering Pass。
Offscreen Render為什么卡頓,從上圖我們就可以知道,Offscreen Render需要更多的渲染通道,而且不同的渲染通道間切換需要耗費一定的時間,這個時間內GPU會閑置,當通道達到一定數量,對性能也會有較大的影響;
為什么會產生離屏渲染?
首先,OpenGL提交一個命令到Command Buffer,隨后GPU開始渲染,渲染結果放到Render Buffer中,這是正常的渲染流程。【但是有一些復雜的效果無法直接渲染出結果,它需要分步渲染最后再組合起來】,比如添加一個蒙版(mask)。
會造成 offscreen rendering 的原因有:
陰影(UIView.layer.shadowOffset/shadowRadius/…)
圓角(當 UIView.layer.cornerRadius 和 UIView.layer.maskToBounds 一起使用時)
圖層蒙板(Mask)
開啟光柵化(shouldRasterize = true,同時設置 rasterizationScale)
③. Mask
一個圖層可以有一個和它相關聯的 mask(蒙板),mask 是一個擁有 alpha 值的(位圖)(不是矢量圖,所以矢量圖是不能作為遮罩)。只有在 mask 中顯示出來的(即圖層中的部分)才會被渲染出來。
使用陰影時同時設置 shadowPath 就能避免離屏渲染大大提升性能,圓角觸發的離屏渲染可以用 CoreGraphics 將圖片處理成圓角來避免。
CALayer 有一個 shouldRasterize 屬性,將這個屬性設置成 true 后就開啟了光柵化。
④. 光柵化
光柵化其實是一種將幾何圖元變為二維圖像的過程。
你模型的那些頂點在經過各種矩陣變換后也僅僅是頂點。而由頂點構成的圖形要在屏幕上顯示出來,除了需要頂點的信息以外,還需要確定構成這個圖形的所有像素的信息。
光柵化優缺點
開啟光柵化后會將圖層繪制到一個屏幕外的圖像,然后這個圖像將會被緩存起來并繪制到實際圖層的 contents 和子圖層,對于有很多的子圖層或者有復雜的效果應用,這樣做就會比重繪所有事務的所有幀來更加高效。但是光柵化原始圖像需要時間,而且會消耗額外的內存。
光柵化也會帶來一定的性能損耗,是否要開啟就要根據實際的使用場景了,圖層內容頻繁變化時不建議使用。最好還是用 Instruments 比對開啟前后的 FPS 來看是否起到了優化效果。
我們來總結一下:
- 離屏渲染消耗性能的原因:
①.需要創建新的緩沖區;
②.離屏渲染的整個過程,需要多次切換上下文環境,先是從當前屏幕切換到離屏;等到離屏渲染結束以后,將離屏緩沖區的渲染結果顯示到屏幕上,又需要將上下文環境從離屏切換到當前屏幕;- 哪些操作會出發離屏渲染?
①.光柵化,layer.shouldRasterize = YES;
②.遮罩,layer.mask;
③.圓角,同時設置layer.maskToBounds = Yes,Layer.cornerRadis 大于
考慮通過CoreGraphics繪制裁剪圓角,或者美工提供圓角圖片;
④.陰影,layer.shadowXXX,如果設置了layer.shadowPath就不會產生離屏渲染;- 處理GPU的卡頓:
①.盡量減少視圖數量和層次。
②.GPU能處理的最大紋理尺寸是4096x4096,一旦超過這個尺寸,就會占用CPU資源進行處理,所以紋理盡量不要超過這個尺寸。
③.盡量避免段時間內大量圖片的顯示,盡可能將多張圖片合成一張圖片顯示。
④.減少透明的視圖(alpha<1),不透明的就設置opaque為yes。
⑤.盡量避免出現離屏渲染。
3. 卡頓檢測
平時所說的“卡頓”主要是因為在主線程執行了比較耗時的操作
可以添加Observer到主線程RunLoop中,通過監聽RunLoop狀態切換的耗時,以達到監控卡頓的目的
想了解更多iOS學習知識請聯系:QQ(814299221)