CPU&GPU的圖像故事

在聊這一話題之前,我們先看看屏幕是如何顯示圖像的。

屏幕顯示圖像的原理


首先從過(guò)去的 CRT 顯示器原理說(shuō)起。CRT 的電子槍按照上面方式,從上到下一行行掃描,掃描完成后顯示器就呈現(xiàn)【一幀畫(huà)面】,隨后電子槍回到初始位置繼續(xù)下一次掃描。
為了把顯示器的顯示過(guò)程和系統(tǒng)的視頻控制器進(jìn)行同步,顯示器(或者其他硬件)會(huì)用硬件時(shí)鐘產(chǎn)生一系列的定時(shí)信號(hào)。當(dāng)電子槍換到新的一行,準(zhǔn)備進(jìn)行掃描時(shí),顯示器會(huì)發(fā)出一個(gè)水平同步信號(hào)(horizonal synchronization),簡(jiǎn)稱(chēng) HSync;而當(dāng)一幀畫(huà)面繪制完成后,電子槍回復(fù)到原位,準(zhǔn)備畫(huà)下一幀前,顯示器會(huì)發(fā)出一個(gè)垂直同步信號(hào)(vertical synchronization),簡(jiǎn)稱(chēng) VSync。
顯示器通常以固定頻率進(jìn)行刷新,這個(gè)刷新率就是 VSync 信號(hào)產(chǎn)生的頻率。盡管現(xiàn)在的設(shè)備大都是液晶顯示屏了,但原理仍然沒(méi)有變。


通常來(lái)說(shuō),計(jì)算機(jī)系統(tǒng)中 CPU、GPU、顯示器是以上面這種方式協(xié)同工作的。CPU 計(jì)算好顯示內(nèi)容提交到 GPU,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū),隨后視頻控制器會(huì)按照 VSync 信號(hào)逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過(guò)可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示。

在最簡(jiǎn)單的情況下,幀緩沖區(qū)只有一個(gè),這時(shí)幀緩沖區(qū)的讀取和刷新都都會(huì)有比較大的效率問(wèn)題。為了解決效率問(wèn)題,顯示系統(tǒng)通常會(huì)引入兩個(gè)緩沖區(qū),即雙緩沖機(jī)制。在這種情況下,GPU 會(huì)預(yù)先渲染好一幀放入一個(gè)緩沖區(qū)內(nèi),讓視頻控制器讀取,當(dāng)下一幀渲染好后,GPU 會(huì)直接把視頻控制器的指針指向第二個(gè)緩沖器。如此一來(lái)效率會(huì)有很大的提升。
雙緩沖雖然能解決效率問(wèn)題,但會(huì)引入一個(gè)新的問(wèn)題。當(dāng)視頻控制器還未讀取完成時(shí),即屏幕內(nèi)容剛顯示一半時(shí),GPU 將新的一幀內(nèi)容提交到幀緩沖區(qū)并把兩個(gè)緩沖區(qū)進(jìn)行交換后,視頻控制器就會(huì)把新的一幀數(shù)據(jù)的下半段顯示到屏幕上,造成畫(huà)面撕裂現(xiàn)象。
為了解決這個(gè)問(wèn)題,GPU 通常有一個(gè)機(jī)制叫做垂直同步(簡(jiǎn)寫(xiě)也是 V-Sync),當(dāng)開(kāi)啟垂直同步后,GPU 會(huì)等待顯示器的 VSync 信號(hào)發(fā)出后,才進(jìn)行新的一幀渲染和緩沖區(qū)更新。這樣能解決畫(huà)面撕裂現(xiàn)象,也增加了畫(huà)面流暢度,但需要消費(fèi)更多的計(jì)算資源,也會(huì)帶來(lái)部分延遲。

下圖就是IOS應(yīng)用界面渲染到展示的流程:



Display 的上一層便是圖形處理單元 GPU,GPU 是一個(gè)專(zhuān)門(mén)為圖形高并發(fā)計(jì)算而量身定做的處理單元。這也是為什么它能同時(shí)更新所有的像素,并呈現(xiàn)到顯示器上。它并發(fā)的本性讓它能高效的將不同紋理合成起來(lái)。我們將有一小塊內(nèi)容來(lái)更詳細(xì)的討論圖形合成。關(guān)鍵的是,GPU 是非常專(zhuān)業(yè)的,因此在某些工作上非常高效。比如,GPU 非???,并且比 CPU 使用更少的電來(lái)完成工作。通常 CPU 都有一個(gè)普遍的目的,它可以做很多不同的事情,但是合成圖像在 CPU 上卻顯得比較慢。

