圖片ImageI/O解碼探究

最近在做ImageI/O的相關調研,在使用CGImageSourceCreateImageAtIndex方法創建UIImage對象,和使用CGImageSourceCreateThumbnailAtIndex創建UIImage對象縮略圖時,引發了一系列的問題和思考探究,主要是關于ImageI/O的使用以及解碼過程。

正常情況下,當你用 UIImage 或 CGImageSource 的那幾個方法創建圖片時,圖片數據并不會立刻解碼。圖片設置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的數據才會得到解碼。這一步是發生在主線程的,并且不可避免。我們可以通過下面的方法模擬圖片被解碼并渲染的過程:


- (void)drawImage:(UIImage*)image {

    size_t width = CGImageGetWidth(image.CGImage);

    size_t height = CGImageGetHeight(image.CGImage);

    CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();

    CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpaceRef, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);

    if(!context)return;

    CGColorSpaceRelease(colorSpaceRef);

    CGContextDrawImage(context,CGRectMake(0,0, width, height), image.CGImage);// decode

    CGImageRef newImageRef = CGBitmapContextCreateImage(context);

    CFRelease(context);

    CGImageRelease(newImageRef);

}

用TimeProfiler一步一步來看過程中內部調用的函數可以幫助我們解決問題,由于TimeProfiler統計函數棧為間隔一段時間統計一次,導致沒有記錄下所有函數的調用而且每次函數棧還可能不一致,所以沒法精確判斷函數棧是如何調用的,但是可以大概推測出每步做了什么。

那么我們看下正常情況下圖片解碼時候,系統都是如何做的。首先是PNG格式的圖片:

PNG的解碼過程

CGContextDrawImageWithOptions方法中,調用了PNGPlugin庫中的一系列方法,沒有明顯看到帶有decode關鍵字的方法,猜測png_read_IDAT_dataApple就是執行的解碼過程。

接著看下JPEG格式的圖片:

JPEG的解碼過程

CGContextDrawImageWithOptions方法中,調用了AppleJPEGPlugin庫中的一系列方法,可以看到帶有decode關鍵字的方法FigPhotoJPEGDecodeJPEGIntoRGBSurface,這個應該就是執行解碼的過程。

好了,以上的實驗知道了PNG和JPEG格式的圖片執行解碼的關鍵方法,接下來正式進入本文章的探究主題。

下面的方法是使用ImageI/O,通過獲取縮略圖的方法,將圖片進行裁剪操作,生成所需要的UIImage對象。


- (UIImage*)resizeWithData:(NSData*)data scaleSize:(CGSize)size {

    if(!data) {

        returnnil;

    }

    // Create the image source

    CGImageSourceRef imageSourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);

    if(!imageSourceRef) {

        returnnil;

    }

    CGFloatmaxPixelSize =MAX(size.width, size.height);

    // Create thumbnail options

    CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceThumbnailMaxPixelSize: [NSNumbernumberWithFloat:maxPixelSize]

                                                           };

    // Generate the thumbnail

    CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(imageSourceRef, 0, options);

    UIImage*thumbnailImage = [UIImageimageWithCGImage:imageRef];

    CFRelease(imageSourceRef);

    CGImageRelease(imageRef);

    returnthumbnailImage;

}

這里有幾個參數需要解釋一下,依次如下:

kCGImageSourceShouldCacheImmediately,查看文檔解釋:


/* Specifies whether image decoding and caching should happen at image creation time.

 * The value of this key must be a CFBooleanRef. The default value is kCFBooleanFalse (image decoding will

 * happen at rendering time).

 */

翻譯過來就是說:

是否應該在圖像創建過程中,進行圖像解碼和緩存。 此鍵的值必須是CFBooleanRef。 默認值為kCFBooleanFalse(圖像解碼將在渲染時發生)。

kCGImageSourceShouldCache,查看下官方文檔解釋:


/** Keys for the options dictionary of "CGImageSourceCopyPropertiesAtIndex"

 ** and "CGImageSourceCreateImageAtIndex". **/

/* Specifies whether the image should be cached in a decoded form. The

 * value of this key must be a CFBooleanRef.

 * kCFBooleanFalse indicates no caching, kCFBooleanTrue indicates caching.

 * For 64-bit architectures, the default is kCFBooleanTrue, for 32-bit the default is kCFBooleanFalse.

 */

翻譯過來就是:

在方法CGImageSourceCopyPropertiesAtIndex和CGImageSourceCreateImageAtIndex中使用

