1. 程式人生 > >使用多張圖片做幀動畫的效能優化

使用多張圖片做幀動畫的效能優化

背景

QQ群的送禮物功能需要載入幾十張圖然後做幀動畫,但是多張圖片載入造成了非常大的效能開銷,導致圖片開始載入到真正播放動畫的時間間隔比較長。所以需要研究一些優化方案提升載入圖片和幀動畫的效能。

原理分析

iOS系統從磁碟載入一張圖片,使用UIImageView顯示到螢幕上,需要經過以下步驟:

  1. 從磁碟拷貝圖片資料到核心緩衝區。

  2. 從核心緩衝區複製資料到使用者空間。

  3. 生成UIImageView,把影象資料賦值給UIImageView。

  4. 如果影象資料為未解碼的PNG/JPG,解碼為點陣圖資料。

  5. CATransaction捕獲到UIImageView layer樹的變化,主執行緒Runloop提交CATransaction,開始進行影象渲染。如果資料沒有位元組對齊,Core Animation會再拷貝一份資料,進行位元組對齊。GPU處理點陣圖資料,進行渲染。

載入圖片

載入圖片和圖片解碼是比較容易影響效能的一些因素。iOS裝置上的快閃記憶體雖然是非常快的,但是還是要比記憶體慢接近200倍左右。圖片載入的效能取決於CPU和IO,所以適當的減少IO次數可以提升一部分效能。

通常情況下,應用應該在使用者不會察覺的時候載入圖片,可以針對情況採用預載入圖片或者延遲載入。如果是幀動畫這種情況,圖片很難做延遲載入,因為幀動畫的時間比較短,延遲載入很難保證播放每一幀時能提前載入到圖片。有些比如列表滑動之類的情況延遲載入就會比較合理,並且可以採用子執行緒非同步之類的方法防止滑動卡頓。

解碼

圖片載入結束之後在被渲染到螢幕之前,如果是未解碼的JPEG或者PNG格式,圖片會先被解碼為點陣圖資料。經過實際的測試,圖片解碼通常要比圖片載入耗費更多的時間。iOS預設會在主執行緒對影象進行解碼。很多庫都解決了影象解碼的問題,不過由於解碼後的影象太大,一般不會快取到磁碟,SDWebImage的做法是把解碼操作從主執行緒移到子執行緒,讓耗時的解碼操作不佔用主執行緒的時間。

解碼與載入圖片耗時對比

測試機型:iPhone 6+

—- 只加載不解碼平均時長 載入和解碼平均時長
同一張圖載入三十次 0.000858531 0.005955906
載入三十張不同的圖 0.002828871 0.015458194

解碼的時間通常是載入影象資料時間的三到四倍左右。

各種載入圖片API的解碼的時機

UIKit:

+imageNamed://載入到原圖後立刻進行解碼
+imageWithContentsOfFile://影象渲染前進行解碼

UIImageView的image被賦值時會立刻進行解碼。

影象用UIKit內的繪圖API繪製時會立刻進行解碼,這個API的好處是可以在子執行緒進行。

ImageIO:

NSURL *imageURL = [NSURL fileURLWithPath:str];
NSDictionary *options = @{(__bridge id)kCGImageSourceShouldCache: @YES, (__bridge id)kCGImageSourceShouldCacheImmediately: @NO};
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)imageURL, NULL);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0,(__bridge CFDictionaryRef)options);
UIImage *image = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CFRelease(source);

kCGImageSourceShouldCacheImmediately 決定是否會在載入完後立刻開始解碼。

快取

快取分為原影象的快取和解碼後點陣圖資料的快取。
一般情況下,解碼後的點陣圖資料的快取會跟隨著原圖UIImage,並且UIImage被拷貝後,指標變了之後影象需要重新去解碼。

解碼後的點陣圖資料可以通過以下的API獲取。

1.CGImageSourceCreateWithData(data) 建立 ImageSource。
2.CGImageSourceCreateImageAtIndex(source) 建立一個未解碼的 CGImage。
3.CGImageGetDataProvider(image) 獲取這個圖片的資料來源。
4.CGDataProviderCopyData(provider) 從資料來源獲取直接解碼的資料。
各種載入圖片API的快取策略

UIKit:

+imageNamed://原圖和解碼後的點陣圖資料都會儲存在系統快取下,只有在記憶體低之類的時候才會被釋放。所以這個API適合在應用多次使用的圖片上使用。
+imageWithContentsOfFile://不會對原圖做快取,只用一次的圖片應該使用這個API。