卡頓產(chǎn)生的原因
  由于垂直同步的機(jī)制,如果在一個(gè) VSync 時(shí)間內(nèi),CPU 或者 GPU 沒(méi)有完成內(nèi)容提交,則那一幀就會(huì)被丟棄,等待下一次機(jī)會(huì)再顯示,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變。這就是界面卡頓的原因。
  因此,我們需要平衡 CPU 和 GPU 的負(fù)荷避免一方超負(fù)荷運(yùn)算。為了做到這一點(diǎn),我們首先得了解 CPU 和 GPU 各自負(fù)責(zé)哪些內(nèi)容。
 

垂直同步.jpg

CPU和GPU的職責(zé)


  在 iOS 系統(tǒng)中,圖像內(nèi)容展示到屏幕的過(guò)程需要 CPU 和 GPU 共同參與。

  1. CPU 負(fù)責(zé)計(jì)算顯示內(nèi)容,比如視圖的創(chuàng)建、布局計(jì)算、圖片解碼、文本繪制等。
  2. 隨后 CPU 會(huì)將計(jì)算好的內(nèi)容提交到 GPU 去,由 GPU 進(jìn)行變換、合成、渲染。
  3. 之后 GPU 會(huì)把渲染結(jié)果提交到幀緩沖區(qū)去,等待下一次 VSync 信號(hào)到來(lái)時(shí)顯示到屏幕上。

CPU 消耗型任務(wù)
布局計(jì)算
布局計(jì)算是 iOS 中最為常見(jiàn)的消耗 CPU 資源的地方,如果【視圖層級(jí)關(guān)系比較復(fù)雜】,計(jì)算出所有圖層的布局信息就會(huì)消耗一部分時(shí)間。因此我們應(yīng)該盡量提前計(jì)算好布局信息,然后在合適的時(shí)機(jī)調(diào)整對(duì)應(yīng)的屬性。【還要避免不必要的更新】,只在真正發(fā)生了布局改變時(shí)再更新。

對(duì)象創(chuàng)建
對(duì)象創(chuàng)建過(guò)程伴隨著內(nèi)存分配、屬性設(shè)置、甚至還有讀取文件等操作,比較消耗 CPU 資源。盡量用輕量的對(duì)象代替重量的對(duì)象,可以對(duì)性能有所優(yōu)化。比如 CALayer 比 UIView 要輕量許多,如果視圖元素不需要響應(yīng)觸摸事件,用 CALayer 會(huì)更加合適。
通過(guò) Storyboard 創(chuàng)建視圖對(duì)象還會(huì)涉及到文件反序列化操作,其資源消耗會(huì)比直接通過(guò)代碼創(chuàng)建對(duì)象要大非常多,在性能敏感的界面里,Storyboard 并不是一個(gè)好的技術(shù)選擇。

Autolayout
Autolayout 對(duì)于復(fù)雜視圖來(lái)說(shuō)常常會(huì)產(chǎn)生嚴(yán)重的性能問(wèn)題,【對(duì)于性能敏感的頁(yè)面建議還是使用手動(dòng)布局的方式】,并控制好刷新頻率,做到真正需要調(diào)整布局時(shí)再重新布局。

文本計(jì)算
如果一個(gè)界面中包含大量文本(比如微博、微信朋友圈等),文本的寬高計(jì)算會(huì)占用很大一部分資源,并且不可避免。
一個(gè)比較常見(jiàn)的場(chǎng)景是在 UITableView 中,heightForRowAtIndexPath
這個(gè)方法會(huì)被頻繁調(diào)用。這里的優(yōu)化就是盡量避免每次都重新進(jìn)行文本的行高計(jì)算,緩存高度即可。如UITableView-FDTemplateLayoutCell

文本渲染


