tips:若是直接使用下面的方式加載圖片,圖片將會在視圖渲染在手機屏幕上時去解壓縮,即在主線程去解壓縮圖片,對性能消耗較大,所以產生了在子線程解壓縮的需求。html
UIImage *image = [UIImage imageWithContentsOfFile:@""]; UIImageView *imgView = [[UIImageView alloc] initWithImage:image]; [self.view addSubview:imgView];
對於大多數 iOS 應用來講,圖片每每是最佔用手機內存的資源之一,同時也是不可或缺的組成部分。將一張圖片從磁盤中加載出來,並最終顯示到屏幕上,中間其實通過了一系列複雜的處理過程,其中就包括了對圖片的解壓縮。git
圖片加載的工做流github
歸納來講,從磁盤中加載一張圖片,並將它顯示到屏幕上,中間的主要工做流以下:算法
1.假設咱們使用 +imageWithContentsOfFile: 方法從磁盤中加載一張圖片,這個時候的圖片並無解壓縮;數組
2.而後將生成的 UIImage 賦值給 UIImageView ;app
3.接着一個隱式的 CATransaction 捕獲到了 UIImageView 圖層樹的變化;ide
4.在主線程的下一個 run loop 到來時,Core Animation 提交了這個隱式的 transaction ,這個過程可能會對圖片進行 copy 操做,而受圖片是否字節對齊等因素的影響,這個 copy 操做可能會涉及如下部分或所有步驟:函數
分配內存緩衝區用於管理文件 IO 和解壓縮操做;oop
將文件數據從磁盤讀到內存中;佈局
將壓縮的圖片數據解碼成未壓縮的位圖形式,這是一個很是耗時的 CPU 操做;
最後 Core Animation 使用未壓縮的位圖數據渲染 UIImageView 的圖層。
在上面的步驟中,咱們提到了圖片的解壓縮是一個很是耗時的 CPU 操做,而且它默認是在主線程中執行的。那麼當須要加載的圖片比較多時,就會對咱們應用的響應性形成嚴重的影響,尤爲是在快速滑動的列表上,這個問題會表現得更加突出。
爲何須要解壓縮
既然圖片的解壓縮須要消耗大量的 CPU 時間,那麼咱們爲何還要對圖片進行解壓縮呢?是否能夠不通過解壓縮,而直接將圖片顯示到屏幕上呢?答案是否認的。要想弄明白這個問題,咱們首先須要知道什麼是位圖:
「A bitmap image (or sampled image) is an array of pixels (or samples). Each pixel represents a single point in the image. JPEG, TIFF, and PNG graphics files are examples of bitmap images.」
其實,位圖就是一個像素數組,數組中的每一個像素就表明着圖片中的一個點。咱們在應用中常常用到的 JPEG 和 PNG 圖片就是位圖。下面,咱們來看一個具體的例子,這是一張 PNG 圖片,像素爲 30?×?30 ,文件大小爲 843B :
咱們使用下面的代碼:
1 2 |
|
就能夠獲取到這個圖片的原始像素數據,大小爲 3600B :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
也就是說,這張文件大小爲 843B 的 PNG 圖片解壓縮後的大小是 3600B ,是原始文件大小的 4.27 倍。那麼這個 3600B 是怎麼得來的呢?與圖片的文件大小或者像素有什麼必然的聯繫嗎?事實上,解壓縮後的圖片大小與原始文件大小之間沒有任何關係,而只與圖片的像素有關:
1 |
|
至於這個公式是怎麼得來的,咱們後面會有詳細的說明,如今只須要知道便可。
至此,咱們已經知道了什麼是位圖,而且直觀地看到了它的原始像素數據,那麼它與咱們常常提到的圖片的二進制數據有什麼聯繫嗎?是同一個東西嗎?事實上,這兩者是徹底獨立的兩個東西,它們之間沒有必然的聯繫。爲了加深理解,我把這個圖片拖進 Sublime Text 2 中,獲得了這個圖片的二進制數據,大小與原始文件大小一致,爲 843B :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
事實上,不論是 JPEG 仍是 PNG 圖片,都是一種壓縮的位圖圖形格式。只不過 PNG 圖片是無損壓縮,而且支持 alpha 通道,而 JPEG 圖片則是有損壓縮,能夠指定 0-100% 的壓縮比。值得一提的是,在蘋果的 SDK 中專門提供了兩個函數用來生成 PNG 和 JPEG 圖片:
1 2 3 4 5 |
|
所以,在將磁盤中的圖片渲染到屏幕以前,必須先要獲得圖片的原始像素數據,才能執行後續的繪製操做,這就是爲何須要對圖片解壓縮的緣由。
強制解壓縮的原理
既然圖片的解壓縮不可避免,而咱們也不想讓它在主線程執行,影響咱們應用的響應性,那麼是否有比較好的解決方案呢?答案是確定的。
咱們前面已經提到了,當未解壓縮的圖片將要渲染到屏幕時,系統會在主線程對圖片進行解壓縮,而若是圖片已經解壓縮了,系統就不會再對圖片進行解壓縮。所以,也就有了業內的解決方案,在子線程提早對圖片進行強制解壓縮。
而強制解壓縮的原理就是對圖片進行從新繪製,獲得一張新的解壓縮後的位圖。其中,用到的最核心的函數是 CGBitmapContextCreate :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
顧名思義,這個函數用於建立一個位圖上下文,用來繪製一張寬 width 像素,高 height 像素的位圖。這個函數的註釋比較長,參數也比較難理解,可是先彆着急,咱們先來了解下相關的知識,而後再回過頭來理解這些參數,就會比較簡單了。
Pixel Format
咱們前面已經提到了,位圖其實就是一個像素數組,而像素格式則是用來描述每一個像素的組成格式,它包括如下信息:
Bits per component :一個像素中每一個獨立的顏色份量使用的 bit 數;
Bits per pixel :一個像素使用的總 bit 數;
Bytes per row :位圖中的每一行使用的字節數。
有一點須要注意的是,對於位圖來講,像素格式並非隨意組合的,目前只支持如下有限的 17 種特定組合:
從上圖可知,對於 iOS 來講,只支持 8 種像素格式。其中顏色空間爲 Null 的 1 種,Gray 的 2 種,RGB 的 5 種,CMYK 的 0 種。換句話說,iOS 並不支持 CMYK 的顏色空間。另外,在表格的第 2 列中,除了像素格式外,還指定了 bitmap information constant ,咱們在後面會詳細介紹。
Color and Color Spaces
在上面咱們提到了顏色空間,那麼什麼是顏色空間呢?它跟顏色有什麼關係呢?在 Quartz 中,一個顏色是由一組值來表示的,好比 0, 0, 1 。而顏色空間則是用來講明如何解析這些值的,離開了顏色空間,它們將變得毫無心義。好比,下面的值都表示藍色:
若是不知道顏色空間,那麼咱們根本沒法知道這些值所表明的顏色。好比 0, 0, 1 在 RGB 下表明藍色,而在 BGR 下則表明的是紅色。在 RGB 和 BGR 兩種顏色空間下,綠色是相同的,而紅色和藍色則相互對調了。所以,對於同一張圖片,使用 RGB 和 BGR 兩種顏色空間可能會獲得兩種不同的效果:
是否是感受很是有意思呢?
Color Spaces and Bitmap Layout
咱們前面已經知道了,像素格式是用來描述每一個像素的組成格式的,好比每一個像素使用的總 bit 數。而要想確保 Quartz 可以正確地解析這些 bit 所表明的含義,咱們還須要提供位圖的佈局信息 CGBitmapInfo :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
它主要提供了三個方面的佈局信息:
alpha 的信息;
顏色份量是否爲浮點數;
像素格式的字節順序。
其中,alpha 的信息由枚舉值 CGImageAlphaInfo 來表示:
1 2 3 4 5 6 7 8 9 10 |
|
上面的註釋其實已經比較清楚了,它一樣也提供了三個方面的 alpha 信息:
是否包含 alpha ;
若是包含 alpha ,那麼 alpha 信息所處的位置,在像素的最低有效位,好比 RGBA ,仍是最高有效位,好比 ARGB ;
若是包含 alpha ,那麼每一個顏色份量是否已經乘以 alpha 的值,這種作法能夠加速圖片的渲染時間,由於它避免了渲染時的額外乘法運算。好比,對於 RGB 顏色空間,用已經乘以 alpha 的數據來渲染圖片,每一個像素均可以免 3 次乘法運算,紅色乘以 alpha ,綠色乘以 alpha 和藍色乘以 alpha 。
那麼咱們在解壓縮圖片的時候應該使用哪一個值呢?根據 Which CGImageAlphaInfo should we use 和官方文檔中對 UIGraphicsBeginImageContextWithOptions 函數的討論:
「You use this function to configure the drawing environment for rendering into a bitmap. The format for the bitmap is a ARGB 32-bit integer pixel format using host-byte order. If the opaque parameter is YES, the alpha channel is ignored and the bitmap is treated as fully opaque (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host). Otherwise, each pixel uses a premultipled ARGB format (kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host).」
咱們能夠知道,當圖片不包含 alpha 的時候使用 kCGImageAlphaNoneSkipFirst ,不然使用 kCGImageAlphaPremultipliedFirst 。另外,這裏也提到了字節順序應該使用 32 位的主機字節順序 kCGBitmapByteOrder32Host ,而這個值具體是什麼,咱們後面再討論。
至於顏色份量是否爲浮點數,這個就比較簡單了,直接邏輯或 kCGBitmapFloatComponents 就能夠了。更詳細的內容就不展開了,由於咱們通常用不上這個值。
接下來,咱們來簡單地瞭解下像素格式的字節順序,它是由枚舉值 CGImageByteOrderInfo 來表示的:
1 2 3 4 5 6 7 |
|
它主要提供了兩個方面的字節順序信息:
對於 iPhone 來講,採用的是小端模式,可是爲了保證應用的向後兼容性,咱們可使用系統提供的宏,來避免 Hardcoding :
1 2 3 4 5 6 7 |
|
根據前面的討論,咱們知道字節順序的值應該使用的是 32 位的主機字節順序 kCGBitmapByteOrder32Host ,這樣的話無論當前設備採用的是小端模式仍是大端模式,字節順序始終與其保持一致。
下面,咱們來看一張圖,它很是形象地展現了在使用 16 或 32 位像素格式的 CMYK 和 RGB 顏色空間下,一個像素是如何被表示的:
咱們從圖中能夠看出,在 32 位像素格式下,每一個顏色份量使用 8 位;而在 16 位像素格式下,每一個顏色份量則使用 5 位。
好了,瞭解完這些相關知識後,咱們再回過頭來看看 CGBitmapContextCreate 函數中每一個參數所表明的具體含義:
data :若是不爲 NULL ,那麼它應該指向一塊大小至少爲 bytesPerRow * height 字節的內存;若是 爲 NULL ,那麼系統就會爲咱們自動分配和釋放所需的內存,因此通常指定 NULL 便可;
width 和 height :位圖的寬度和高度,分別賦值爲圖片的像素寬度和像素高度便可;
bitsPerComponent :像素的每一個顏色份量使用的 bit 數,在 RGB 顏色空間下指定 8 便可;
bytesPerRow :位圖的每一行使用的字節數,大小至少爲 width * bytes per pixel 字節。有意思的是,當咱們指定 0 時,系統不只會爲咱們自動計算,並且還會進行 cache line alignment 的優化,更多信息能夠查看 what is byte alignment (cache line alignment) for Core Animation? Why it matters? 和 Why is my image’s Bytes per Row more than its Bytes per Pixel times its Width? ,親測可用;
space :就是咱們前面提到的顏色空間,通常使用 RGB 便可;
bitmapInfo :就是咱們前面提到的位圖的佈局信息。
到這裏,你已經掌握了強制解壓縮圖片須要用到的最核心的函數,點個贊。
開源庫的實現
接下來,咱們來看看在三個比較流行的開源庫 YYKit 、SDWebImage 和 FLAnimatedImage 中,對圖片的強制解壓縮是如何實現的。
首先,咱們來看看 YYKit 中的相關代碼,用於解壓縮圖片的函數 YYCGImageCreateDecodedCopy 存在於 YYImageCoder 類中,核心代碼以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
它接受一個原始的位圖參數 imageRef ,最終返回一個新的解壓縮後的位圖 newImage ,中間主要通過了如下三個步驟:
使用 CGBitmapContextCreate 函數建立一個位圖上下文;
使用 CGContextDrawImage 函數將原始位圖繪製到上下文中;
使用 CGBitmapContextCreateImage 函數建立一張新的解壓縮後的位圖。
事實上,SDWebImage 和 FLAnimatedImage 中對圖片的解壓縮過程與上述徹底一致,只是傳遞給 CGBitmapContextCreate 函數的部分參數存在細微的差異,以下表所示:
在上表中,用淺綠色背景標記的參數即爲咱們在前面的分析中所推薦的參數,用這些參數解壓縮後的圖片渲染的速度會更快。所以,從理論上說 YYKit 中的解壓縮算法是三者之中最優的。
性能對比
口說無憑,所以我編寫了一個小的測試程序,來簡單地對比一下這三個開源庫的解壓縮性能,源碼能夠在 GitHub 上找到。
採用的測試樣例分別爲 5 張 PNG 圖片和 5 張 JPEG 圖片,像素依次爲 128x96 、256x192 、512x384 、1024x768 和 2048x1536 ,它們其實都長一個樣:
首先,咱們來了解下測試的原理,咱們能夠將從磁盤加載一張圖片到最終渲染到屏幕上的過程劃分爲三個階段:
初始化階段:從磁盤初始化圖片,生成一個未解壓縮的 UIImage 對象;
解壓縮階段:分別使用 YYKit 、SDWebImage 和 FLAnimatedImage 對第 1 步中獲得的 UIImage 對象進行解壓縮,獲得一個新的解壓縮後的 UIImage 對象;
繪製階段:將第 2 步中獲得的 UIImage 對象繪製到屏幕上。
這裏咱們以繪製階段的耗時爲依據來評測解壓縮的性能,解壓縮的算法越優秀,那麼獲得的圖片就越符合系統渲染時的需求,繪製的時間也就越短。爲了讓測試的結果更準確,咱們對每張圖片都解壓縮 10 次,而後取平均值。說明,本次使用的測試設備是 iPhone 5s 。
首先,咱們來看看解壓縮 PNG 圖片的測試結果:
相應的柱狀圖以下:
從上圖能夠看出,就咱們採用的測試樣例來講,解壓縮 PNG 圖片的性能 SDWebImage 最好,FLAnimatedImage 次之,YYKit 最差。這與咱們前面的理論結果有必定的差距,多是測試樣例太少,也可能這就是真實結果。另外,須要說明的是,咱們這裏使用的 PNG 圖片都是不帶 alpha 值,由於 SDWebImage 不支持解壓縮帶 alpha 值的 PNG 圖片。
接着,咱們再來看看解壓縮 JPEG 圖片的測試結果:
相應的柱狀圖以下:
此次 YYKit 終於翻盤了,解壓縮 JPEG 圖片的性能最好,SDWebImage 和 FLAnimatedImage 並列第二。
總結
其實,要理解 iOS 中圖片的解壓縮並不難,重點是要理解位圖的概念。而圖片解壓縮的過程其實就是將圖片的二進制數據轉換成像素數據的過程。瞭解這些知識,將有助於咱們更好地處理圖片,管理好它們所佔用的內存。