iOS屏幕繪制

屏幕是如何繪制的?這其中涉及到了很多的流程和不同的系統(tǒng)框架,這里就來梳理一邊在屏幕繪制的過程中都發(fā)生了什么,希望可以幫助大家在性能調(diào)優(yōu)的時候選擇合適的方法和API。這里講解的內(nèi)容主要是針對iOS平臺,同樣也適用于OS X平臺。

圖形堆棧

不管前面發(fā)生了什么,最終在屏幕顯示的所有東西都是通過像素展示出來的。每個像素由三個顏色單元組成,RGB紅綠藍(lán),每個顏色單元通過不同的強(qiáng)度亮起來,共同形成了這一個像素的顏色。例如在iPhone 5手機(jī)上由1136 x 640 = 727040個像素,那么就一共有2181120個顏色單元,在一個15寸的配置了視網(wǎng)膜屏幕MacBook Pro上有1550萬個顏色單元。整個圖形堆棧的任務(wù)就是通力協(xié)作確保每一個顏色單元按照正確的強(qiáng)度亮起來。當(dāng)屏幕滾動的時候,所有這些顏色單元都要以每秒60次的頻率刷新自己的強(qiáng)度。

軟件架構(gòu)

軟件架構(gòu).png

在屏幕上方是GPU,圖像處理單元,專門設(shè)計用來并行處理圖像計算的,從而保證了屏幕像素的快速刷新,包括不同紋理的疊加顯示,因為他是專門用來處理圖像的,因此它在處理圖像方面要比CPU更快,耗電更少。
GPU的上層是GPU驅(qū)動,直觀上理解就是直接操作GPU的一段代碼,用來屏蔽不同GPU的特性,從而向上層提供更加統(tǒng)一的接口,這里的上層通常是指OpenGL,現(xiàn)在應(yīng)該已經(jīng)被Metal所取代了。
OpenGL是一套繪制2D和3D圖形的API接口,因為GPU的差異性非常大,OpenGL直接與GPU通信來最大化利用GPU的特性同時加速硬件渲染。雖然OpenGL看起來非常底層,但是在1992年剛出現(xiàn)的時候,是第一個標(biāo)準(zhǔn)化的與GPU通信的方式,結(jié)束了針對不同GPU需要重寫和優(yōu)化代碼的局面。
再向上就比較雜了,在iOS應(yīng)用上,基本上是逃不開使用Core Aanimation的, 在OS X上,則常常跳過Core Animation而直接使用Core Graphics, 對于游戲或者某些特殊的應(yīng)用,可能會直接使用OpenGL。對于其他的一些系統(tǒng)庫,如AVFoundation、Core Image等則會同時用到上面提到這些技術(shù)。
需要注意的是,GPU與CPU通過某些總線相連,像是OpenGL、Core Animation和Core Graphics會協(xié)調(diào)數(shù)據(jù)在CPU和GPU之間的傳輸。有一些操作在CPU內(nèi)完成,然后將數(shù)據(jù)上傳到GPU,然后GPU在通過一些計算最終將圖片以像素的方式顯示在屏幕上。

硬件架構(gòu)

硬件架構(gòu).png

對于GPU來講,它的瓶頸在于需要在每一幀將所有的位圖(紋理texture)合成一張完整的像素表顯示在屏幕上,每個位圖都會占用一部分VRAM(顯存),而顯存的容量又是有限制的,GPU在位圖組合上是非常高效的,但是有些合成會非常耗時,因此GPU在16.7ms也就是1/60s內(nèi)能做的事情其實是非常有限的。
另外一個瓶頸在與將數(shù)據(jù)從內(nèi)存上傳至顯存中,當(dāng)數(shù)據(jù)量較大的時候,這個過程會非常耗時。
另外,也有很多過程是由CPU在計算的,比如我們會告訴CPU從沙盒中家在一個PNG圖片并解壓縮,所有這些過程都由CPU在計算,當(dāng)它需要被顯示在屏幕上的時候,這些數(shù)據(jù)會被上傳給GPU。對于平常的文字顯示來講,CPU需要通過Core Text與Core Graphics的通力合作來生成一張顯示的文字的唯獨,然后交給GPU顯示,當(dāng)屏幕滾動的時候,這張位圖是可以被重用的,此時CPU僅僅需要告訴GPU這張位圖新的位置在哪里就可以了。CPU不想要重新生成位圖,位圖也不需要重新被上傳至GPU。

合成