屏幕上能看到的所有文本內(nèi)容控件,包括 UIWebView,在底層都是通過(guò) CoreText 排版、繪制為 Bitmap 顯示的。常見(jiàn)的文本控件 ,其排版和繪制都是在主線程進(jìn)行的,當(dāng)顯示大量文本時(shí),CPU 的壓力會(huì)非常大。
這一部分的性能優(yōu)化就需要我們放棄使用系統(tǒng)提供的上層控件轉(zhuǎn)而直接使用 CoreText 進(jìn)行排版控制。

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 開(kāi)頭的方法把圖像繪制到畫(huà)布中,然后從畫(huà)布創(chuàng)建圖片并顯示的過(guò)程。前面的模塊圖里介紹了 CoreGraphic 是作用在 CPU 之上的,因此調(diào)用 CG 開(kāi)頭的方法消耗的是 CPU 資源。我們可以將繪制過(guò)程放到后臺(tái)線程,然后在主線程里將結(jié)果設(shè)置到 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.(圖片被加載后需要解碼,圖片的解碼是一個(gè)復(fù)雜耗時(shí)的過(guò)程,并且需要占用比原始圖片還多的內(nèi)存資源)

【為什么需要解碼?】
把圖片從PNG或JPEG等格式中解壓出來(lái),得到像素?cái)?shù)據(jù)。如果GPU不支持這種顏色各式,CPU需要進(jìn)行格式轉(zhuǎn)換。
比如應(yīng)用中有一些從網(wǎng)絡(luò)下載的圖片,而GPU恰好不支持這個(gè)格式,這就需要CPU預(yù)先進(jìn)行格式轉(zhuǎn)化。SDwebImageDecoder就是這個(gè)作用。

【默認(rèn)延遲解碼】
當(dāng)你用 UIImage 或 CGImageSource 的那幾個(gè)方法創(chuàng)建圖片時(shí),為了節(jié)省內(nèi)存,圖片數(shù)據(jù)并不會(huì)立刻解碼。圖片設(shè)置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的數(shù)據(jù)才會(huì)得到解碼。這一步是發(fā)生在主線程的,并且不可避免。
如果想要繞開(kāi)這個(gè)機(jī)制,可以使用 ImageIO 【怎么使用?】或者提前在后臺(tái)線程先把圖片繪制到 CGBitmapContext 中,然后從 Bitmap 直接創(chuàng)建圖片。目前常見(jiàn)的網(wǎng)絡(luò)圖片庫(kù)都自帶這個(gè)功能。

【不一定是默認(rèn)延遲解碼】
常用的 UIImage 加載方法有 imageNamed和 imageWithContentsOfFile。其中 imageNamed加載圖片后會(huì)馬上解碼,并且系統(tǒng)會(huì)將解碼后的圖片緩存起來(lái),但是這個(gè)緩存策略是不公開(kāi)的,我們無(wú)法知道圖片什么時(shí)候會(huì)被釋放。因此在一些性能敏感的頁(yè)面,我們還可以用 static 變量 hold 住 imageNamed加載到的圖片避免被釋放掉,以空間換時(shí)間的方式來(lái)提高性能。
imageWithContentsOfFile解碼后的UIImage對(duì)象如果作為臨時(shí)變量被釋放了,則它下次仍然會(huì)解碼?!舅匀绻趖ableview-cell,即使是讀取本地(Bundle或者沙盒)路徑圖片,仍然建議使用SDWebImage異步緩存讀取】
關(guān)于圖片解碼可以參考:
iOS中的imageIO與image解碼
[iOS]如何避免圖像解壓縮的時(shí)間開(kāi)銷(xiāo)[還介紹了圖片的時(shí)間和空間消耗]

GPU消耗型任務(wù)
相對(duì)于 CPU 來(lái)說(shuō),GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點(diǎn)描述(三角形),應(yīng)用變換(transform)、混合并渲染,然后輸出到屏幕上。寬泛的說(shuō),【大多數(shù) CALayer 的屬性都是用 GPU 來(lái)繪制】。

以下一些操作會(huì)降低 GPU 繪制的性能,
大量幾何結(jié)構(gòu)
所有的 Bitmap,包括圖片、文本、柵格化的內(nèi)容,最終都要由內(nèi)存提交到顯存,綁定為 GPU Texture。不論是提交到顯存的過(guò)程,還是 GPU 調(diào)整和渲染 Texture 的過(guò)程,都要消耗不少 GPU 資源。當(dāng)在較短時(shí)間顯示大量圖片時(shí)(比如 TableView 存在非常多的圖片并且快速滑動(dòng)時(shí)),CPU 占用率很低,GPU 占用非常高,界面仍然會(huì)掉幀。
避免這種情況的方法只能是盡量減少在短時(shí)間內(nèi)大量圖片的顯示,盡可能將多張圖片合成為一張進(jìn)行顯示。
另外當(dāng)圖片過(guò)大,超過(guò) GPU 的最大紋理尺寸時(shí),圖片需要先由 CPU 進(jìn)行預(yù)處理,這對(duì) CPU 和 GPU 都會(huì)帶來(lái)額外的資源消耗。目前來(lái)說(shuō),iPhone 4S 以上機(jī)型,【紋理尺寸上限都是 4096x4096】,更詳細(xì)的資料可以看這里:iosres.com。所以,盡量不要讓圖片和視圖的大小超過(guò)這個(gè)值。