指定是否應以解碼形式緩存圖像。 此鍵的值必須是CFBooleanRef。 kCFBooleanFalse表示沒有緩存,kCFBooleanTrue表示緩存。 對于64位體系結構,默認值為kCFBooleanTrue,對于32位,默認值為kCFBooleanFalse。

注意:此key指定的是解碼后的數據是否需要緩存。此處我們設置為kCFBooleanFalse,不進行緩存。

kCGImageSourceCreateThumbnailFromImageAlways,文檔解釋:


/* Specifies whether a thumbnail should be created from the full image even

 * if a thumbnail is present in the image source file. The thumbnail will

 * be created from the full image, subject to the limit specified by

 * kCGImageSourceThumbnailMaxPixelSize---if a maximum pixel size isn't

 * specified, then the thumbnail will be the size of the full image, which

 * probably isn't what you want. The value of this key must be a

 * CFBooleanRef; the default value of this key is kCFBooleanFalse. */

翻譯過來就是:

指定是否應從完整圖像創建縮略圖,即使圖像源文件中存在縮略圖也是如此。 縮略圖將根據完整圖像創建,受kCGImageSourceThumbnailMaxPixelSize指定的限制---如果未指定最大像素大小,則縮略圖將是完整圖像的大小,這可能不是您想要的。 該鍵的值必須是CFBooleanRef; 此鍵的默認值為kCFBooleanFalse。

這里我們設置為kCFBooleanTrue。

kCGImageSourceThumbnailMaxPixelSize,官方解釋:


/* Specifies the maximum width and height in pixels of a thumbnail.  If

 * this this key is not specified, the width and height of a thumbnail is

 * not limited and thumbnails may be as big as the image itself.  If

 * present, this value of this key must be a CFNumberRef. */

翻譯如下:

指定縮略圖的最大寬度和高度(以像素為單位)。 如果未指定此鍵,則縮略圖的寬度和高度不受限制,縮略圖可能與圖像本身一樣大。 如果存在,則此鍵的此值必須為CFNumberRef。

好的,接下來,我們看看CGImageSourceCreateThumbnailAtIndex系統具體做了什么。

首先我們看下PNG格式的圖片 512x384.png

同時為了測試圖片的解碼過程,我們將代碼中kCGImageSourceShouldCacheImmediately對應的值修改為kCFBooleanTrue,也就是創建圖片過程中進行解碼。

根據Time Profiler我們查看下系統都在這個函數里面做了什么,調用結果如下:

PNG的resizeWithData

可以看到,- (UIImage)resizeWithData:(NSData)data scaleSize:(CGSize)size方法執行了48.00ms,其中CGImageSourceCreateThumbnailAtIndex執行了大約45.00ms,大部分耗時都在這里。我們看下里面究竟做了什么。過程中系統調用了CGContextDrawImageWithOptions。因為我們前面設置了kCGImageSourceShouldCacheImmediately對應的值修改為kCFBooleanTrue,也就是需要解碼,所以這里系統調用了CGContextDrawImageWithOptions方法,會將圖片渲染到畫布,這個過程是會解碼的。那么接著往下看,具體解碼的步驟在哪里。可以看到接下來最耗時的操作分別在img_interpolate_extent和img_interpolate_read兩個函數。然后分別看看這兩個函數做了什么。

PNG的img_interpolate_extent

這里系統調用了CGImageProviderCopyImageBlockSet,里面調用了PNGPlugin庫的_cg_png_read_row和_cg_png_read_info方法,_cg_png_read_row方法調用了png_read_IDAT_dataApple,這個方法上面已經提到了,是進行的解碼操作。

PNG的img_interpolate_read

img_interpolate_read里面調用了img_decide_read,猜測應該是讀取解碼完成的數據。

好了,PNG格式的圖片如何解碼我們大致推理出來了,那么再看看JPEG格式的圖片,512x384.jpg

JPEG的resizeWithData

JPEG圖片的- (UIImage)resizeWithData:(NSData)data scaleSize:(CGSize)size方法執行了55.00ms,其中CGImageSourceCreateThumbnailAtIndex執行了大約53.00ms,這里系統同樣調用了CGContextDrawImageWithOptions方法,與PNG不同的是,JPEG里面調用了img_decode_stage和img_interpolate_read方法。

JPEG的img_decode_stage

可以看到img_decode_stage方法里面同樣調用了CGImageProviderCopyImageBlockSet方法,然后調用了AppleJPEGPlugin庫的FigPhotoJPEGDecodeJPEGIntoRGBSurface方法,這里進行了解碼。