合成的意思是將多張位圖組合在一起形成一張最終展示在屏幕上的圖像的過程。
我們假設(shè)一種最簡單的情況,需要展示的是一個矩形的區(qū)域,那么這張位圖就是一個由RGBA值組成的矩陣,這其實就是Core Aanimation中CALayer。
每個layer都是一張位圖,假設(shè)每個layer都完整地疊加張另一個layer上面,那么最終顯示在屏幕的時候,GPU需要計算出所有l(wèi)ayer疊加后,矩陣中的每一個值。這就是合成。
如果只有一個位圖,且位圖的每一個點正好對應(yīng)屏幕上的每一個像素,那么位圖的像素就是最終需要顯示在屏幕的像素。如果這張位圖上面還有一張位圖,那么GPU就需要進(jìn)行合成動作,合成有三種模式。假設(shè)這兩張位圖的像素一一對應(yīng),且使用正常的模式合成,那么最終像素的結(jié)果是:
R = S + D * (1 - Sa)
最終的沒一點的像素是上面的像素加上下面像素與1減去上面像素透明值的乘機(jī)。這里假設(shè)每一個自己的像素點都已經(jīng)與自己的透明值進(jìn)行了乘機(jī),如(RGB為 240, 99, 24且透明度為0.33的像素表示為ARGB84, 80, 33, 8)。
如果我們假設(shè)第一張位圖(1 0 0)的alpha是1,那么最終的結(jié)果就是R = S。
這里我們可以看到,圖形中的每個點計算過去計算量非常大的。上面僅僅是針對兩張位圖的計算,而真實場景中通常是有非常多的圖層相互疊加而成,即使GPU是專門設(shè)計用來進(jìn)行這種計算的,這仍然是一項非常繁重的工作。

透明vs不透明

如果上面的像素是不透明的話,那么GPU會省去很多的工作,直接使用上面的像素顯示就可以了,但是GPU無法知道是不是上面的位圖中所有的像素都是不透明的,所以CALayer中有一個opaque的屬性,如果設(shè)置為YES,那么GPU將會忽略掉下面的所有圖層,直接使用當(dāng)前的圖層進(jìn)行顯示。通過Instruments工具中的blended layers選項可以查看到那些圖層是不透明的,需要GPU進(jìn)行合成動作。
如果你確定你的圖層是不透明的,那么請將它的opaque屬性設(shè)置為YES。如果你家在的圖片沒有alpha通道,那么在通過UIImageView顯示圖片的時候系統(tǒng)會自動地將opaque屬性設(shè)置為YES。但是一個沒有alpha通道的圖片和一個有alpha通道但是完全不透明的圖片是有區(qū)別的,因為Core Animation需要一個一個像素判斷過去那些像素是不透明的。在Finder中可以通過Get Info屬性查看圖片的alpha通道信息。

像素對齊和錯位

到目前為止,我們討論的情況都是位圖像素與屏幕像素正好對齊的情況。這種情況的計算比較簡單,直接將圖層中像素與對應(yīng)的屏幕上的點的像素做合并就可以了。
當(dāng)圖層的像素與屏幕上的像素正好是一一對應(yīng)的時候,我們稱之為像素對齊,但是有兩種情況卻無法滿足一一對應(yīng)的要求。第一種情況是縮放,當(dāng)圖像被縮放的時候,圖像中的像素?zé)o法與屏幕上的像素一一對應(yīng);另一種情況是圖像起點不在像素的邊界上。
這兩種情況下,GPU都需要做額外的運算。需要將原圖上的多個像素進(jìn)行運算才能確定用來進(jìn)行合成動作的像素值。
在Intrument和模擬器中可以查看到那些圖層發(fā)生了像素錯位。

蒙版

一個圖層可以關(guān)聯(lián)一個蒙版,一個蒙版就是一個具有alpha值的位圖,該圖層會先與蒙版組合,然后再與下面的圖層進(jìn)行組合。當(dāng)在設(shè)置一個圖層的圓角的時候,其實就是在為圖層添加一個蒙版,我們還可以通過設(shè)置一個自定形狀的蒙版,比如字母A,那么就會只用字母形狀的部分被顯示出來。

離屏渲染