視圖以及圖層的混合
屏幕上每一個(gè)點(diǎn)都是一個(gè)像素,像素有R、G、B三種顏色構(gòu)成(有時(shí)候還帶有alpha值)。如果某一塊區(qū)域上覆蓋了多個(gè)layer,最后的顯示效果受到這些layer的共同影響。舉個(gè)例子,上層是藍(lán)色(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

當(dāng)多個(gè)視圖(或者說(shuō) CALayer)重疊在一起顯示時(shí),GPU 會(huì)首先把他們混合到一起。如果視圖結(jié)構(gòu)過(guò)于復(fù)雜,混合的過(guò)程也會(huì)消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,【應(yīng)用應(yīng)當(dāng)盡量減少視圖數(shù)量和層次,并且減少不必要的透明視圖】

離屏渲染
離屏渲染是指圖層在被顯示之前,GPU在當(dāng)前屏幕緩沖區(qū)以外新開(kāi)辟一個(gè)緩沖區(qū)進(jìn)行渲染操作。離屏渲染耗時(shí)是發(fā)生在離屏這個(gè)動(dòng)作上面,而不是渲染。為什么離屏這么耗時(shí)?原因主要有創(chuàng)建緩沖區(qū)和上下文切換。創(chuàng)建新的緩沖區(qū)代價(jià)都不算大,付出最大代價(jià)的是上下文切換。

【上下文切換】
不管是在GPU渲染過(guò)程中,還是一直所熟悉的進(jìn)程切換,上下文切換在哪里都是一個(gè)相當(dāng)耗時(shí)的操作。首先我要保存當(dāng)前屏幕渲染環(huán)境,然后切換到一個(gè)新的繪制環(huán)境,申請(qǐng)繪制資源,初始化環(huán)境,然后開(kāi)始一個(gè)繪制,繪制完畢后銷(xiāo)毀這個(gè)繪制環(huán)境,如需要切換到On-Screen Rendering或者再開(kāi)始一個(gè)新的離屏渲染重復(fù)之前的操作。

【渲染流程】
我們先看看最基本的渲染通道流程:


iOS UI Arch

我們?cè)賮?lái)看看需要Offscreen Render的渲染通道流程:


一般情況下,OpenGL會(huì)將應(yīng)用提交到Render Server的動(dòng)畫(huà)直接渲染顯示(基本的Tile-Based渲染流程),但對(duì)于一些復(fù)雜的圖像動(dòng)畫(huà)的渲染并不能直接渲染疊加顯示,而是需要根據(jù)Command Buffer分通道進(jìn)行渲染之后再組合,這一組合過(guò)程中,就有些渲染通道是不會(huì)直接顯示的;對(duì)比基本渲染通道流程和Masking渲染通道流程圖,我們可以看到到Masking渲染需要更多渲染通道和合并的步驟;而這些沒(méi)有直接顯示在屏幕的上的通道(如上圖的 Pass 1 和 Pass 2)就是Offscreen Rendering Pass。
Offscreen Render為什么卡頓,從上圖我們就可以知道,Offscreen Render需要更多的渲染通道,而且不同的渲染通道間切換需要耗費(fèi)一定的時(shí)間,這個(gè)時(shí)間內(nèi)GPU會(huì)閑置,當(dāng)通道達(dá)到一定數(shù)量,對(duì)性能也會(huì)有較大的影響;

【為什么會(huì)產(chǎn)生離屏渲染?】
首先,OpenGL提交一個(gè)命令到Command Buffer,隨后GPU開(kāi)始渲染,渲染結(jié)果放到Render Buffer中,這是正常的渲染流程?!镜怯幸恍?fù)雜的效果無(wú)法直接渲染出結(jié)果,它需要分步渲染最后再組合起來(lái)】,比如添加一個(gè)蒙版(mask)。
會(huì)造成 offscreen rendering 的原因有:

陰影(UIView.layer.shadowOffset/shadowRadius/…)
圓角(當(dāng) UIView.layer.cornerRadius 和 UIView.layer.maskToBounds 一起使用時(shí))
圖層蒙板(Mask)
開(kāi)啟光柵化(shouldRasterize = true,同時(shí)設(shè)置 rasterizationScale)

【Mask】
一個(gè)圖層可以有一個(gè)和它相關(guān)聯(lián)的 mask(蒙板),mask 是一個(gè)擁有 alpha 值的【位圖】【不是矢量圖,所以矢量圖是不能作為遮罩】。只有在 mask 中顯示出來(lái)的(即圖層中的部分)才會(huì)被渲染出來(lái)。

使用陰影時(shí)同時(shí)設(shè)置 shadowPath 就能避免離屏渲染大大提升性能,圓角觸發(fā)的離屏渲染可以用 CoreGraphics 將圖片處理成圓角來(lái)避免。
CALayer 有一個(gè) shouldRasterize 屬性,將這個(gè)屬性設(shè)置成 true 后就開(kāi)啟了光柵化。
【什么是光柵化】
光柵化其實(shí)是一種將幾何圖元變?yōu)槎S圖像的過(guò)程。
你模型的那些頂點(diǎn)在經(jīng)過(guò)各種矩陣變換后也僅僅是頂點(diǎn)。而由頂點(diǎn)構(gòu)成的圖形要在屏幕上顯示出來(lái),除了需要頂點(diǎn)的信息以外,還需要確定構(gòu)成這個(gè)圖形的所有像素的信息。
【光柵化優(yōu)缺點(diǎn)】
開(kāi)啟光柵化后會(huì)將圖層繪制到一個(gè)屏幕外的圖像,然后這個(gè)圖像將會(huì)被緩存起來(lái)并繪制到實(shí)際圖層的 contents 和子圖層,對(duì)于有很多的子圖層或者有復(fù)雜的效果應(yīng)用,這樣做就會(huì)比重繪所有事務(wù)的所有幀來(lái)更加高效。但是光柵化原始圖像需要時(shí)間,而且會(huì)消耗額外的內(nèi)存。
光柵化也會(huì)帶來(lái)一定的性能損耗,是否要開(kāi)啟就要根據(jù)實(shí)際的使用場(chǎng)景了,圖層內(nèi)容頻繁變化時(shí)不建議使用。最好還是用 Instruments 比對(duì)開(kāi)啟前后的 FPS 來(lái)看是否起到了優(yōu)化效果。
離屏渲染請(qǐng)參考:iOS 離屏渲染的研究

參考資料:
1.iOS進(jìn)階之頁(yè)面性能優(yōu)化
2.繪制像素到屏幕上
3.iOS 保持界面流暢的技巧
4.如何正確地寫(xiě)好一個(gè)界面

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

  • CPU和GPU的區(qū)別 CUP和GPU之所以大不相同,是由于其設(shè)計(jì)目的的不同,它們分別針對(duì)了兩種不同的應(yīng)用場(chǎng)景。CP...
    夏天的風(fēng)_song閱讀 7,836評(píng)論 8 34
  • 繪制像素到屏幕上 answer-huang22 Mar 2014 分享文章 一個(gè)像素是如何繪制到屏幕上去的?有很多...
    阿貍旅途T恤閱讀 1,779評(píng)論 0 7
  • 卷首語(yǔ) 歡迎來(lái)到 objc.io 的第三期! 這一期都是關(guān)于視圖層的。當(dāng)然視圖層有很多方面,我們需要把它們縮小到幾...
    評(píng)評(píng)分分閱讀 1,943評(píng)論 0 18
  • (1)Time Profiler:用來(lái)測(cè)量被方法/函數(shù)打斷的CPU使用情況。 (2)Core Animation:...
    錢(qián)噓噓閱讀 1,767評(píng)論 2 6
  • 中秋的夜晚, 天邊那皎潔的月光, 曾留下多少憂(yōu)傷而美麗的詩(shī)句。 北海源,歡迎您! 請(qǐng)將您作品中最好的一首詩(shī), 回復(fù)...
    北海源閱讀 2,608評(píng)論 220 58

友情鏈接更多精彩內(nèi)容