JPEG的img_interpolate_read

img_interpolate_read里面調用了img_decode_read,跟PNG圖片的一模一樣,應該也是對解碼完成的數據進行讀取。

以上就是解碼過程的剖析,那么作為對比試驗,我們接下來看下不經過解碼時的調用過程。

將kCGImageSourceShouldCacheImmediately對應的值修改為kCFBooleanFalse,也就是創建圖片過程中不進行解碼。

首先還是看下PNG格式的圖片,512x384.png

PNG

然后就震驚了!!!有沒有!!!居然跟之前強制解碼的一模一樣!!!展開圖中標出的兩個方法。

JPEG
JPEG

真的是也會解碼!!!這究竟是為什么呢?

難道是


CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceThumbnailMaxPixelSize: [NSNumbernumberWithFloat:maxPixelSize]

                                                           };

這個options的問題?試著嘗試使用不同的options,有了下面的結果:

情況一:


CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,

//                                                          (__bridge id) kCGImageSourceThumbnailMaxPixelSize : [NSNumber numberWithFloat:maxPixelSize]

                                                           };

實驗結果如下:

情況一PNG
情況一JPEG

情況一結論:如果不設置kCGImageSourceThumbnailMaxPixelSize,同時kCGImageSourceShouldCacheImmediately設置為kCFBooleanFalse,那么不管是PNG還是JPEG格式的圖片,都沒有進行解碼操作。

情況二:


CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,

//                                                          (__bridge id) kCGImageSourceThumbnailMaxPixelSize : [NSNumber numberWithFloat:maxPixelSize]

                                                           };

實驗結果如下:

情況二PNG
情況二JPEG

情況二結論:如果不設置kCGImageSourceThumbnailMaxPixelSize,同時kCGImageSourceShouldCacheImmediately設置為kCFBooleanTure,那么不管是PNG還是JPEG格式的圖片,都進行了解碼操作。

情況三:


CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceThumbnailMaxPixelSize: [NSNumbernumberWithFloat:maxPixelSize]

                                                           };

實驗結果如下:

情況三PNG
情況三JPEG

情況三結論:如果設置了kCGImageSourceThumbnailMaxPixelSize,同時kCGImageSourceShouldCacheImmediately設置為kCFBooleanFalse,那么不管是PNG還是JPEG格式的圖片,都進行了解碼操作。

情況四:


CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceThumbnailMaxPixelSize: [NSNumbernumberWithFloat:maxPixelSize]

                                                           };

實驗結果如下:

情況四PNG
情況四JPEG

情況四結論:如果設置了kCGImageSourceThumbnailMaxPixelSize,同時kCGImageSourceShouldCacheImmediately設置為kCFBooleanTure,那么不管是PNG還是JPEG格式的圖片,也都進行了解碼操作。

總結:

1、在使用CGImageSourceCreateThumbnailAtIndex方法時,如果設置了kCGImageSourceThumbnailMaxPixelSize,那么肯定會進行解碼操作,生成對應的新圖CGImageRef。

2、如果不設置kCGImageSourceThumbnailMaxPixelSize,那么是否進行解碼操作,取決于kCGImageSourceShouldCacheImmediately對應的值是kCFBooleanTure還是kCFBooleanFalse。

ImageI/O中使用CGImageSourceCreateThumbnailAtIndex創建縮略圖方法的結論就是如上所述。那么這里又有另一個思考,如果是CGImageSourceCreateImageAtIndex方法,那么上述的kCGImageSourceShouldCacheImmediately鍵值對會造成什么影響呢?


- (UIImage*)resizeWithData:(NSData*)data scaleSize:(CGSize)size {

    if(!data) {

        returnnil;

    }

    // Create the image source

    CGImageSourceRef imageSourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);

    if(!imageSourceRef) {

        returnnil;

    }

    // Create thumbnail options

    CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse

                                                           };

    // Generate the thumbnail

    NSLog(@"%@", options);

    CGImageRefimageRef =

    CGImageSourceCreateImageAtIndex(imageSourceRef,0, options);

    UIImage*thumbnailImage = [UIImageimageWithCGImage:imageRef];

    CFRelease(imageSourceRef);

    CGImageRelease(imageRef);

    returnthumbnailImage;

}

情況一:


CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse

                                                           };

實驗如下:

CGImageSourceCreateImageAtIndex不解碼_PNG
CGImageSourceCreateImageAtIndex不解碼_JPEG