離屏渲染可以被Core Animation自動觸發(fā),也可以被應(yīng)用強(qiáng)制觸發(fā)。離屏渲染將圖層樹的一部分放到新的緩存中(也就是說不是直接顯示在屏幕上的)的進(jìn)行組合,然后再將buffer渲染到屏幕上。
當(dāng)組合非常耗時的時候,你可能會想要強(qiáng)制進(jìn)行離屏渲染,這是一種緩存像素組合結(jié)果的方法。當(dāng)圖層樹非常復(fù)雜的時候,可以通過離屏渲染來緩存這些圖層組合的結(jié)果,然后再將緩存繪制到屏幕上。
如果應(yīng)用組合了非常多的圖層,同時需要將他們一起做一個動畫,那么GPU通常會在每一幀的時候重新繪制這些圖層到屏幕上,如果使用離屏渲染,那么GPU會首先將這些圖層組合的結(jié)果緩存起來,然后再利用緩存繪制屏幕,這樣當(dāng)圖層移動的時候,GPU可以重復(fù)利用緩存的位圖,從而減少工作量。但是需要注意的是,這種情況必須保證圖層沒有變化,如果圖冊內(nèi)容發(fā)生了變化,那么GPU就必須重新創(chuàng)建位圖緩存??梢酝ㄟ^屬性shouldRasterize設(shè)置為YES,來觸發(fā)離屏渲染。
這是一種取舍,因為它也有可能使得渲染變得很慢。創(chuàng)建額外的離屏緩存對于GPU來講是一種額外的工作負(fù)擔(dān),如果創(chuàng)建出來的緩存后面都用不到,那么無疑這是一種浪費,如果緩存經(jīng)常會被用到,那么對GPU來講會是一種減負(fù)。需要通過衡量GPU的使用情況來判斷是否需要離屏渲染。
離屏渲染可可能是某些屬性設(shè)置的負(fù)面效果,如果直接或間接設(shè)置了圖層的蒙版,那么Core Animation將會強(qiáng)制使用離屏渲染來應(yīng)用蒙版,這對GPU來講是一種負(fù)擔(dān)。通常GPU的工作負(fù)荷只夠直接將圖層繪制到屏幕的幀緩存中。
Instruments中的Core Aanimation工具有一個Color Offscreen-Rendered Yellow的工具(模擬器的調(diào)試選項中也有),可以將離屏渲染的部分用黃色標(biāo)識出來。勾選上Color Hits Green和Miises Red選項,綠色表示離屏緩存被重用,紅色表示離屏緩存被重建。
通常來講應(yīng)該盡量避免離屏緩存,因為操作耗時。直接將結(jié)果組合到屏幕的幀緩存上要更快。首先創(chuàng)建一個離屏緩存,將圖層繪制到離屏緩存上,然后再繪制到幀緩存上,這一系列的動作涉及到上下文的來回切換,會非常耗時。
所以當(dāng)調(diào)試的時候看到黃色標(biāo)識的時候需要警惕。但也要根據(jù)實際情況來判斷,如果Core Animation可以重用緩存的話也是可以提升性能的,但重用的前提是緩存的圖層是沒有變化的。
還需要注意的是柵格化圖層(也就是需要先離屏渲染的圖層)的緩存空間是有限的,蘋果暗示大概是屏幕幀緩存的兩倍。
通常設(shè)置圓角、設(shè)置蒙版圖層添加陰影等都會觸發(fā)離屏渲染,需要盡量避免。針對需要圓角的圖層,可以創(chuàng)建一個圓角的圖像作為圖層的內(nèi)容來避免離屏渲染,而針對一個矩形的蒙版,則可以通過使用contentsRect來實現(xiàn)矩形蒙版的效果。
如果你仍然執(zhí)意要把shouldRasterize屬性數(shù)值為YES,記住同時還需要將rasterizationScale設(shè)置為contentsScale。

OS X

如果使用的是OS X平臺,那么你會發(fā)現(xiàn)大部分的調(diào)試工具都在Quartz Debug應(yīng)用中。Quartz Debug工具在Graphics Tools應(yīng)用中,是一個需要單獨下載的開發(fā)工具。

Core Animation和OpenGL ES

Core Animation主要負(fù)責(zé)動畫,但是這里我們跳過動畫,專注于繪制。Core Animation可以很高效地進(jìn)行繪制,這也是為什么使用Core Animation動畫可以輕松滿足每秒60幀的動畫。
Core Animation的核心是OpenGL ES,它封裝了OpenGL ES的復(fù)雜性。當(dāng)我們在討論合成的時候經(jīng)常使用到圖層和位圖兩個術(shù)語,這兩個是不同的概念,但卻非常相近。
Core Animation圖層可以有子圖層,是一個圖層樹的結(jié)構(gòu),Core Animation的主要工作是計算出圖層需要顯示的內(nèi)容,而OpenGL ES的主要工作是將這些圖層進(jìn)行組合并展示到屏幕上。
例如,當(dāng)設(shè)置圖層的內(nèi)容為某個CGImageRef的時候Core Animation創(chuàng)建一個OpenGL紋理,然后將圖片的位圖上傳到對應(yīng)的紋理中。如果重寫了drawInContext方法,那么Core Animation會分配一個紋理,同時調(diào)用你的方法將生成對應(yīng)的位圖數(shù)據(jù)。圖層的屬性以及CALayer的子類會影響OpenGL如何進(jìn)行繪制,很多OpenGL ES的底層行為都被封裝到了CALayer的概念中。
Core Animation一邊通過Core Graphics在CPU內(nèi)計算出對應(yīng)的位圖,另一邊連接著OpenGL ES,因為Core Animation是整個繪制流程中的核心管道,因為它的使用方式將直接影響到繪制的性能。

CPU綁定vxGPU版定

在一個完整的顯示流程中,GPU與CPU是一起工作的,各自處理各自的任務(wù),各自也都有著各自有限的資源。
為了保證每秒60幀的刷新速率,我們需要保證CPU和GPU都不能超負(fù)荷工作,或者極端一點,我們需要將盡可能多的工作交給GPU,將CPU釋放出來來處理程序的邏輯,而不是忙于顯示。另外,GPU在處理顯示相關(guān)的邏輯要比CPU更快,耗電更少,負(fù)荷也更低。
因為顯示需要CPU與GPU共同完成,因此我們需要搞清楚到底是哪個在影響我們的繪制性能。如果是GPU影響了性能,那么稱之為GPU綁定,如果是CPU,則是CPU綁定。對應(yīng)的解決方法當(dāng)然是給GPU減負(fù)或給CPU減負(fù)。
我們可以通過OpenGL ES Driver工具來查看,點擊小i按鈕,然后配置,確保選中了Device Utilization選項,就可以查看app運行的時候GPU的負(fù)荷,如果接近100%,則表示GPU的工作量超負(fù)荷了。
而CPU綁定則比較常見,剋有通過Timer Profile工具來查看耗時在哪里。

