iOS圖片加載優化二三事

前言

一張圖片從引入project中,到最后展示在用戶面前,經歷了許多環節。其中壓縮解壓縮就是一個值得我們探究的環節。

開始之前,我們需要了解一些基本的圖像原理。平時開發中接觸的最多的當屬png格式的圖片,其次就是jpg。這兩種文件格式本質上是圖片的壓縮格式。區別在于png是無損壓縮,支持alpha通道,也就是透明,而jpg是有損壓縮。事實上,UIKit中就有兩個API來生成pngjpg

// return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format
UIKIT_EXTERN NSData * __nullable UIImagePNGRepresentation(UIImage * __nonnull image);

// return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least)                           
UIKIT_EXTERN NSData * __nullable UIImageJPEGRepresentation(UIImage * __nonnull image, CGFloat compressionQuality);

compressionQuality指的就是壓縮質量。

通過以下方法我們可以獲取到原始圖像的數據,經過比較遠大于圖片本身大小。也就是說經過解壓縮以后,圖片的大小得到了進一步的增大。

UIImage *image = [UIImage imageNamed:@"xxx"];
CFDataRef origenal = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));

事實上,圖片是由一個個像素點組成的一個集合,所以我們可以很容易得出:

解壓縮后的圖片大小 = 圖片的像素寬  * 圖片的像素高  * 每個像素所占的字節數 4

這也符合我們的預期,圖片越大,文件越大。

圖片加載的工作流

  1. 假設我們使用 +imageWithContentsOfFile: 方法從磁盤中加載一張圖片,這個時候的圖片并沒有解壓縮;
  1. 然后將生成的 UIImage 賦值給 UIImageView ;
  2. 接著一個隱式的 CATransaction 捕獲到了 UIImageView 圖層樹的變化;
  3. 在主線程的下一個 run loop 到來時,Core Animation 提交了這個隱式的 transaction ,這個過程可能會對圖片進行 copy 操作,而受圖片是否字節對齊等因素的影響,這個 copy 操作可能會涉及以下部分或全部步驟:
  • 分配內存緩沖區用于管理文件 IO 和解壓縮操作;
  • 將文件數據從磁盤讀到內存中;
  • 將壓縮的圖片數據解碼成未壓縮的位圖形式,這是一個非常耗時的 CPU 操作;
  • 最后 Core Animation 使用未壓縮的位圖數據渲染 UIImageView 的圖層。

在上面的步驟中,我們提到了圖片的解壓縮是一個非常耗時的 CPU 操作,并且它默認是在主線程中執行的。那么當需要加載的圖片比較多時,就會對我們應用的響應性造成嚴重的影響,尤其是在快速滑動的列表上,這個問題會表現得更加突出。

強制解壓縮

通過圖片加載的工作流我們發現,在執行圖片解壓縮這一步不可避免驟時,主線程會有不同程度的阻塞,從而影響到程序的流暢性。
很容易能夠聯想到的解決方法就是通過將這一耗時操作放到子線程中由我們自己接管解壓縮的流程,從而避免在主線程中去執行這個耗時的操作。
如果圖片已經解壓縮,系統就不會再對圖片進行解壓縮,因此這個問題能夠得到優化。

而強制解壓縮的原理就是對圖片進行重新繪制,得到一張新的解壓縮后的位圖。其中,用到的最核心的函數是:

/* Create a bitmap context. The context draws into a bitmap which is `width'
   pixels wide and `height' pixels high. The number of components for each
   pixel is specified by `space', which may also specify a destination color
   profile. The number of bits for each component of a pixel is specified by
   `bitsPerComponent'. The number of bytes per pixel is equal to
   `(bitsPerComponent * number of components + 7)/8'. Each row of the bitmap
   consists of `bytesPerRow' bytes, which must be at least `width * bytes
   per pixel' bytes; in addition, `bytesPerRow' must be an integer multiple
   of the number of bytes per pixel. `data', if non-NULL, points to a block
   of memory at least `bytesPerRow * height' bytes. If `data' is NULL, the
   data for context is allocated automatically and freed when the context is
   deallocated. `bitmapInfo' specifies whether the bitmap should contain an
   alpha channel and how it's to be generated, along with whether the
   components are floating-point or integer. */
CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
    size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
    CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
    CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

這個函數用于創建一個位圖上下文,用來繪制一張寬 width像素,高 height像素的位圖。

Pixel Format

像素格式用來描述每個像素包含的信息:

Bits per component :一個像素中每個獨立的顏色分量使用的 bit 數;
Bits per pixel :一個像素使用的總 bit 數;
Bytes per row :位圖中的每一行使用的字節數。```

像素格式的組合并不是隨機的,而是存在一定的模式,在目前MacOS & iOS環境下,支持[十七種像素格式的組合](https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB)


![Supported Pixel Formats.png](http://upload-images.jianshu.io/upload_images/1677365-114f0fcfe703a605.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

從上圖可知,對于 iOS 來說,只支持 8 種像素格式。其中顏色空間為 Null 的 1 種,Gray 的 2 種,RGB 的 5 種,CMYK 的 0 種。換句話說,iOS 并不支持 CMYK 的顏色空間。另外,在表格的第 2 列中,除了像素格式外,還指定了 `bitmap information constant `

####[Color and Color Spaces](https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_color/dq_color.html#//apple_ref/doc/uid/TP30001066-CH205-TPXREF101)

在 Quartz 中,一個顏色是由一組值來表示的,如圖所示,而顏色空間則是用來說明如何解析這些值的,離開了顏色空間,它們將變得毫無意義。

![](http://upload-images.jianshu.io/upload_images/1677365-6747f6b7bb55fe12.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

表中的值最后的結果都是藍色,但是我們可以看到相同的顏色在不同的色彩空間中所需要的值是不同的。


![左邊BGR,右邊RGB](http://upload-images.jianshu.io/upload_images/1677365-22d9c1798f6930dc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

這張圖很形象的展示了相同的值在不同顏色空間下的結果是截然不同的。


####[Bitmap Layout](https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_images/dq_images.html#//apple_ref/doc/uid/TP30001066-CH212-CJBHEGIB)
```objectivec
typedef CF_OPTIONS(uint32_t, CGBitmapInfo) {
    kCGBitmapAlphaInfoMask = 0x1F,

    kCGBitmapFloatInfoMask = 0xF00,
    kCGBitmapFloatComponents = (1 << 8),

    kCGBitmapByteOrderMask     = kCGImageByteOrderMask,
    kCGBitmapByteOrderDefault  = (0 << 12),
    kCGBitmapByteOrder16Little = kCGImageByteOrder16Little,
    kCGBitmapByteOrder32Little = kCGImageByteOrder32Little,
    kCGBitmapByteOrder16Big    = kCGImageByteOrder16Big,
    kCGBitmapByteOrder32Big    = kCGImageByteOrder32Big
} CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

位圖布局信息,是為了讓Quartz正確的解釋每個像素的信息。
其中主要包含了三點內容:

  • Alpha通道的信息
  • 像素格式的字節順序。
  • 顏色分量的數據格式 - 整數或浮點值。

其中alpha 的信息由枚舉值 CGImageAlphaInfo來表示:

typedef CF_ENUM(uint32_t, CGImageAlphaInfo) {
    kCGImageAlphaNone,               /* For example, RGB. */
    kCGImageAlphaPremultipliedLast,  /* For example, premultiplied RGBA */
    kCGImageAlphaPremultipliedFirst, /* For example, premultiplied ARGB */
    kCGImageAlphaLast,               /* For example, non-premultiplied RGBA */
    kCGImageAlphaFirst,              /* For example, non-premultiplied ARGB */
    kCGImageAlphaNoneSkipLast,       /* For example, RBGX. */
    kCGImageAlphaNoneSkipFirst,      /* For example, XRGB. */
    kCGImageAlphaOnly                /* No color data, alpha data only */
};

alpha信息提供了:

  1. 是否包含 alpha ;
  1. 如果包含 alpha ,那么 alpha 信息所處的位置,在像素的最低有效位,比如 RGBA ,還是最高有效位比如 ARGB ;
  2. 如果包含 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

而顏色分量上,kCGBitmapFloatComponents或者直接使用值就行

上文官方文檔中提到的字節順序應該使用 32 位的主機字節順序 kCGBitmapByteOrder32Host,這個參數來源于官方準備的宏,雖然iPhone用的是16位小端模式,但是通過此使用此宏來適配不管什么設備,字節順序始終是對的。

#ifdef __BIG_ENDIAN__
    #define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
    #define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
#else /* Little endian. */
    #define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
    #define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
#endif
typedef CF_ENUM(uint32_t, CGImageByteOrderInfo) {
    kCGImageByteOrderMask     = 0x7000,
    kCGImageByteOrder16Little = (1 << 12),
    kCGImageByteOrder32Little = (2 << 12),
    kCGImageByteOrder16Big    = (3 << 12),
    kCGImageByteOrder32Big    = (4 << 12)
} CG_AVAILABLE_STARTING(__MAC_10_12, __IPHONE_10_0);

在這里,大家可以參考官方文檔-字節順序

總結:

data:如果不為 NULL,那么它應該指向一塊大小至少為 bytesPerRow * height字節的內存;如果 為 NULL,那么系統就會為我們自動分配和釋放所需的內存,所以一般指定 NULL
即可;
widthheight:位圖的寬度和高度,分別賦值為圖片的像素寬度和像素高度即可;
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:就是我們前面提到的位圖的布局信息。

最后

圖片解壓縮的僅僅在于這一函數方法的使用,重點在于理解位圖包含的信息以及構成,在程序中合理使用函數,將有助于我們縮短圖片加載時間,更優化App的性能。

=================

REFRENCE
https://www.cocoanetics.com/2011/10/avoiding-image-decompression-sickness/ https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/Introduction/Introduction.html https://github.com/path/FastImageCache http://stackoverflow.com/questions/23790837/what-is-byte-alignment-cache-line-alignment-for-core-animation-why-it-matters
http://blog.leichunfeng.com/blog/2017/02/20/talking-about-the-decompression-of-the-image-in-ios/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 知道了那么多關于iOS上界面渲染的理論知識后,終于可以回歸最開始的問題,將一張 png/jpg 格式的圖片渲染到頁...
    巫師學徒閱讀 745評論 0 2
  • 繪制像素到屏幕上 answer-huang22 Mar 2014 分享文章 一個像素是如何繪制到屏幕上去的?有很多...
    阿貍旅途T恤閱讀 1,649評論 0 7
  • 有很多種framework以及很多種方法的組合可以在屏幕上渲染UI元素,我們在這里討論這個過程中發生的事情,希望這...
    縱橫而樂閱讀 4,512評論 4 25
  • 誰吃掉我們的CPU: 方法CA::Render::create_image_from_provider 圖片預解碼...
    神采飛揚_2015閱讀 3,308評論 2 6
  • 卷首語 歡迎來到 objc.io 的第三期! 這一期都是關于視圖層的。當然視圖層有很多方面,我們需要把它們縮小到幾...
    評評分分閱讀 1,796評論 0 18