情況一結論:設置kCGImageSourceShouldCacheImmediately為kCFBooleanFalse時,PNG和JPEG都沒有進行解碼。

情況二:


CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                           (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse

                                                           };

實驗如下:

CGImageSourceCreateImageAtIndex解碼_PNG
CGImageSourceCreateImageAtIndex解碼_JPEG

情況二結論:設置kCGImageSourceShouldCacheImmediately為kCFBooleanTrue時,PNG和JPEG都進行了解碼。

總結:在使用CGImageSourceCreateImageAtIndex方法創建CGImageRef時,kCGImageSourceShouldCacheImmediately值會影響是否開啟解碼操作。ImageI/O默認的kCGImageSourceShouldCacheImmediately為kCFBooleanFalse,也就是說創建圖片時候不解碼,會等到圖片被渲染的時候才進行解碼。

以上部分就明確了CGImageSourceCreateImageAtIndex和CGImageSourceCreateThumbnailAtIndex時系統底層具體的實現。

當然,在使用這個- (UIImage*)resizeWithData:(NSData*)data scaleSize:(CGSize)size方法時候也采坑了。因為一般大部分情況下(參考圖片縮放使用UIKIt、Core Graphics、Core Foundation等情況下的方法),是為UIImage添加一個分類,使用分類方法進行縮放。那么既然如此,為什么不同樣使用分類呢?嗯,不錯的想法,筆者剛開始是這樣做的:


- (UIImage*)resizeWithImage:(UIImage*)image scaleSize:(CGSize)size {

    CFDataRef bitmapData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));

    // Create the image source

    CGImageSourceRef imageSourceRef = CGImageSourceCreateWithData(bitmapData, NULL);

    if(!imageSourceRef) {

        returnnil;

    }

    CGFloatmaxPixelSize =MAX(size.width, size.height);

    // Create thumbnail options

    CFDictionaryRef options = (__bridge CFDictionaryRef) @{

                                                                                                                      (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceThumbnailMaxPixelSize: [NSNumbernumberWithFloat:maxPixelSize]

                                                           };

    // Generate the thumbnail

    CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(imageSourceRef, 0, options);

    UIImage*thumbnailImage = [UIImageimageWithCGImage:imageRef];

    CFRelease(imageSourceRef);

    CGImageRelease(imageRef);

    returnthumbnailImage;

}

干凈漂亮,直接跑起來,美滋滋。

但是!!!結果返回的圖片為nil。

為什么?百思不得其解,詳細了解了下CGDataProvider的一系列API,CGimage的dataProvider,指的是CGImageCreate時候,傳入的承載了Bitmap Buffer數組的一個提供者,可以是一個內存中的buffer,也可以是一個callback來實現惰性解碼。也就是說,這個傳入的Bitmap Buffer數組,必須是未經過解壓縮的數據。如果是經過了解壓縮的圖片數據,那么傳給ImageI/0是沒有意義的。

問題真的出在這里嗎?這兩個方法參數不同之處是,一個是使用UIImage *image = [UIImage imageWithContentsOfFile:path],傳入UIImage對象,而另一個是通過NSData *data = [NSData dataWithContentsOfFile:path],傳入的NSData對象。

那么這兩個方法本質的區別到底是什么呢?為什么造成不同的結局呢?系統在這兩個方法里面具體都干了什么呢?

首先,拿JPEG格式的做實驗,看看[UIImage imageWithContentsOfFile:]都做了什么。

JPEG的imageWithContentsOfFile

可以看到,在該方法中系統調用了CGImageSourceCreateImageAtIndex方法,在該方法中,系統使用了AppleJPEGPlugin庫的一些方法,但是并沒有發現decode相關的函數,所以這里應該沒有進行解碼,而只是將圖片進行了解壓縮(decompress)。這也就解釋了為什么使用CGImageGetDataProvider獲取的CGDataProvider對象是無效的了。

那么同時可以看下PNG格式的圖片,在使用[UIImage imageWithContentsOfFile:]時系統都做了什么。

PNG的imageWithContentsOfFile

PNG格式的圖片,同樣是調用CGImageSourceCreateImageAtIndex等方法,同時可以看到使用的是PNGPlugin庫相關的方法,PNGReadPlugin讀取文件數據進行解壓縮。

有興趣的同學可以看下[UIImage imageWithNamed:]方法創建的UIImage對象,至于[UIImage imageWithNamed:]和[UIImage imageWithContentsOfFile:]的具體區別,會另起一篇文章進行分析。

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

推薦閱讀更多精彩內容