Core Graphics Quartz 2D

Quartz 2D相比于它所在的框架Core Graphics,并不是那么被人們所熟悉。Quartz 2D功能非常強(qiáng)大,以PDF的創(chuàng)建、繪制和打印為例,其流程與在屏幕上繪制位圖基本上一樣,都是基于Quartz 2D的。
我們主要看下Quartz 2D的2D繪制功能。其中涉及到了基于路徑的繪制、抗鋸齒渲染、透明圖層、分辨率與設(shè)備獨立性,涉及到的各種API非常龐大,而且大部分都是基于C的接口。
好在UIKit和AppKit封裝了部分的Quartz 2D接口,使其更加方便使用。這些接口可以實現(xiàn)類似于Photoshop和Illustrator的效果。蘋果提到過預(yù)安裝的股票app就是使用了Quartz 2D,其中的曲線繪制就是使用了Quartz 2D編碼。
當(dāng)app在繪制位圖的時候,不管怎樣總會用到Quartz 2D,這其中一部分位圖的計算是由CPU通過Quartz 2D來完成的。雖然Quartz 2D可以完成更多豐富的功能,但是這里我們僅聚焦于使用Quartz 2D來生成一段內(nèi)存中的RGBA數(shù)據(jù)。
比如,我們想要繪制一個八邊形,使用UIKit:

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(16.72, 7.22)];
[path addLineToPoint:CGPointMake(3.29, 20.83)];
[path addLineToPoint:CGPointMake(0.4, 18.05)];
[path addLineToPoint:CGPointMake(18.8, -0.47)];
[path addLineToPoint:CGPointMake(37.21, 18.05)];
[path addLineToPoint:CGPointMake(34.31, 20.83)];
[path addLineToPoint:CGPointMake(20.88, 7.22)];
[path addLineToPoint:CGPointMake(20.88, 42.18)];
[path addLineToPoint:CGPointMake(16.72, 42.18)];
[path addLineToPoint:CGPointMake(16.72, 7.22)];
[path closePath];
path.lineWidth = 1;
[[UIColor redColor] setStroke];
[path stroke];

使用Core Graphics代碼如下:

CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 16.72, 7.22);
CGContextAddLineToPoint(ctx, 3.29, 20.83);
CGContextAddLineToPoint(ctx, 0.4, 18.05);
CGContextAddLineToPoint(ctx, 18.8, -0.47);
CGContextAddLineToPoint(ctx, 37.21, 18.05);
CGContextAddLineToPoint(ctx, 34.31, 20.83);
CGContextAddLineToPoint(ctx, 20.88, 7.22);
CGContextAddLineToPoint(ctx, 20.88, 42.18);
CGContextAddLineToPoint(ctx, 16.72, 42.18);
CGContextAddLineToPoint(ctx, 16.72, 7.22);
CGContextClosePath(ctx);
CGContextSetLineWidth(ctx, 1);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextStrokePath(ctx);

問題是這些東西繪制在哪里?這也就是CGContext存在的目的,這個上下文就是我們需要繪制的地方。如果我們重寫了CALayer的drawInContext方法,其中有個上下文的參數(shù),向那個上下文寫入也就是向那個圖層的緩存中繪制數(shù)據(jù)。但是我們也可以通過CGBitmapContextCreate方法來創(chuàng)建自己的上下文,然后向其中繪制數(shù)據(jù)。
在UIKit的代碼中不需要使用上下文,因為UIKit和AppKit中上下文是自動維護(hù)的,UIKit維護(hù)了一個上下文堆棧,UIKit方法總是向頂層的上下文中繪制數(shù)據(jù),可以通過UIGraphicsGetCurrentContext方法獲取到最頂層的上下文,還可以通過UIGraphicsPushContext和UIGraphicsPopContext向堆棧中添加和刪除上下文。
在UIKit中可以使用UIGraphicsBeginImageContextWithOptions和UIGraphicsEndImageContext來創(chuàng)建一個位圖的上下文,類似于CGBitmapContextCreate方法,UIKit與Core Grahpics混寫也是可以的:

UIGraphicsBeginImageContextWithOptions(CGSizeMake(45, 45), YES, 2);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 16.72, 7.22);
CGContextAddLineToPoint(ctx, 3.29, 20.83);
...
CGContextStrokePath(ctx);
UIGraphicsEndImageContext();

或者

CGContextRef ctx = CGBitmapContextCreate(NULL, 90, 90, 8, 90 * 4, space, bitmapInfo);
CGContextScaleCTM(ctx, 0.5, 0.5);
UIGraphicsPushContext(ctx);
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(16.72, 7.22)];
[path addLineToPoint:CGPointMake(3.29, 20.83)];
...
[path stroke];
UIGraphicsPopContext(ctx);
CGContextRelease(ctx);