ImageIO:

ImageIO的API的kCGImageSourceShouldCache選項決定了是否會對解碼後的點陣圖資料做快取。64位裝置上預設為開,32位裝置上預設為關。

任何時候,選取如何快取總是一件比較難的事,正如菲爾 卡爾頓曾經說過:“在電腦科學中只有兩件難事:快取和命名”。

解碼後點陣圖資料的快取不好控制,我們一般也不會去操作它,但是原影象還是可以做一些快取的。除了使用系統API來做快取以外,應用也可以自定義一些快取策略。或者可以使用NSCache。

NSCache的API和NSDictionary很像,並且會根據所儲存資料的使用頻率,記憶體佔用情況會適時的釋放掉一其中一部分資料。

圖片格式

常用的圖片格式一般分為PNG和JPEG。

對於PNG圖片來說,載入會比JPEG更長,因為檔案可能更大,但是解碼會相對較快,而且Xcode會把PNG圖片進行解碼優化之後引入工程。JPEG圖片更小,載入更快,但是解壓的步驟要消耗更長的時間,因為JPEG解壓演算法比基於zip的PNG演算法更加複雜。

JPEG 對於噪點大的圖片效果比較好,PNG適合鋒利的線條或者漸變色的圖片。對於不友好的png圖片,相同畫素的JPEG圖片總是比PNG載入更快,除非一些非常小的圖片、但對於友好的PNG圖片,一些中大尺寸的圖效果還是很好的

本文測試時使用的所有圖片均為PNG格式。

渲染

關於影象的渲染,主要從以下三點分析:

  • offscreen rendring

  • Blending

  • Rasterize

Offscreen rendering指的是在影象在繪製到當前螢幕前,需要先進行一次渲染,之後才繪製到當前螢幕。在進行offscreen rendring的時候,顯示卡需要另外alloc一塊記憶體來進行渲染,渲染完畢後在繪製到當前螢幕,而且對於顯示卡來說,onscreen到offscreen的上下文環境切換是非常昂貴的(涉及到OpenGL的pipelines和barrier等),

會造成offscreen rendring的操作有:

  • layer.mask 的使用

  • layer.maskToBounds 的使用

  • layer.allowsGroupOpacity 設定為yes 和 layer.opacity 小於1.0

  • layer.shouldRasterize 設定為yes

  • layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing

Blending 會導致效能的損失。在iOS的圖形處理中,Blending主要指的是混合畫素顏色的計算。最直觀的例子就是,我們把兩個圖層疊加在一起,如果第一個圖層的透明的,則最終畫素的顏色計算需要將第二個圖層也考慮進來。這一過程即為Blending。更多的計算,導致效能的損失,在一些不需要透明度的地方,可以設定alpha為1.0 或者減少圖層的疊加。

Rasterize啟用shouldRasterize屬性會將圖層繪製到一個螢幕之外的影象。然後這個影象將會被快取起來並繪製到實際圖層的contents和子圖層。如果有很多的子圖層或者有複雜的效果應用,這樣做就會比重繪所有事務的所有幀划得來得多。但是光柵化原始影象需要時間,而且還會消耗額外的記憶體。
當我們使用得當時,光柵化可以提供很大的效能優勢但是一定要避免作用在內容不斷變動的圖層上,否則它快取方面的好處就會消失,而且會讓效能變的更糟。

優化點

載入

應用可以通過減少IO次數來優化影象載入效能。這時候就可以使用精靈序列來把多張小圖合成一張大圖。 這樣就能極大的減少IO次數了。

因為IOS裝置的限制,大圖的大小不能超過2048 2048,有部分裝置上這個限制可以達到4096 4096,但我們這裡以小的為準。所以一張大圖內最多隻能容納10張測試用的小圖。

精靈序列這種技術在cocos2d-x等平臺上使用的很多。把多張小圖拼成一張大圖時可以使用TexturePacker軟體來完成。

精靈序列


簡單來說精靈序列就是把多張小圖拼成一張大圖,附加一份plist檔案儲存著每一張小圖對應在大圖上的位置。然後iOS上就可以通過如下程式碼在不同小圖之間切換。

layer.contents = (id)img_.CGImage;//img為對應的大圖
layer.contentsRect = CGRectMake(0.1, 0.1, 0.2, 0.2);//contentsRect就是對應大圖上小圖的位置,更改這個值就可以在不同的小圖間切換了。
精靈序列與普通幀動畫的效能對比

