iOS 圖形效能優化

年糕媽媽技術團隊發表於2018-11-19

引言

當一個產品漸漸成熟,我們便開始重視產品效能的優化。而這其中圖形效能的優化在iOS客戶端佔比較重要的部分。這裡我們將介紹Core Animation的執行機制,首先我們不要被它的名字誤導了,Core Animation不是隻用來做動畫的,iOS檢視的顯示都是通過它來完成的,所以我們想要優化圖形效能必須瞭解Core Animation。下面我們根據蘋果WWDC視訊講解來認識Core Animation工作機制,據此分析具體卡頓的原因,如何避免這些問題造成的卡頓,並且結合實際情況說明從哪些方面優化可以事半功倍。

Core Animation 工作機制

1.png-c
如上圖所示,Core Animation在App將圖層資料提交到應用外程式Render Server,這是Core Animation的服務端,把資料解碼成GPU可執行的指令交給GPU執行。可以看出一個問題渲染服務並不是在App程式內進行的,也就是說渲染部分我們無法進行優化,我們可以優化的點只能在第一個提交事務的階段。那麼這個階段Core Animation到底做了什麼呢?下面我們一起來看看!

Commit Transaction

提交事務分為四個階段:佈局、顯示、準備、提交。

Core Animation.png-c

  • 佈局階段 當呼叫addSubview時layer被加入到layer tree中,layoutSubviews被呼叫,建立view。同時還會進行資料查詢,例如app做了本地化,label要顯示這些本地化字串必須從本地化檔案中查詢到對應語言的佈局,這就涉及了I/O操作。所以這裡主要是CPU工作,而瓶頸也會是CPU。
  • 顯示階段 在這個階段如果你重寫了drawRect方法,Core Graphics會進行繪製渲染工作。為檢視繪製寄宿圖即contents。但是drawRect裡繪製的內容不會立即顯示出來,而是先備換竄起來,等需要的時候被更新到螢幕上。如手動呼叫setNeedsDisplay或sizeThatFits被呼叫,也可以設定cententMode屬性值為UIViewContentModeRedraw當每次bounds改變會自動呼叫setNeedsDisplay方法。這個階段主要是CPU和記憶體的消耗,很多人喜歡用Core Graphics的方法來繪製圖形,認為可以提高效能,後面我們會說明這個方法的弊端。
  • 準備階段 這裡的工作主要是圖片的解碼,因為大部分都是編碼後的圖片,要讀取原始資料必須經過編碼過程。並且當我們使用了iOS不支援的圖片格式,即不支援硬編碼,就需要進行轉化工作,也是比較耗時的。所以這裡就是GPU消耗,如果進行軟解碼也要消耗CPU。
  • 提交階段 最後一個階段負責打包圖層資料併傳送到我們上面說的渲染服務中。這個過程是一個遞迴操作,圖層樹越複雜越是需要消耗更多資源。像CALaler有很多隱式動畫屬性也會在這裡提交,省去了多次動畫屬性程式間的互動,提高了效能。

優化

根據上面我們所提到4個階段,我們看看哪些因素會影響到App的效能,並且如何優化可以提高我們App的效能。

混合

平時我們寫程式碼的時候,往往會給不同的CALayer新增不同的顏色,不同的透明度,我們最後看到是所有這些層CALayer混合出的結果。

那麼在iOS中是如何進行混合的?前面我們說明了每個畫素都包含了R(紅)、G(綠)、B(藍)和R(透明度),GPU要計算每個畫素混合來的RGB值。那麼如何計算這些顏色的混合值呢?假設在正常混合模式下,並且是畫素對齊的兩個CALayer,混合計算公式如下:

R = S + D * ( 1 – Sa )
複製程式碼

蘋果的文件中有對每個引數的解釋:

The blend mode constants introduced in OS X v10.5   represent the Porter-Duff blend modes. The symbols in the   equations for these blend modes are:

    * R is the premultiplied result

    * S is the source color, and includes alpha

    * D is the destination color, and includes alpha

    * Ra, Sa, and Da are the alpha components of R, S, and D
複製程式碼

R就是得到的結果色,S和D是包含透明度的源色和目標色,其實就是預先乘以透明度後的值。Sa就是源色的透明度。iOS為我們提供了多種的Blend mode:

 /* Available in Mac OS X 10.5 & later. R, S, and D are, respectively,
   premultiplied result, source, and destination colors with alpha; Ra,
   Sa, and Da are the alpha components of these colors.

   The Porter-Duff "source over" mode is called `kCGBlendModeNormal':
     R = S + D*(1 - Sa)

   Note that the Porter-Duff "XOR" mode is only titularly related to the
   classical bitmap XOR operation (which is unsupported by
   CoreGraphics). */

kCGBlendModeClear,                  /* R = 0 */
kCGBlendModeCopy,                   /* R = S */
kCGBlendModeSourceIn,               /* R = S*Da */
kCGBlendModeSourceOut,              /* R = S*(1 - Da) */
kCGBlendModeSourceAtop,             /* R = S*Da + D*(1 - Sa) */
kCGBlendModeDestinationOver,        /* R = S*(1 - Da) + D */
kCGBlendModeDestinationIn,          /* R = D*Sa */
kCGBlendModeDestinationOut,         /* R = D*(1 - Sa) */
kCGBlendModeDestinationAtop,        /* R = S*(1 - Da) + D*Sa */
kCGBlendModeXOR,                    /* R = S*(1 - Da) + D*(1 - Sa) */
kCGBlendModePlusDarker,             /* R = MAX(0, (1 - D) + (1 - S)) */
kCGBlendModePlusLighter             /* R = MIN(1, S + D) */
複製程式碼

似乎計算也不是很複雜,但是這只是一個畫素覆蓋另一個畫素簡單的一步計算,而正常情況我們現實的介面會有非常多的層,每一層都會有百萬計的畫素,這都要GPU去計算,負擔是很重的。

畫素對齊

畫素對齊就是檢視上畫素和螢幕上的物理畫素完美對齊。上面我們說混合的時候,假設的情況是多個layer是在每個畫素都完全對齊的情況下來進行計算的,如果畫素不對齊的情況下,GPU需要進行Anti-aliasing反抗鋸齒計算,GPU的負擔就會加重。畫素對齊的情況下,我們只需要把所有layer上的單個畫素進行混合計算即可。

那麼什麼原因造成畫素不對齊?主要有兩點:

  1. 圖片大小和UIImageView大小不符合2倍3倍關係時,如一張12x12二倍,18x18三倍的圖,UIimageView的size為6x6才符合畫素對齊。
  2. 邊緣畫素不對齊,即起始座標不是整數,可以使用CGRectIntegral()方法去除小數位。 這兩點都有可能造成畫素不對齊。如果想獲得更好的圖形效能,作為開發者要儘可能得避免這兩種情況。

不透明

上面我們說過一個混合計算的公式:

R = S + D * ( 1 – Sa )
複製程式碼

如果Sa值為1,也就是源色對應的畫素不透明。那麼得到R = S,這樣就只需要拷貝最上層的layer,不需要再進行復雜的計算了。因為下面層的layer全部是可不見的,所以GPU無需進行混合計算了。如何讓GPU知道這個影象是不透明的呢?如果使用的是CALayer,那麼要把opaque屬性設定成YES(預設是NO)。而若只用的是UIView,opaque預設屬性是YES。當GPU知道是不透明的時候,只會做簡單的拷貝工作,避免了複雜的計算,大大減輕了GPU的工作量。

如果載入一個沒有alpha通道的圖片,opaque屬性會自動設定為YES。但是如果是一個每個畫素alpha值都為100%的圖片,儘管此圖不透明但是Core Animation依然會假定是否存在alpha值不為100%的畫素。