Core Graphics功能異常強(qiáng)大,以至于蘋果將其描述為可以做到無與倫比的保真輸出。它與Adobie Illustrator和Adobe Photoshop的工作模型非常相近,而且Core Graphics中的絕大部分概念也是出自其中。畢竟它起源于NeXTSTEP,也是使用Display PostScript,與Adobe相同。

像素

屏幕上的像素通常使用三個顏色單位來表示RGB,每一張位圖都可以使用RGB數(shù)據(jù)來表示,那么這些數(shù)據(jù)在內(nèi)存中是如何表示的呢?目前有很多種不同的表示方法。
首先我們以一張位圖為例,通常每個像素我們使用RGB配合上透明度alpha,四個單位來表示。

默認(rèn)像素布局

在iOS和OS X上一種常見的格式是32 bits-per-pixel(bpp,每個像素32位),8 bits-per-component(bpc,每個單位8位),透明值預(yù)算。在內(nèi)存中如下:

  A   R   G   B   A   R   G   B   A   R   G   B  
| pixel 0       | pixel 1       | pixel 2   
  0   1   2   3   4   5   6   7   8   9   10  11 ...

這種格式通常稱為ARGB,開頭的第一個為透明度,后面的RGB的值都是與透明度相乘過的結(jié)果。
另一種常見的也是32bpp,8bpc,透明度開頭但無值的格式如下:

  x   R   G   B   x   R   G   B   x   R   G   B  
| pixel 0       | pixel 1       | pixel 2   
  0   1   2   3   4   5   6   7   8   9   10  11 ...

這種格式也被成為xRGB,沒有透明度的值,但是內(nèi)存依然預(yù)留了位置,這樣做除了增加了25%內(nèi)存開銷有什么好處呢?最大的好處就是更容易被現(xiàn)代的CPU進(jìn)行計算,如果所有的像素都是32位的寬度,那么所有的運算數(shù)據(jù)都是對齊的,不用進(jìn)行大量的位移運算,尤其是在與有透明度的格式進(jìn)行混合的時候。
在處理RGB數(shù)據(jù)的時候,Core Graphics也支持將透明度值放到最后,如RGBA和RGBx。

Esoteric Layouts

大多數(shù)情況下,我們處理位圖數(shù)據(jù)的時候都是通過Core Graphics和Quartz 2D,它支持有限的數(shù)據(jù)格式,但是RGB還有其他的數(shù)據(jù)格式,如16 bpp 5bpc無透明度的格式。這種格式大概是之前格式所占用內(nèi)存的一半。但是因為每個顏色單位只有5位,因此圖像(尤其是平滑梯度)會產(chǎn)生條帶的效果。
另外也有64bpp 16bpc 和 128bpp 32 bpc(同時支持和不支持透明度)的格式,因為每個顏色單位使用的位數(shù)更大,因此具有更大的保真度,但是同時也占用了更多的內(nèi)存和計算資源。
為了解決這個問題,Core Graphics還支持了一些會讀和CMKY格式,也是同樣包含了支持和不支持透明度的格式。

平面數(shù)據(jù)

大部分的處理框架,包括Core Graphics使用的像素數(shù)據(jù)是混合在一起的。但是有些情況下也會遇到所謂的平面組件或者組件平面的東西。它的意思是每個顏色單元共同占據(jù)一塊內(nèi)存區(qū)域,例如RGB來講,有三個獨立的內(nèi)存區(qū)域,一塊包含了所有的R值,一塊包含所有的G值和一塊包含所有的B值。通常在一些視頻處理框架中在某些情況下會使用平面數(shù)據(jù)。

YCbCr
YCbCr是處理視頻數(shù)據(jù)的時候常用的格式。它也包含了三個單元(Y表示光的濃度,Cb和Cr表示藍(lán)色和紅色濃度偏移量)用來表示顏色。簡單來講,它更接近于人眼對于顏色的識別。相對于Cb和Cr,人眼對于亮度更加luma Y更加敏感,因此Cb和Cr可以被更加激進(jìn)地壓縮,而不影響總體的圖像質(zhì)量。
出于同樣的原因,JPEG有時候會把RGB數(shù)據(jù)轉(zhuǎn)換為YCbCr,對每個顏色單位單獨進(jìn)行壓縮,在對YCbCr進(jìn)行壓縮的時候,Cb和Cr可以被壓縮得更小。

圖片格式

通常在iOS和 OS X平臺上最常用的是JPEG和PNG格式的圖片,讓我們重點看下這兩種格式。

JPEG