下面的效能對比是通過20張小圖和小圖拼接而成的2張大圖完成一組動畫的對比資料。 測試機型是iPhone4s.

1.檔案大小

小圖總大小 大圖總大小
758K 827K

因為大圖內很難平鋪所有小圖,所以大圖內很容易有空白畫素,這就導致拼接後大圖的總解析度很容易超過所有小圖的總解析度,也就造成大圖的體積會比所有小圖的體積大。但是也有大圖的體積小於所有小圖總體積的情況,因為TexturePacker在拼接的時候會把小圖內空白的畫素適當的做點裁剪,然後把這個偏移值儲存在plist檔案內。

2.載入速度

小圖的載入時間 大圖的載入時間
≈25ms ≈5ms

一張大圖能容納10張小圖,極大的減小了IO數量,所以載入速度能大大提升。

3.解碼時間

小圖的解碼時間 大圖的解碼時間
≈35ms ≈40ms

因為大圖的資料量比較大,所以大圖的總體解碼時間要比小圖的解碼時間長。

4.CPU佔用(CPU佔用是通過同時執行兩種動畫,然後分別計算出兩種動畫的CPU佔用率)

小圖方案的CPU佔用率 大圖方案的CPU佔用率
≈8% 峰值比較高 ≈20% 峰值比較低

佔用CPU最多的一個是從本地載入資料,另一個是圖片解碼。因為大圖的IO次數比較少,所以載入大圖時的CPU佔用率要比載入所有小圖時的低,但是大圖的解碼和兩張大圖切換時比較耗費CPU。總體來說大圖這種方案的CPU佔用率要高一點。

5.記憶體佔用

小圖方案的記憶體佔用 大圖方案的記憶體佔用
≈27MB ≈30MB

6.幀率

小圖方案的幀率 大圖方案的幀率
≈7fps ≈7fps

大圖方案和小圖方案的幀率基本一致。

精靈序列總結

總體來說精靈序列這種方案能明顯減小圖片開始載入到動畫開始播放的延遲。但是精靈序列的檔案大小容易變大,並且CPU佔用率也要高一點。

檔案大小 載入速度 解碼時間 CPU佔用 記憶體佔用
通過大圖實現的精靈序列
通過小圖實現的普通幀動畫

精靈序列這種方案或許也可以直接用解碼後的資料作為原圖資料,這樣雖然會讓記憶體佔用更高,檔案大小進一步加大,但是能降低解碼時間和CPU佔用率。這個可以作為一個優化點繼續研究一下。

精靈序列的風險點:

  1. 因為一張大圖最多能容納10張小圖,所以在兩張大圖切換的時候會造成CPU佔用變高,這樣就會有造成卡頓的風險。但是在iPhone4s上的測試下基本沒有出現過卡頓。

  2. 拼接後的大圖的檔案大小容易變大,如果大圖通過網路下載時耗時會變長。

延遲解碼

根據上文的原理分析,針對多圖幀動畫,應用可以將圖片解碼延遲到圖片渲染前,不要讓圖片載入後立馬開始解碼。這樣也能降低圖片載入到動畫開始播放的延遲。

快取

因為QQ群的送禮物功能中同一副幀動畫重複播放的頻率並不高,所以不需要考慮對原圖或者對解碼後點陣圖資料做快取。

渲染

渲染圖形方面應用只能在將需要繪製的內容提交給GPU前做一些優化。針對幀動畫這種場景,我們可以通過保證影象素材做到畫素對其,儘量減少透明畫素來做一些優化。

如果使用精靈序列來做幀動畫,應用必須通過CALayer進行渲染。播放幀動畫時可以用NSTimer也可以使用CADisplayLink來不斷的重新整理影象資料。

如果不使用精靈序列,應用也可以通過自定義一個UIImageView,自己通過CADisplayLink或者NSTimer實現幀動畫,這樣的話應用就可以自由控制影象解碼的時間,影象資料的快取等。

總結

本文通過對圖片載入,多圖做幀動畫進行了一些原理分析,並且給出了一些優化點。也簡單介紹了一下用精靈序列做幀動畫的方案。除了QQ群送禮物的功能外其他通過圖片做幀動畫的功能也可以針對具體的業務情況選取其中的一些優化點進行一些優化。