在聊這一話題之前稀拐,我們先看看屏幕是如何顯示圖像的赡鲜。
首先從過去的 CRT 顯示器原理說起吝镣。CRT 的電子槍按照上面方式睬塌,從上到下一行行掃描泉蝌,掃描完成后顯示器就呈現(xiàn)【一幀畫面】,隨后電子槍回到初始位置繼續(xù)下一次掃描揩晴。
為了把顯示器的顯示過程和系統(tǒng)的視頻控制器進行同步勋陪,顯示器(或者其他硬件)會用硬件時鐘產(chǎn)生一系列的定時信號。當電子槍換到新的一行硫兰,準備進行掃描時诅愚,顯示器會發(fā)出一個水平同步信號(horizonal synchronization),簡稱 HSync劫映;而當一幀畫面繪制完成后违孝,電子槍回復到原位壕曼,準備畫下一幀前,顯示器會發(fā)出一個垂直同步信號(vertical synchronization)等浊,簡稱 VSync。
顯示器通常以固定頻率進行刷新摹蘑,這個刷新率就是 VSync 信號產(chǎn)生的頻率筹燕。盡管現(xiàn)在的設備大都是液晶顯示屏了,但原理仍然沒有變衅鹿。
通常來說撒踪,計算機系統(tǒng)中 CPU、GPU大渤、顯示器是以上面這種方式協(xié)同工作的制妄。CPU 計算好顯示內(nèi)容提交到 GPU,GPU 渲染完成后將渲染結果放入幀緩沖區(qū)泵三,隨后視頻控制器會按照 VSync 信號逐行讀取幀緩沖區(qū)的數(shù)據(jù)耕捞,經(jīng)過可能的數(shù)模轉換傳遞給顯示器顯示。
在最簡單的情況下烫幕,幀緩沖區(qū)只有一個俺抽,這時幀緩沖區(qū)的讀取和刷新都都會有比較大的效率問題。為了解決效率問題较曼,顯示系統(tǒng)通常會引入兩個緩沖區(qū)磷斧,即雙緩沖機制。在這種情況下捷犹,GPU 會預先渲染好一幀放入一個緩沖區(qū)內(nèi)弛饭,讓視頻控制器讀取,當下一幀渲染好后萍歉,GPU 會直接把視頻控制器的指針指向第二個緩沖器侣颂。如此一來效率會有很大的提升。
雙緩沖雖然能解決效率問題枪孩,但會引入一個新的問題横蜒。當視頻控制器還未讀取完成時,即屏幕內(nèi)容剛顯示一半時销凑,GPU 將新的一幀內(nèi)容提交到幀緩沖區(qū)并把兩個緩沖區(qū)進行交換后丛晌,視頻控制器就會把新的一幀數(shù)據(jù)的下半段顯示到屏幕上,造成畫面撕裂現(xiàn)象斗幼。
為了解決這個問題澎蛛,GPU 通常有一個機制叫做垂直同步(簡寫也是 V-Sync),當開啟垂直同步后蜕窿,GPU 會等待顯示器的 VSync 信號發(fā)出后谋逻,才進行新的一幀渲染和緩沖區(qū)更新呆馁。這樣能解決畫面撕裂現(xiàn)象,也增加了畫面流暢度毁兆,但需要消費更多的計算資源浙滤,也會帶來部分延遲。
下圖就是IOS應用界面渲染到展示的流程:

Display 的上一層便是圖形處理單元 GPU气堕,GPU 是一個專門為圖形高并發(fā)計算而量身定做的處理單元纺腊。這也是為什么它能同時更新所有的像素,并呈現(xiàn)到顯示器上茎芭。它并發(fā)的本性讓它能高效的將不同紋理合成起來揖膜。我們將有一小塊內(nèi)容來更詳細的討論圖形合成。關鍵的是梅桩,GPU 是非常專業(yè)的壹粟,因此在某些工作上非常高效。比如宿百,GPU 非吵孟桑快,并且比 CPU 使用更少的電來完成工作垦页。通常 CPU 都有一個普遍的目的幸撕,它可以做很多不同的事情,但是合成圖像在 CPU 上卻顯得比較慢外臂。
卡頓產(chǎn)生的原因
由于垂直同步的機制坐儿,如果在一個 VSync 時間內(nèi),CPU 或者 GPU 沒有完成內(nèi)容提交宋光,則那一幀就會被丟棄貌矿,等待下一次機會再顯示,而這時顯示屏會保留之前的內(nèi)容不變罪佳。這就是界面卡頓的原因逛漫。
因此,我們需要平衡 CPU 和 GPU 的負荷避免一方超負荷運算赘艳。為了做到這一點酌毡,我們首先得了解 CPU 和 GPU 各自負責哪些內(nèi)容。
CPU和GPU的職責
在 iOS 系統(tǒng)中蕾管,圖像內(nèi)容展示到屏幕的過程需要 CPU 和 GPU 共同參與枷踏。
- CPU 負責計算顯示內(nèi)容,比如視圖的創(chuàng)建掰曾、布局計算旭蠕、圖片解碼、文本繪制等。
- 隨后 CPU 會將計算好的內(nèi)容提交到 GPU 去掏熬,由 GPU 進行變換佑稠、合成、渲染旗芬。
- 之后 GPU 會把渲染結果提交到幀緩沖區(qū)去舌胶,等待下一次 VSync 信號到來時顯示到屏幕上。
CPU 消耗型任務
布局計算
布局計算是 iOS 中最為常見的消耗 CPU 資源的地方疮丛,如果【視圖層級關系比較復雜】幔嫂,計算出所有圖層的布局信息就會消耗一部分時間。因此我們應該盡量提前計算好布局信息这刷,然后在合適的時機調(diào)整對應的屬性∶渚【還要避免不必要的更新】暇屋,只在真正發(fā)生了布局改變時再更新。
對象創(chuàng)建
對象創(chuàng)建過程伴隨著內(nèi)存分配洞辣、屬性設置咐刨、甚至還有讀取文件等操作,比較消耗 CPU 資源扬霜。盡量用輕量的對象代替重量的對象定鸟,可以對性能有所優(yōu)化。比如 CALayer 比 UIView 要輕量許多著瓶,如果視圖元素不需要響應觸摸事件联予,用 CALayer 會更加合適。
通過 Storyboard 創(chuàng)建視圖對象還會涉及到文件反序列化操作材原,其資源消耗會比直接通過代碼創(chuàng)建對象要大非常多沸久,在性能敏感的界面里,Storyboard 并不是一個好的技術選擇余蟹。
Autolayout
Autolayout 對于復雜視圖來說常常會產(chǎn)生嚴重的性能問題卷胯,【對于性能敏感的頁面建議還是使用手動布局的方式】,并控制好刷新頻率威酒,做到真正需要調(diào)整布局時再重新布局窑睁。
文本計算
如果一個界面中包含大量文本(比如微博、微信朋友圈等)葵孤,文本的寬高計算會占用很大一部分資源担钮,并且不可避免。
一個比較常見的場景是在 UITableView 中尤仍,heightForRowAtIndexPath
這個方法會被頻繁調(diào)用裳朋。這里的優(yōu)化就是盡量避免每次都重新進行文本的行高計算,緩存高度即可。如UITableView-FDTemplateLayoutCell
屏幕上能看到的所有文本內(nèi)容控件鲤嫡,包括 UIWebView送挑,在底層都是通過 CoreText 排版、繪制為 Bitmap 顯示的暖眼。常見的文本控件 惕耕,其排版和繪制都是在主線程進行的,當顯示大量文本時诫肠,CPU 的壓力會非常大司澎。
這一部分的性能優(yōu)化就需要我們放棄使用系統(tǒng)提供的上層控件轉而直接使用 CoreText 進行排版控制。
Wherever possible, try to avoid making changes to the frame of a view that contains text, because it will cause the text to be redrawn. For example, if you need to display a static block of text in the corner of a layer that frequently changes size, put the text in a sublayer instead.
圖像的繪制
圖像的繪制通常是指用那些以 CG 開頭的方法把圖像繪制到畫布中栋豫,然后從畫布創(chuàng)建圖片并顯示的過程挤安。前面的模塊圖里介紹了 CoreGraphic 是作用在 CPU 之上的,因此調(diào)用 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;
});
});
}
圖片的解碼
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.(圖片被加載后需要解碼丛肢,圖片的解碼是一個復雜耗時的過程围肥,并且需要占用比原始圖片還多的內(nèi)存資源)
【為什么需要解碼拦焚?】
把圖片從PNG或JPEG等格式中解壓出來钱骂,得到像素數(shù)據(jù)谨履。如果GPU不支持這種顏色各式邦蜜,CPU需要進行格式轉換勇吊。
比如應用中有一些從網(wǎng)絡下載的圖片扰柠,而GPU恰好不支持這個格式酷宵,這就需要CPU預先進行格式轉化或粮。SDwebImageDecoder就是這個作用幽歼。
【默認延遲解碼】
當你用 UIImage 或 CGImageSource 的那幾個方法創(chuàng)建圖片時腐芍,為了節(jié)省內(nèi)存,圖片數(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對象如果作為臨時變量被釋放了韭邓,則它下次仍然會解碼措近。【所以如果在tableview-cell女淑,即使是讀取本地(Bundle或者沙盒)路徑圖片瞭郑,仍然建議使用SDWebImage異步緩存讀取】
關于圖片解碼可以參考:
iOS中的imageIO與image解碼
[iOS]如何避免圖像解壓縮的時間開銷[還介紹了圖片的時間和空間消耗]
GPU消耗型任務
相對于 CPU 來說,GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點描述(三角形)鸭你,應用變換(transform)屈张、混合并渲染,然后輸出到屏幕上苇本。寬泛的說袜茧,【大多數(shù) CALayer 的屬性都是用 GPU 來繪制】菜拓。
以下一些操作會降低 GPU 繪制的性能瓣窄,
大量幾何結構
所有的 Bitmap,包括圖片纳鼎、文本俺夕、柵格化的內(nèi)容,最終都要由內(nèi)存提交到顯存贱鄙,綁定為 GPU Texture劝贸。不論是提交到顯存的過程,還是 GPU 調(diào)整和渲染 Texture 的過程逗宁,都要消耗不少 GPU 資源映九。當在較短時間顯示大量圖片時(比如 TableView 存在非常多的圖片并且快速滑動時),CPU 占用率很低瞎颗,GPU 占用非常高件甥,界面仍然會掉幀。
避免這種情況的方法只能是盡量減少在短時間內(nèi)大量圖片的顯示哼拔,盡可能將多張圖片合成為一張進行顯示引有。
另外當圖片過大,超過 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ù)量和層次拍顷,并且減少不必要的透明視圖】
離屏渲染
離屏渲染是指圖層在被顯示之前,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需要更多的渲染通道志珍,而且不同的渲染通道間切換需要耗費一定的時間橙垢,這個時間內(nèi)GPU會閑置,當通道達到一定數(shù)量伦糯,對性能也會有較大的影響柜某;
【為什么會產(chǎn)生離屏渲染?】
首先敛纲,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 和子圖層呢蛤,對于有很多的子圖層或者有復雜的效果應用惶傻,這樣做就會比重繪所有事務的所有幀來更加高效。但是光柵化原始圖像需要時間其障,而且會消耗額外的內(nèi)存银室。
光柵化也會帶來一定的性能損耗,是否要開啟就要根據(jù)實際的使用場景了励翼,圖層內(nèi)容頻繁變化時不建議使用蜈敢。最好還是用 Instruments 比對開啟前后的 FPS 來看是否起到了優(yōu)化效果。
離屏渲染請參考:iOS 離屏渲染的研究
參考資料:
1.iOS進階之頁面性能優(yōu)化
2.繪制像素到屏幕上
3.iOS 保持界面流暢的技巧
4.如何正確地寫好一個界面