JPEG家喻戶曉,通常用來作為相機(jī)圖片的格式。很多人認(rèn)為JPEG格式僅僅是一種圖片的存儲格式,將所有的RGB數(shù)據(jù)按照上面的某種方式存儲在磁盤上,實際上不是這個樣子。
將JPEG數(shù)據(jù)轉(zhuǎn)換成可以直接顯示的RGB數(shù)據(jù)是非常復(fù)雜的。對于每個顏色平面,JPEG壓縮使用基于離散余弦算法將空間數(shù)據(jù)轉(zhuǎn)換位頻域數(shù)據(jù)。然后再基于霍夫曼編碼的改進(jìn),對數(shù)據(jù)進(jìn)行量化、排序和打包。壓縮的時候?qū)GB轉(zhuǎn)換為YCbCr數(shù)據(jù)。解壓的時候是上述算法的逆向過程。
這就是為什么創(chuàng)建一個JPEG的UIImage顯示在屏幕上的時候,會有一個延遲,因為CPU需要進(jìn)行數(shù)據(jù)的解壓。如果在tableview中使用了JPEG格式的圖片,那么在滾動的時候就很容易發(fā)生卡頓。
那么為什么還要使用JPEG格式呢?因為JPEG格式壓縮照片的效果非常好。iPhone 5拍攝的原始照片有24MB,默認(rèn)的壓縮設(shè)置可以將圖片壓縮到2到2M。JPEG壓縮效果好主要是因為他是有損壓縮,通過將人眼不敏感的信息都丟掉,可以遠(yuǎn)遠(yuǎn)超過普通壓縮算法的效果。但這也僅僅是針對照片,因為JPEG依賴于圖片中對于人眼不敏感的信息。如果從網(wǎng)頁中截屏一段文字,JPEG就無法很好地進(jìn)行壓縮,壓縮過程會變慢,同時還可以明顯感覺到JPEG修改了截屏的原始圖片。

PNG

PNG讀作ping。相對于JPEG,他是一種無損的壓縮格式。將圖片保存成PNG,然后再打開它,數(shù)據(jù)會和之前一摸一樣。因此PNG對圖片的壓縮無法向JPEG一樣優(yōu)秀,但是對于app中常用的美術(shù)圖片,如按鈕、圖標(biāo)等,非常實用。而且解碼PNG數(shù)據(jù)要比解碼JPEG輕松得多。
但是現(xiàn)世界中也存在著多種PNG格式,但是簡單來講,PNG也同時支持有和沒有透明通道的RGB像素壓縮。這也是PNG適合app藝術(shù)圖片的一種原因。

選擇格式

在app中一般僅在JPEG和PNG中選擇需要的格式。讀取和寫入這兩種格式的編碼和解碼器是經(jīng)過高度優(yōu)化的,且支持并行。而且還可以享受這蘋果升級帶來的長期持續(xù)地免費優(yōu)化。如果使用其他格式的圖片,有可能會影響到app的性能,而且還可能會有安全漏洞,因此在圖像解碼的過程是黑客們很喜歡注入代碼的地方。
需要注意的是,Xcode針對PNG格式的優(yōu)化與其他常見的優(yōu)化引擎有很大的不同。當(dāng)Xcode在對PNG文件優(yōu)化的時候,這些PNG文件從嚴(yán)格意義上來講已經(jīng)不再是PNG圖片了,但是對于iOS來講卻可以更高效地解壓這些圖片。Xcode通過這種方式,來使得這些圖片在解壓的時候可以使用更高效的算法,但是這種算法并不適用于一般的PNG格式的圖片。就像我們之前提到過的,針對像素數(shù)據(jù)的處理,有多種表示RGB數(shù)據(jù)的方法,如果這種格式不是iOS圖像系統(tǒng)所需要的,就需要對每個像素數(shù)據(jù)進(jìn)行便宜操作,而能省略這一步將會大大提升效率。
還需要注意的一點是,如果可以,盡量使用可伸縮圖片作為app的素材,這樣文件可以盡可能地小,文件需要只需要加載和解碼更少的數(shù)據(jù)量。

UIKit和像素

UIKit中的每個視圖都有自己的CALayer,因此每個圖層通常都對應(yīng)一個自己的位圖緩存,類似于一張圖片,這個緩存就是最終要被顯示在屏幕上的數(shù)據(jù)。

-drawRect:

如果你的視圖重寫了-drawRect:方法,那么當(dāng)調(diào)用setNeedsDisplay方法的時候,UIKit會調(diào)用圖層setNeedsDisplay方法,將圖層打上標(biāo)記,標(biāo)識該圖層需要被重繪。這個時候還有沒做任何實際的動作,因此一口氣連續(xù)調(diào)用setNeedsDisplay方法并不會有效率問題。
然后當(dāng)繪制系統(tǒng)準(zhǔn)備好以后,會調(diào)用圖層的display方法,這個時候圖層會建立自己的位圖緩存,并創(chuàng)建一個Core Graphics上下文CGContextRef與該位圖緩存的內(nèi)存相關(guān)聯(lián),使用該上下文繪制的時候,內(nèi)容會被寫進(jìn)該內(nèi)存區(qū)域。
當(dāng)使用UIKit的繪制方法,如在drawRect方法內(nèi)調(diào)用UIRectFill或[UIBezierPath fill] 的時候,他們將會使用該上下文。因為UIKit會將圖層的上下文壓到自己的圖層堆棧內(nèi),將其設(shè)置為當(dāng)前的上下文。使用UIGraphicsGetCurrent將會返回該堆棧頂部的上下文,因此使用UIGraphicsGetCurrent繪圖的時候,會繪制到對應(yīng)圖層的位圖緩存內(nèi)。因此當(dāng)你想要直接使用Core Graphics方法的時候,就可以使用UIGraphicsGetCurrent來獲取當(dāng)前圖層的上下文,然后傳遞到Core Graphics方法內(nèi)作為上下文參數(shù)。
然后圖層的位圖緩存會被不斷地繪制到屏幕上,直到某個時候再次被調(diào)用了setNeedsDisplay方法來更新該位圖緩存。

