性能優(yōu)化是一個很重要的一部分,我們首先看CPU和GPU的部分,想知道CPU和GPU是怎么優(yōu)化的,就必須要明白CPU和GPU的原理.
1. 屏幕的成像原理
首先從過去的 CRT 顯示器原理說起瓢棒。CRT 的電子槍按照上面方式帆吻,從上到下一行行掃描窟却,掃描完成后顯示器就呈現(xiàn)(一幀畫面)熊楼,隨后電子槍回到初始位置繼續(xù)下一次掃描。
為了把顯示器的顯示過程和系統(tǒng)的視頻控制器進行同步叉瘩,顯示器(或者其他硬件)會用硬件時鐘產生一系列的定時信號教硫。當電子槍換到新的一行,準備進行掃描時宋列,顯示器會發(fā)出一個水平同步信號(horizonal synchronization)昭抒,簡稱 HSync;而當一幀畫面繪制完成后,電子槍回復到原位灭返,準備畫下一幀前盗迟,顯示器會發(fā)出一個垂直同步信號(vertical synchronization),簡稱 VSync熙含。
顯示器通常以固定頻率進行刷新罚缕,這個刷新率就是 VSync 信號產生的頻率。盡管現(xiàn)在的設備大都是液晶顯示屏了怎静,但原理仍然沒有變邮弹。
通常來說,計算機系統(tǒng)中 CPU蚓聘、GPU腌乡、顯示器是以上面這種方式協(xié)同工作的。CPU 計算好顯示內容提交到 GPU夜牡,GPU 渲染完成后將渲染結果放入幀緩沖區(qū)导饲,隨后視頻控制器會按照 VSync 信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉換傳遞給顯示器顯示氯材。
在最簡單的情況下渣锦,幀緩沖區(qū)只有一個,這時幀緩沖區(qū)的讀取和刷新都都會有比較大的效率問題氢哮。為了解決效率問題袋毙,顯示系統(tǒng)通常會引入兩個緩沖區(qū),即雙緩沖機制冗尤。在這種情況下听盖,GPU 會預先渲染好一幀放入一個緩沖區(qū)內,讓視頻控制器讀取裂七,當下一幀渲染好后皆看,GPU 會直接把視頻控制器的指針指向第二個緩沖器。如此一來效率會有很大的提升背零。
雙緩沖雖然能解決效率問題腰吟,但會引入一個新的問題。當視頻控制器還未讀取完成時徙瓶,即屏幕內容剛顯示一半時毛雇,GPU 將新的一幀內容提交到幀緩沖區(qū)并把兩個緩沖區(qū)進行交換后,視頻控制器就會把新的一幀數(shù)據(jù)的下半段顯示到屏幕上侦镇,造成畫面撕裂現(xiàn)象灵疮。
為了解決這個問題,GPU 通常有一個機制叫做垂直同步(簡寫也是 V-Sync)壳繁,當開啟垂直同步后震捣,GPU 會等待顯示器的 VSync 信號發(fā)出后荔棉,才進行新的一幀渲染和緩沖區(qū)更新。這樣能解決畫面撕裂現(xiàn)象蒿赢,也增加了畫面流暢度江耀,但需要消費更多的計算資源,也會帶來部分延遲诉植。
下圖就是iOS應用界面渲染到展示的流程:
Display 的上一層便是圖形處理單元 GPU祥国,GPU 是一個專門為圖形高并發(fā)計算而量身定做的處理單元。這也是為什么它能同時更新所有的像素晾腔,并呈現(xiàn)到顯示器上舌稀。它并發(fā)的本性讓它能高效的將不同紋理合成起來。我們將有一小塊內容來更詳細的討論圖形合成灼擂。關鍵的是壁查,GPU 是非常專業(yè)的,因此在某些工作上非常高效剔应。比如睡腿,GPU 非常快峻贮,并且比 CPU 使用更少的電來完成工作席怪。通常 CPU 都有一個普遍的目的,它可以做很多不同的事情纤控,但是合成圖像在 CPU 上卻顯得比較慢挂捻。
一. 卡頓產生的原因
由于垂直同步的機制,如果在一個 VSync 時間內船万,CPU 或者 GPU 沒有完成內容提交刻撒,則那一幀就會被丟棄,等待下一次機會再顯示耿导,而這時顯示屏會保留之前的內容不變声怔。這就是界面卡頓的原因。
CPU(紅色)——>GPU(藍色)
1.CPU完成計算舱呻,提交給GPU渲染醋火,這是來個垂直同步信號,則會將渲染的內容顯示到屏幕上狮荔。
2.CPU計算時間正常胎撇,CPU渲染時間短介粘,等待VSync
3.CPU計算時間正持呈希或慢,GPU渲染時間長姻采,這時來了VSync雅采,而這一幀還沒有渲染完,那么就會出現(xiàn)掉幀現(xiàn)象,屏幕回去顯示上一幀的畫面婚瓜。這樣就產生了卡頓宝鼓。
4.而當下一幀VSync出現(xiàn)時,丟掉的那一幀畫面才會出現(xiàn)巴刻。
因此愚铡,我們需要平衡 CPU 和 GPU 的負荷避免一方超負荷運算。為了做到這一點胡陪,我們首先得了解 CPU 和 GPU 各自負責哪些內容沥寥。
二. CPU和GPU的職責
在 iOS 系統(tǒng)中,圖像內容展示到屏幕的過程需要 CPU 和 GPU 共同參與柠座。
- CPU 負責計算顯示內容邑雅,比如視圖的創(chuàng)建、布局計算淮野、圖片解碼吹泡、文本繪制等。
- 隨后 CPU 會將計算好的內容提交到 GPU 去爆哑,由 GPU 進行變換妈踊、合成泪漂、渲染。
- 之后 GPU 會把渲染結果提交到幀緩沖區(qū)去萝勤,等待下一次 VSync 信號到來時顯示到屏幕上露筒。
A. CPU卡頓分析
a. 布局計算
視圖布局的計算是 App 中最為常見的消耗 CPU 資源的地方。如果能在后臺線程提前計算好視圖布局敌卓、并且對視圖布局進行緩存趟径,那么這個地方基本就不會產生性能問題了。
不論通過何種技術對視圖進行布局掌眠,其最終都會落到對 UIView.frame/bounds/center 等屬性的調整上幕屹。上面也說過级遭,對這些屬性的調整非常消耗資源挫鸽,所以盡量提前計算好布局鸥跟,在需要時一次性調整好對應屬性,而不要多次蚂夕、頻繁的計算和調整這些屬性婿牍。
b. 對象創(chuàng)建
對象創(chuàng)建過程伴隨著內存分配惩歉、屬性設置、甚至還有讀取文件等操作上遥,比較消耗 CPU 資源粉楚。盡量用輕量的對象代替重量的對象亮垫,可以對性能有所優(yōu)化饮潦。比如 CALayer 比 UIView 要輕量許多,如果視圖元素不需要響應觸摸事件回俐,用 CALayer 會更加合適稀并。
通過 Storyboard 創(chuàng)建視圖對象還會涉及到文件反序列化操作碘举,其資源消耗會比直接通過代碼創(chuàng)建對象要大非常多,在性能敏感的界面里政冻,Storyboard 并不是一個好的技術選擇线欲。
c. Autolayout
Autolayout 是蘋果本身提倡的技術李丰,在大部分情況下也能很好的提升開發(fā)效率,但是 Autolayout 對于復雜視圖來說常常會產生嚴重的性能問題舟舒。隨著視圖數(shù)量的增長秃励,Autolayout 帶來的 CPU 消耗會呈指數(shù)級上升吉捶。具體數(shù)據(jù)可以看這個文章:http://pilky.me/36/呐舔。 如果你不想手動調整 frame 等屬性,你可以用一些工具方法替代(比如常見的 left/right/top/bottom/width/height 快捷屬性)食呻,或者使用 ComponentKit澎现、AsyncDisplayKit 等框架剑辫。
d. 文本計算
如果一個界面中包含大量文本(比如微博、微信朋友圈等)莱革,文本的寬高計算會占用很大一部分資源盅视,并且不可避免旦万。
一個比較常見的場景是在 UITableView 中成艘,heightForRowAtIndexPath
這個方法會被頻繁調用贺归。這里的優(yōu)化就是盡量避免每次都重新進行文本的行高計算拂酣,緩存高度即可仲义。
e. 文本渲染
屏幕上能看到的所有文本內容控件埃撵,包括 UIWebView暂刘,在底層都是通過 CoreText 排版、繪制為 Bitmap 顯示的募寨。常見的文本控件 (UILabel芝发、UITextView 等)辅鲸,其排版和繪制都是在主線程進行的,當顯示大量文本時例书,CPU 的壓力會非常大刻炒。對此解決方案只有一個,那就是自定義文本控件树瞭,用 TextKit 或最底層的 CoreText 對文本異步繪制晒喷。盡管這實現(xiàn)起來非常麻煩访敌,但其帶來的優(yōu)勢也非常大,CoreText 對象創(chuàng)建好后爷抓,能直接獲取文本的寬高等信息蓝撇,避免了多次計算(調整 UILabel 大小時算一遍、UILabel 繪制時內部再算一遍)据悔;CoreText 對象占用內存較少,可以緩存下來以備稍后多次渲染群嗤。
f.圖像的繪制
圖像的繪制通常是指用那些以 CG 開頭的方法把圖像繪制到畫布中兵琳,然后從畫布創(chuàng)建圖片并顯示的過程躯肌。前面的模塊圖里介紹了 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等格式中解壓出來,得到像素數(shù)據(jù)黔宛。如果GPU不支持這種顏色各式臀晃,CPU需要進行格式轉換。
比如應用中有一些從網(wǎng)絡下載的圖片案淋,而GPU恰好不支持這個格式踢京,這就需要CPU預先進行格式轉化宦棺。SDwebImageDecoder就是這個作用代咸。
②.默認延遲解碼
當你用 UIImage 或 CGImageSource 的那幾個方法創(chuàng)建圖片時,為了節(jié)省內存逻杖,圖片數(shù)據(jù)并不會立刻解碼荸百。圖片設置到 UIImageView 或者 CALayer.contents 中去滨攻,并且 CALayer 被提交到 GPU 前铡买,CGImage 中的數(shù)據(jù)才會得到解碼奇钞。這一步是發(fā)生在主線程的,并且不可避免媒至。
如果想要繞開這個機制拒啰,可以使用 ImageIO (怎么使用完慧?)或者提前在后臺線程先把圖片繪制到 CGBitmapContext 中,然后從 Bitmap 直接創(chuàng)建圖片拴孤。目前常見的網(wǎng)絡圖片庫都自帶這個功能甲捏。
③.不一定是默認延遲解碼
常用的 UIImage 加載方法有 imageNamed和 imageWithContentsOfFile司顿。其中 imageNamed加載圖片后會馬上解碼大溜,并且系統(tǒng)會將解碼后的圖片緩存起來,但是這個緩存策略是不公開的获三,我們無法知道圖片什么時候會被釋放。因此在一些性能敏感的頁面伞租,我們還可以用 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锣险;能用基本數(shù)據(jù)類型,就別用NSNumber類型夯接。
2.不要頻繁地跳用UIVIew的相關屬性盔几,比如frame逊拍、bounds际邻、transform等屬性世曾,盡量減少不必要的修改。
3.盡量提前計算好布局骗露,在有需要時一次性調整對應的布局萧锉,不要多次修改屬性柿隙。
4.Autolayout會比直接設置frame消耗更多的CPU資源鲫凶。
5.圖片的size最好剛好跟UIImageView的size保持一致螟炫。
6.控制一下線程的最大并發(fā)數(shù)量不恭。
7.盡量把耗時的操作放到子線程。
8.文本處理(尺寸的計算折晦,繪制)满着。
9.圖片處理(解碼风喇、繪制)。
B. GPU卡頓分析
1.On-SCreen Rendering:當前屏幕渲染还蹲,在當前用語顯示的屏幕緩沖區(qū)進行渲染操作谜喊。
2.Off-Screen Rendring: 離屏渲染斗遏,在當前屏幕緩沖區(qū)以外新開辟一個緩沖區(qū)進行渲染操作鞋邑。
相對于 CPU 來說枚碗,GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點描述(三角形)视译,應用變換(transform)、混合并渲染,然后輸出到屏幕上椅亚。寬泛的說呀舔,(大多數(shù) 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值)芽淡。如果某一塊區(qū)域上覆蓋了多個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 消耗宣蔚,(應用應當盡量減少視圖數(shù)量和層次向抢,并且減少不必要的透明視圖)
b.離屏渲染
離屏渲染是指圖層在被顯示之前,GPU在當前屏幕緩沖區(qū)以外新開辟一個緩沖區(qū)進行渲染操作胚委。離屏渲染耗時是發(fā)生在離屏這個動作上面挟鸠,而不是渲染。為什么離屏這么耗時亩冬?原因主要有創(chuàng)建緩沖區(qū)和上下文切換艘希。創(chuàng)建新的緩沖區(qū)代價都不算大,付出最大代價的是上下文切換硅急。
①. 上下文切換
不管是在GPU渲染過程中覆享,還是一直所熟悉的進程切換,上下文切換在哪里都是一個相當耗時的操作营袜。首先我要保存當前屏幕渲染環(huán)境撒顿,然后切換到一個新的繪制環(huán)境,申請繪制資源荚板,初始化環(huán)境凤壁,然后開始一個繪制,繪制完畢后銷毀這個繪制環(huán)境啸驯,如需要切換到On-Screen Rendering或者再開始一個新的離屏渲染重復之前的操作客扎。
②. 渲染流程
我們先看看最基本的渲染通道流程:
我們再來看看需要Offscreen Render的渲染通道流程:
一般情況下,OpenGL會將應用提交到Render Server的動畫直接渲染顯示(基本的Tile-Based渲染流程)罚斗,但對于一些復雜的圖像動畫的渲染并不能直接渲染疊加顯示徙鱼,而是需要根據(jù)Command Buffer分通道進行渲染之后再組合,這一組合過程中针姿,就有些渲染通道是不會直接顯示的塌鸯;對比基本渲染通道流程和Masking渲染通道流程圖,我們可以看到到Masking渲染需要更多渲染通道和合并的步驟兜粘;而這些沒有直接顯示在屏幕的上的通道(如上圖的 Pass 1 和 Pass 2)就是Offscreen Rendering Pass拦耐。
Offscreen Render為什么卡頓,從上圖我們就可以知道榕暇,Offscreen Render需要更多的渲染通道蓬衡,而且不同的渲染通道間切換需要耗費一定的時間,這個時間內GPU會閑置彤枢,當通道達到一定數(shù)量狰晚,對性能也會有較大的影響;
為什么會產生離屏渲染缴啡?
首先壁晒,OpenGL提交一個命令到Command Buffer,隨后GPU開始渲染业栅,渲染結果放到Render Buffer中秒咐,這是正常的渲染流程谬晕。【但是有一些復雜的效果無法直接渲染出結果携取,它需要分步渲染最后再組合起來】攒钳,比如添加一個蒙版(mask)。
會造成 offscreen rendering 的原因有:
陰影(UIView.layer.shadowOffset/shadowRadius/…)
圓角(當 UIView.layer.cornerRadius 和 UIView.layer.maskToBounds 一起使用時)
圖層蒙板(Mask)
開啟光柵化(shouldRasterize = true歹茶,同時設置 rasterizationScale)
③. Mask
一個圖層可以有一個和它相關聯(lián)的 mask(蒙板)夕玩,mask 是一個擁有 alpha 值的(位圖)(不是矢量圖,所以矢量圖是不能作為遮罩)惊豺。只有在 mask 中顯示出來的(即圖層中的部分)才會被渲染出來燎孟。
使用陰影時同時設置 shadowPath 就能避免離屏渲染大大提升性能,圓角觸發(fā)的離屏渲染可以用 CoreGraphics 將圖片處理成圓角來避免尸昧。
CALayer 有一個 shouldRasterize 屬性揩页,將這個屬性設置成 true 后就開啟了光柵化。
④. 光柵化
光柵化其實是一種將幾何圖元變?yōu)槎S圖像的過程烹俗。
你模型的那些頂點在經(jīng)過各種矩陣變換后也僅僅是頂點爆侣。而由頂點構成的圖形要在屏幕上顯示出來,除了需要頂點的信息以外幢妄,還需要確定構成這個圖形的所有像素的信息兔仰。
光柵化優(yōu)缺點
開啟光柵化后會將圖層繪制到一個屏幕外的圖像,然后這個圖像將會被緩存起來并繪制到實際圖層的 contents 和子圖層蕉鸳,對于有很多的子圖層或者有復雜的效果應用乎赴,這樣做就會比重繪所有事務的所有幀來更加高效。但是光柵化原始圖像需要時間潮尝,而且會消耗額外的內存榕吼。
光柵化也會帶來一定的性能損耗,是否要開啟就要根據(jù)實際的使用場景了勉失,圖層內容頻繁變化時不建議使用羹蚣。最好還是用 Instruments 比對開啟前后的 FPS 來看是否起到了優(yōu)化效果。
我們來總結一下:
- 離屏渲染消耗性能的原因:
①.需要創(chuàng)建新的緩沖區(qū)乱凿;
②.離屏渲染的整個過程顽素,需要多次切換上下文環(huán)境,先是從當前屏幕切換到離屏徒蟆;等到離屏渲染結束以后戈抄,將離屏緩沖區(qū)的渲染結果顯示到屏幕上,又需要將上下文環(huán)境從離屏切換到當前屏幕;- 哪些操作會出發(fā)離屏渲染后专?
①.光柵化,layer.shouldRasterize = YES;
②.遮罩输莺,layer.mask;
③.圓角戚哎,同時設置layer.maskToBounds = Yes裸诽,Layer.cornerRadis 大于
考慮通過CoreGraphics繪制裁剪圓角,或者美工提供圓角圖片;
④.陰影型凳,layer.shadowXXX,如果設置了layer.shadowPath就不會產生離屏渲染;- 處理GPU的卡頓:
①.盡量減少視圖數(shù)量和層次丈冬。
②.GPU能處理的最大紋理尺寸是4096x4096,一旦超過這個尺寸甘畅,就會占用CPU資源進行處理埂蕊,所以紋理盡量不要超過這個尺寸。
③.盡量避免段時間內大量圖片的顯示疏唾,盡可能將多張圖片合成一張圖片顯示蓄氧。
④.減少透明的視圖(alpha<1),不透明的就設置opaque為yes槐脏。
⑤.盡量避免出現(xiàn)離屏渲染喉童。
3. 卡頓檢測
平時所說的“卡頓”主要是因為在主線程執(zhí)行了比較耗時的操作
可以添加Observer到主線程RunLoop中,通過監(jiān)聽RunLoop狀態(tài)切換的耗時顿天,以達到監(jiān)控卡頓的目的
想了解更多iOS學習知識請聯(lián)系:QQ(814299221)