解碼

上一篇文章我們有說到,一般在Core Animation準備階段,會對圖片進行解碼操作,即把壓縮的影象解碼成點陣圖資料。這是一個很消耗CPU的事情。系統是在圖片將要渲染到螢幕之前再進行解碼,而且預設是在主執行緒中進行的。所以我們可以將解碼放在子執行緒中進行,下面簡單列舉一種解碼方式:

NSString *picPath = [[NSBundle mainBundle] pathForResource:@"tests" ofType:@"png"];
NSData *imageData = [NSData dataWithContentsOfFile:picPath];//讀取未解碼圖片資料
        
CGImageSourceRef imageSourceRef = CGImageSourceCreateWithData((__bridge CFTypeRef)imageData, NULL);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSourceRef, 0, (CFDictionaryRef)@{(id)kCGImageSourceShouldCache:@(NO)});
CFRelease(imageSourceRef);
size_t width = CGImageGetWidth(imageRef);//獲取圖片寬度
size_t height = CGImageGetHeight(imageRef);//獲取圖片高度
CGColorSpaceRef colorSpace = CGImageGetColorSpace(imageRef);
        
size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);//每個顏色元件佔的bit數
size_t bitsPerPixel = CGImageGetBitsPerPixel(imageRef);//每個畫素佔幾bit
size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);//點陣圖資料每行佔多少bit
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
        
CGDataProviderRef dataProvider = CGImageGetDataProvider(imageRef);
CFRelease(imageRef);
CFDataRef dataRef = CGDataProviderCopyData(dataProvider);//獲得解碼後資料
CGDataProviderRef newProvider = CGDataProviderCreateWithCFData(dataRef);
CFRelease(dataRef);
        
CGImageRef newImageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpace, bitmapInfo, newProvider, NULL, false, kCGRenderingIntentDefault);
CFRelease(newProvider);
        
UIImage *image = [UIImage imageWithCGImage:newImageRef scale:2.0 orientation:UIImageOrientationUp];
CFRelease(newImageRef);
複製程式碼

另外,在iOS7之後蘋果提供了一個屬性kCGImageSourceShouldCacheImmediately,在CGImageSourceCreateImageAtIndex方法中,設定kCGImageSourceShouldCacheImmediatelykCFBooleanTrue的話可以立刻開始解壓縮,預設為kCFBooleanFalse。當然也像AFNetworking 中使用void CGContextDrawImage(CGContextRef __nullable c, CGRect rect, CGImageRef __nullable image)方法也可以實現解碼,具體實現不在此贅述。

位元組對齊

我們前面說畫素對齊時,簡單介紹了位元組對齊。那麼到底什麼是位元組對齊?為什麼要位元組對齊?和我們優化圖形效能有什麼關係呢?

位元組對齊是對基本資料型別的地址做了一些限制,即某種資料型別物件的地址必須是其值的整數倍。例如,處理器從記憶體中讀取一個8個位元組的資料,那麼資料地址必須是8的整數倍。

對齊是為了提高讀取的效能。因為處理器讀取記憶體中的資料不是一個一個位元組讀取的,而是一塊一塊讀取的一般叫做cache lines。如果一個不對齊的資料放在了2個資料塊中,那麼處理器可能要執行兩次記憶體訪問。當這種不對齊的資料非常多的時候,就會影響到讀取效能了。這樣可能會犧牲一些儲存空間,但是對提升了記憶體的效能,對現代計算機來說是更好的選擇。