沒有使用drawRect的時候

當(dāng)使用UIImageView的時候,視圖仍然會有一個對應(yīng)的CALayer,但是該圖層并沒有分配一個對應(yīng)的位圖緩存,而是使用CGImageRef作為他的內(nèi)容,然后繪制引擎會將圖像的數(shù)據(jù)繪制到幀緩存內(nèi)進(jìn)行顯示。
這個過程中并沒有繪制的動作發(fā)生,僅僅是將UIImageView的圖像以位圖的數(shù)據(jù)的形式發(fā)送給Core Animation,再由它將其發(fā)送給繪制引擎。

使用drawRect: 或不使用 -drawRect:

雖然說了等于沒說,但還是要說“最快地繪圖就是不去繪圖”
大多數(shù)情況下,我們不需要組合自己自定義的視圖或者組合圖層,因為UIKit已經(jīng)對他們做過了優(yōu)化。
一個自定義繪圖的例子是蘋果的WWDC 2012’s session 506: Optimizing 2D Graphics and Animation Performance,里面有一個手指畫畫的demo。
另外一個需要自己繪制的例子是iOS的預(yù)裝股票app,k線圖是通過Core Grahpics繪制的。需要注意的是,雖然你需要自己手動繪制一些東西,但并不意味著一定要通過drawRect方法,有些情況下可以通過UIGraphicsBeginImageContextWithOptions或者CGBitmapContextCreate,從中返回一個圖片,設(shè)置到圖層的內(nèi)容當(dāng)中。進(jìn)行測試比較,哪個更快。

實色

如果我們看下面的代碼

- (void)drawRect:(CGRect)rect
{
    [[UIColor redColor] setFill];
    UIRectFill([self bounds]);
}

之所以這段代碼不好,是因為我們讓Core Animation創(chuàng)建了一個圖層的緩存,然后讓Core Graphics向里面填充了實色,然后將其上傳到GPU中。
我們完全可以繞開drawRect方法來省去上面的這些創(chuàng)建、繪制和上傳動作,直接通過設(shè)置視圖圖層的backgroundColor。如果視圖的默認(rèn)圖層是一個CAGradientLayer圖層,同樣的方法可以繞開創(chuàng)建位圖緩存的過程來實現(xiàn)漸變。

可拉伸圖片

類似的,可以通過使用可拉伸圖片來盡情圖像系統(tǒng)的壓力。比如說我們需要一個300 x 50的圖片作為按鈕素材,600 x 100 = 60k像素,然后需要60k x 4 = 240kB的內(nèi)存數(shù)據(jù)需要被上傳到GPU,這些數(shù)據(jù)是需要占用顯存的。如果可以使用可拉伸圖片的話,比如說使用一個54 x 12的素材,那么只需要2.6k像素和10kB的內(nèi)存,會更快。
Core Animation可以通過CALayer的contentsCenter屬性來拉伸圖片,但是大多數(shù)情況下我們還是更常用[UIImage resizableImageWithCapInsets:resizingMode:]。
另外,在按鈕最終被第一次顯示在屏幕上之前,相對于從文件系統(tǒng)中讀取60k像素的PNG圖片并解壓,圖片越小,這個過程也會更快。因此總體來說使用更小的可拉伸圖片在各個流程都會更加高效。

并行繪制

objc.io的最后一個iusse就是并行。我們都知道UIKit的線程模型非常簡單,只能夠在主線程中使用UIKit中的類。那么對于繪制呢?
如果重寫了drawRect方法,并需要繪制一些較為復(fù)雜的內(nèi)容,那么這可能會比較耗時。為了讓動畫更加順滑,我們很傾向于在次線程中做很多工作。多線程非常復(fù)雜,但是通過一些方法,我們還是可以很輕松地實現(xiàn)多線程繪制。
我們只能在主線程中向CALayer的位圖緩存中繪制數(shù)據(jù),這通常不是很高效,但是我們可以在次線程中向另外一個上下文繪制內(nèi)容。
我們之前講過,Core Graphics方法都會有一個上下文參數(shù),表示向哪里繪制內(nèi)容。UIKit有一個當(dāng)前上下文表示數(shù)據(jù)繪制到哪里。這個當(dāng)前上下文針對每一個線程是不同的。
為了實現(xiàn)并線繪制,我們可以采用下面的方法。在另外一個隊列中創(chuàng)建一個圖片,然后切換到主線程中將該圖片設(shè)置為UIImageView的圖片。在WWDC 2012 session 211中有對其進(jìn)行討論:

- (UIImage *)renderInImageOfSize:(CGSize)size;
{
    UIGraphicsBeginImageContextWithOptions(size, NO, 0);

    // do drawing here

    UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return result;
}