在iOS中,如果這個影象的資料沒有位元組對齊,那麼Core Animation會自動拷貝一份資料做對齊處理。這裡我們可以提前做好位元組對齊。在方法CGBitmapContextCreate(void * __nullable data, size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, CGColorSpaceRef __nullable space, uint32_t bitmapInfo)中,有一個引數bytesPerRow,意思是指定要使用的點陣圖每行記憶體的位元組數,ARMv7架構的處理器的cache lines是32byte,A9處理器的是64byte,這裡我們要使bytesPerRow為64的整數倍。具體可以參考官方文件Quartz 2D Programming GuideWWDC 2012 Session 238 "iOS App Performance: Graphics and Animations"。位元組對齊,在一般情況下,感覺對效能的影響很小,沒必要的情況不要過早優化。

離屏渲染

離屏渲染(Off-Screen Rendering)是指GPU在當前螢幕緩衝區以外新開闢一個緩衝區進行渲染操作。離屏渲染是很消耗效能的,因為首先要建立螢幕外緩衝區,還要進行兩次上下文環境切換。先切換到螢幕外環境,離屏渲染完成後再切換到當前螢幕,上下文的切換是很高昂的消耗。產生離屏渲染的原因就是這些圖層不能直接繪製在螢幕上,必須進行預合成。

產生離屏渲染的情況大概有幾種: 1.cornerRadiusmasksToBounds(UIView中是clipToBounds)一起使用的時候,單獨使用不會觸發離屏渲染。cornerRadius只對背景色起作用,所以有contents的圖層需要對其進行裁剪。 2.為圖層設定mask(遮罩)。 3.layer的allowsGroupOpacity屬性為YES且opacity小於1.0,GroupOpacity是指子圖層的透明度值不能大於父圖層的。 4.設定了shadow(陰影)。

上面這幾種情況都是GPU的離屏渲染,還有一種特殊的CPU離屏渲染。只要實現Core Graphics繪製API會產生CPU的離屏渲染。因為它也不是直接繪製到螢幕上的,而且先建立螢幕外的快取。

我們如何解決這幾個產生離屏渲染的問題呢?首先,GroupOpacity對效能幾乎沒有影響,在此就不多說了。圓角是一個無法避免的,網上有很多例子是用Core Graphics繪製來代替系統圓角的,但是Core Graphics是一種軟體繪製,利用的是CPU,效能上要差上不少。當然在CPU利用率不是很高的介面是個不錯的選擇,但是有時候某個介面可能需要CPU去做其他消耗很大的事情,如網路請求。這個時候時候在用Core Graphics繪製大量的圓角圖形就有可能出現掉幀。這種情況怎麼辦呢?最好的就是設計師直接提供圓角影象。還有一種折中的方法就是在混合圖層,在原圖層上覆蓋一個你要的圓角形狀的圖層,中間需要顯示的部分是透明的,覆蓋的部分和周圍背景一致。

對於shadow,如果圖層是個簡單的幾何圖形或者圓角圖形,我們可以通過設定shadowPath來優化效能,能大幅提高效能。示例如下:

imageView.layer.shadowColor = [UIColor grayColor].CGColor;
imageView.layer.shadowOpacity = 1.0;
imageView.layer.shadowRadius = 2.0;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.frame];
imageView.layer.shadowPath = path.CGPath;
複製程式碼

我們還可以通過設定shouldRasterize屬性值為YES來強制開啟離屏渲染。其實就是光柵化(Rasterization)。既然離屏渲染這麼不好,為什麼我們還要強制開啟呢?當一個影象混合了多個圖層,每次移動時,每一幀都要重新合成這些圖層,十分消耗效能。當我們開啟光柵化後,會在首次產生一個點陣圖快取,當再次使用時候就會複用這個快取。但是如果圖層發生改變的時候就會重新產生點陣圖快取。所以這個功能一般不能用於UITableViewCell中,cell的複用反而降低了效能。最好用於圖層較多的靜態內容的圖形。而且產生的點陣圖快取的大小是有限制的,一般是2.5個螢幕尺寸。在100ms之內不使用這個快取,快取也會被刪除。所以我們要根據使用場景而定。

Instruments

上面我們說了這麼多效能相關的因素,那麼我們怎麼進行效能的測試,怎麼知道哪些因素影響了圖形效能?蘋果很人性得為我們提供了一個測試工具Instruments。可以在Xcode->Open Develeper Tools->Instruments中找到,我們看到這裡面有很多的測試工具,像大家可能常用的檢測記憶體洩漏的Leaks,在這裡我們就討論下Core Animation這個工具的使用。

Core Animation工具用來監測Core Animation效能。提供可見的FPS值。並且提供幾個選項來測量渲染效能,下面我們來說明每個選項的能: Color Blended Layers:這個選項如果勾選,你能看到哪個layer是透明的,GPU正在做混合計算。顯示紅色的就是透明的,綠色就是不透明的。

Color Hits Green and Misses Red:如果勾選這個選項,且當我們程式碼中有設定shouldRasterize為YES,那麼紅色代表沒有複用離屏渲染的快取,綠色則表示複用了快取。我們當然希望能夠複用。

Color Copied Images:按照官方的說法,當圖片的顏色格式GPU不支援的時候,即不是32bit的顏色格式,Core Animation會 拷貝一份資料讓CPU進行轉化。例如從網路上下載了8bit的顏色格式的圖片,則需要CPU進行轉化,這個區域會顯示成藍色。還有一種情況會觸發Core Animation的copy方法,就是位元組不對齊的時候。

Color Misaligned Images:勾選此項,如果圖片需要縮放則標記為黃色,如果沒有畫素對齊則標記為紫色。畫素對齊我們已經在上面有所介紹。

Color Offscreen-Rendered Yellow:用來檢測離屏渲染的,如果顯示黃色,表示有離屏渲染。當然還要結合Color Hits Green and Misses Red來看,是否複用了快取。

Color OpenGL Fast Path Blue:這個選項對那些使用OpenGL的圖層才有用,像是GLKView或者 CAEAGLLayer,如果不顯示藍色則表示使用了CPU渲染,繪製在了螢幕外,顯示藍色表示正常。

Flash Updated Regions:當對圖層重繪的時候回顯示黃色,如果頻繁發生則會影響效能。可以用增加快取來增強效能。官方文件Improving Drawing Performance有所說明。

總結

結合前面兩章內容,我們發現,一個簡單的圖片顯示在螢幕上,要經過很多步驟,並且有許多硬體的參與。最主要的就是CPU和GPU,協調他們之間的工作是高效能得關鍵。

因為圖形的效能和兩者都有關係,CPU主要負責軟解碼、I/O相關、佈局的計算等工作,如果使用Core Graphics繪圖API那麼也會用到CPU。GPU的主要責任就是合成渲染。為了能夠得到最好的效能,我們就要找出是哪個限制了效能,CPU過度利用還是GPU負擔太大。通過蘋果給出的Instruments裡面的測試工具,我們在真機上一次次的測試,才能正確的判斷出無法保證畫面60FPS的原因。必須平衡兩者,才能達到最好的效能。

下面我們總結幾個優化點: 1.儘量使用iOS優化處理的圖片格式,減少CPU軟解碼的負擔。 2.能不透明的不要使用透明度,減少混合計算。 3.不要讓圖層過於複雜,不然增加了處理圖層,打包傳送到渲染服務的工作量,GPU渲染負擔也會增大。 4.最好不要使用離屏渲染,必須使用的話最好能夠複用快取,離屏渲染對效能影響是最大的。 5.佈局不要過於複雜,如果必須要複雜的佈局,可以提前快取佈局資料。 6.不要濫用多執行緒,因為建立和銷燬執行緒不僅增加CPU任務量,而且會消耗記憶體。

最後需要說明的就是不要過早和過度得優化,過猶不及。過早優化得不償失,反而耗時耗力。過度優化有時候適得其反。

視訊參考:WWDC 2015’s session 233:Advanced Touch Input on iOS

文件參考:objccn

相關文章