這個方法通過UIGraphicsBeginImageContextWithOptions創(chuàng)建了一個指定尺寸的CGContextRef位圖。同時將新創(chuàng)建的上下文作為UIKit的當(dāng)前上下文。此時就可以像在drawRect中那樣繪制需要的內(nèi)容。然后通過UIGraphicsGetImageFromCurrentImageContext將位圖數(shù)據(jù)以圖片的形式返回,最后銷毀該上下文。
非常重要的一點是,在上面的方法中所有調(diào)用的繪制方法必須是線程安全的,也就是說你所調(diào)用的屬性必須是線程安全的。因為你是在另外一個隊列中調(diào)用這個方法,如果這個方法在你的視圖類中,那么就很危險了。另外一個方法是創(chuàng)建一個單獨的繪制類,設(shè)置后所有屬性后,然后僅僅觸發(fā)繪制圖片的方法。
所有的UIKit繪制API都可以在其他的隊列中調(diào)用,但是要保證所有UIGraphicsBeginImageContextWithOptions和UIGraphicsEndIamgeContext中的所有代碼都在同一個操作中。
可以通過如下的方式來觸發(fā)繪制:

UIImageView *view; // assume we have this
NSOperationQueue *renderQueue; // assume we have this
CGSize size = view.bounds.size;
[renderQueue addOperationWithBlock:^(){
    UIImage *image = [renderer renderInImageOfSize:size];
    [[NSOperationQueue mainQueue] addOperationWithBlock:^(){
        view.image = image;
    }];
}];

注意這里的view.image = image只能在主線程中調(diào)用。
通常,多線程變成會增加復(fù)雜性,我們還需要考慮將取消掉某些后臺繪制,設(shè)置并線隊列中允許執(zhí)行的最大繪制數(shù)量。
為了便于便于,最簡單的方法是使用NSOperation的子類來實現(xiàn)renderInImageOfSize方法。
最后,還需要指出的是在UITableViewCell中使用異步繪制也需要小心。當(dāng)圖像繪制好的時候,cell可能已經(jīng)被重用來顯示其他的數(shù)據(jù)了。

CALayer的七七八八

現(xiàn)在我們已經(jīng)直到CALayer關(guān)聯(lián)著GPU最終顯示的位圖,視圖有一個位圖緩存,存放著需要被手動繪制到界面上的位圖數(shù)據(jù)。
通常使用CALayer的時候,需要將其內(nèi)容設(shè)置為圖片,這樣做是為了讓Core Animation使用圖片的位圖數(shù)據(jù)來顯示在界面上,如果是JPEG或PNG圖片,Core Animation會進(jìn)行圖片的解壓并將數(shù)據(jù)上傳到GPU。
雖然還有一些其他類型的圖層。但是如果使用的是一般的CALayer,那么不要設(shè)置content,而是設(shè)置背景色,Core Animaiton就不需要上傳任何數(shù)據(jù)到GPU,就可以讓GPU在沒有任何像素數(shù)據(jù)的情況下進(jìn)行繪制。對于漸變圖層也是一樣,CPU可以不需要任何工作,GPU也不需要上傳任何像素數(shù)據(jù)就完成繪制

手動繪制的圖層

如果一個CALayer子類重寫了drawInContext或者它的代理方法drawLayer:inContext:,Core Animation會為其分配一個位圖緩存來存放該方法繪制的位圖數(shù)據(jù),這個方法內(nèi)部的代碼需要在CPU上執(zhí)行,然后將結(jié)果上傳到GPU。

形狀和文字圖層

對于圖形和文字圖層,情況會有些不同。這種圖層Core Animation都會為其分配位圖緩存存放生成的位圖數(shù)據(jù)。Core Animation會在位圖緩存中繪制圖形或者文字,這與我們重寫drawInContext方法然后在其內(nèi)部繪制圖形或者文字的情況非常相同。效率也基本相同。
當(dāng)需要更新圖形或者文字圖層的時候,圖層的位圖緩存會被更新,Core Animation會重新繪制該緩存內(nèi)的內(nèi)容,比如在對形狀圖層的大小進(jìn)行一個動畫效果,Core Animation不得不在動畫的每一幀中重新繪制該形狀。

異步繪制

CALayer有一個drawsAsynchronously的屬性,看起來好像是解決所有問題的銀彈,但是,它可能提升性能,也可能是性能的噩夢。
當(dāng)設(shè)置了drawsAsynchronously為YES的時候,drawRect或者drawInContext方法仍然會在主線程中調(diào)用,但是所有對Core Graphics的調(diào)用什么都不會做(所有UIKit的圖形API,也都最終調(diào)用的是Core Graphics),這些繪制命令會延遲然后切到后臺異步執(zhí)行。
也就是說所有的繪圖命令只是被記錄下來,然后稍后在后臺隊列執(zhí)行,為了實現(xiàn)這種方式,系統(tǒng)需要做大量的工作,但是很多工作被從主線程中移除掉了,最好進(jìn)行測試和比較再確定最終方案。
通常對于非常復(fù)雜的繪制方法可以提升性能,但是對于比較簡單的繪制未必起作用。

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

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

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