最近在做ImageI/O的相關(guān)調(diào)研,在使用CGImageSourceCreateImageAtIndex方法創(chuàng)建UIImage對象,和使用CGImageSourceCreateThumbnailAtIndex創(chuàng)建UIImage對象縮略圖時(shí),引發(fā)了一系列的問題和思考探究,主要是關(guān)于ImageI/O的使用以及解碼過程。
正常情況下,當(dāng)你用 UIImage 或 CGImageSource 的那幾個(gè)方法創(chuàng)建圖片時(shí),圖片數(shù)據(jù)并不會立刻解碼。圖片設(shè)置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的數(shù)據(jù)才會得到解碼。這一步是發(fā)生在主線程的,并且不可避免。我們可以通過下面的方法模擬圖片被解碼并渲染的過程:
- (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一步一步來看過程中內(nèi)部調(diào)用的函數(shù)可以幫助我們解決問題,由于TimeProfiler統(tǒng)計(jì)函數(shù)棧為間隔一段時(shí)間統(tǒng)計(jì)一次,導(dǎo)致沒有記錄下所有函數(shù)的調(diào)用而且每次函數(shù)棧還可能不一致,所以沒法精確判斷函數(shù)棧是如何調(diào)用的,但是可以大概推測出每步做了什么。
那么我們看下正常情況下圖片解碼時(shí)候,系統(tǒng)都是如何做的。首先是PNG格式的圖片:
CGContextDrawImageWithOptions方法中,調(diào)用了PNGPlugin庫中的一系列方法,沒有明顯看到帶有decode關(guān)鍵字的方法,猜測png_read_IDAT_dataApple就是執(zhí)行的解碼過程。
接著看下JPEG格式的圖片:
CGContextDrawImageWithOptions方法中,調(diào)用了AppleJPEGPlugin庫中的一系列方法,可以看到帶有decode關(guān)鍵字的方法FigPhotoJPEGDecodeJPEGIntoRGBSurface,這個(gè)應(yīng)該就是執(zhí)行解碼的過程。
好了,以上的實(shí)驗(yàn)知道了PNG和JPEG格式的圖片執(zhí)行解碼的關(guān)鍵方法,接下來正式進(jìn)入本文章的探究主題。
下面的方法是使用ImageI/O,通過獲取縮略圖的方法,將圖片進(jìn)行裁剪操作,生成所需要的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;
}
這里有幾個(gè)參數(shù)需要解釋一下,依次如下:
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).
*/
翻譯過來就是說:
是否應(yīng)該在圖像創(chuàng)建過程中,進(jìn)行圖像解碼和緩存。 此鍵的值必須是CFBooleanRef。 默認(rèn)值為kCFBooleanFalse(圖像解碼將在渲染時(shí)發(fā)生)。
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中使用
指定是否應(yīng)以解碼形式緩存圖像。 此鍵的值必須是CFBooleanRef。 kCFBooleanFalse表示沒有緩存,kCFBooleanTrue表示緩存。 對于64位體系結(jié)構(gòu),默認(rèn)值為kCFBooleanTrue,對于32位,默認(rèn)值為kCFBooleanFalse。
注意:此key指定的是解碼后的數(shù)據(jù)是否需要緩存。此處我們設(shè)置為kCFBooleanFalse,不進(jìn)行緩存。
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. */
翻譯過來就是:
指定是否應(yīng)從完整圖像創(chuàng)建縮略圖,即使圖像源文件中存在縮略圖也是如此。 縮略圖將根據(jù)完整圖像創(chuàng)建,受kCGImageSourceThumbnailMaxPixelSize指定的限制---如果未指定最大像素大小,則縮略圖將是完整圖像的大小,這可能不是您想要的。 該鍵的值必須是CFBooleanRef; 此鍵的默認(rèn)值為kCFBooleanFalse。
這里我們設(shè)置為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系統(tǒng)具體做了什么。
首先我們看下PNG格式的圖片 512x384.png
同時(shí)為了測試圖片的解碼過程,我們將代碼中kCGImageSourceShouldCacheImmediately對應(yīng)的值修改為kCFBooleanTrue,也就是創(chuàng)建圖片過程中進(jìn)行解碼。
根據(jù)Time Profiler我們查看下系統(tǒng)都在這個(gè)函數(shù)里面做了什么,調(diào)用結(jié)果如下:
可以看到,- (UIImage)resizeWithData:(NSData)data scaleSize:(CGSize)size方法執(zhí)行了48.00ms,其中CGImageSourceCreateThumbnailAtIndex執(zhí)行了大約45.00ms,大部分耗時(shí)都在這里。我們看下里面究竟做了什么。過程中系統(tǒng)調(diào)用了CGContextDrawImageWithOptions。因?yàn)槲覀兦懊嬖O(shè)置了kCGImageSourceShouldCacheImmediately對應(yīng)的值修改為kCFBooleanTrue,也就是需要解碼,所以這里系統(tǒng)調(diào)用了CGContextDrawImageWithOptions方法,會將圖片渲染到畫布,這個(gè)過程是會解碼的。那么接著往下看,具體解碼的步驟在哪里。可以看到接下來最耗時(shí)的操作分別在img_interpolate_extent和img_interpolate_read兩個(gè)函數(shù)。然后分別看看這兩個(gè)函數(shù)做了什么。
這里系統(tǒng)調(diào)用了CGImageProviderCopyImageBlockSet,里面調(diào)用了PNGPlugin庫的_cg_png_read_row和_cg_png_read_info方法,_cg_png_read_row方法調(diào)用了png_read_IDAT_dataApple,這個(gè)方法上面已經(jīng)提到了,是進(jìn)行的解碼操作。
img_interpolate_read里面調(diào)用了img_decide_read,猜測應(yīng)該是讀取解碼完成的數(shù)據(jù)。
好了,PNG格式的圖片如何解碼我們大致推理出來了,那么再看看JPEG格式的圖片,512x384.jpg
JPEG圖片的- (UIImage)resizeWithData:(NSData)data scaleSize:(CGSize)size方法執(zhí)行了55.00ms,其中CGImageSourceCreateThumbnailAtIndex執(zhí)行了大約53.00ms,這里系統(tǒng)同樣調(diào)用了CGContextDrawImageWithOptions方法,與PNG不同的是,JPEG里面調(diào)用了img_decode_stage和img_interpolate_read方法。
可以看到img_decode_stage方法里面同樣調(diào)用了CGImageProviderCopyImageBlockSet方法,然后調(diào)用了AppleJPEGPlugin庫的FigPhotoJPEGDecodeJPEGIntoRGBSurface方法,這里進(jìn)行了解碼。
img_interpolate_read里面調(diào)用了img_decode_read,跟PNG圖片的一模一樣,應(yīng)該也是對解碼完成的數(shù)據(jù)進(jìn)行讀取。
以上就是解碼過程的剖析,那么作為對比試驗(yàn),我們接下來看下不經(jīng)過解碼時(shí)的調(diào)用過程。
將kCGImageSourceShouldCacheImmediately對應(yīng)的值修改為kCFBooleanFalse,也就是創(chuàng)建圖片過程中不進(jìn)行解碼。
首先還是看下PNG格式的圖片,512x384.png
然后就震驚了!!!有沒有!!!居然跟之前強(qiáng)制解碼的一模一樣!!!展開圖中標(biāo)出的兩個(gè)方法。
真的是也會解碼!!!這究竟是為什么呢?
難道是
CFDictionaryRef options = (__bridge CFDictionaryRef) @{
(__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,
(__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,
(__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,
(__bridgeid)kCGImageSourceThumbnailMaxPixelSize: [NSNumbernumberWithFloat:maxPixelSize]
};
這個(gè)options的問題?試著嘗試使用不同的options,有了下面的結(jié)果:
情況一:
CFDictionaryRef options = (__bridge CFDictionaryRef) @{
(__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,
(__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,
(__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,
// (__bridge id) kCGImageSourceThumbnailMaxPixelSize : [NSNumber numberWithFloat:maxPixelSize]
};
實(shí)驗(yàn)結(jié)果如下:
情況一結(jié)論:如果不設(shè)置kCGImageSourceThumbnailMaxPixelSize,同時(shí)kCGImageSourceShouldCacheImmediately設(shè)置為kCFBooleanFalse,那么不管是PNG還是JPEG格式的圖片,都沒有進(jìn)行解碼操作。
情況二:
CFDictionaryRef options = (__bridge CFDictionaryRef) @{
(__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanTrue,
(__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,
(__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,
// (__bridge id) kCGImageSourceThumbnailMaxPixelSize : [NSNumber numberWithFloat:maxPixelSize]
};
實(shí)驗(yàn)結(jié)果如下:
情況二結(jié)論:如果不設(shè)置kCGImageSourceThumbnailMaxPixelSize,同時(shí)kCGImageSourceShouldCacheImmediately設(shè)置為kCFBooleanTure,那么不管是PNG還是JPEG格式的圖片,都進(jìn)行了解碼操作。
情況三:
CFDictionaryRef options = (__bridge CFDictionaryRef) @{
(__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,
(__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,
(__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,
(__bridgeid)kCGImageSourceThumbnailMaxPixelSize: [NSNumbernumberWithFloat:maxPixelSize]
};
實(shí)驗(yàn)結(jié)果如下:
情況三結(jié)論:如果設(shè)置了kCGImageSourceThumbnailMaxPixelSize,同時(shí)kCGImageSourceShouldCacheImmediately設(shè)置為kCFBooleanFalse,那么不管是PNG還是JPEG格式的圖片,都進(jìn)行了解碼操作。
情況四:
CFDictionaryRef options = (__bridge CFDictionaryRef) @{
(__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanTrue,
(__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,
(__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,
(__bridgeid)kCGImageSourceThumbnailMaxPixelSize: [NSNumbernumberWithFloat:maxPixelSize]
};
實(shí)驗(yàn)結(jié)果如下:
情況四結(jié)論:如果設(shè)置了kCGImageSourceThumbnailMaxPixelSize,同時(shí)kCGImageSourceShouldCacheImmediately設(shè)置為kCFBooleanTure,那么不管是PNG還是JPEG格式的圖片,也都進(jìn)行了解碼操作。
總結(jié):
1、在使用CGImageSourceCreateThumbnailAtIndex方法時(shí),如果設(shè)置了kCGImageSourceThumbnailMaxPixelSize,那么肯定會進(jìn)行解碼操作,生成對應(yīng)的新圖CGImageRef。
2、如果不設(shè)置kCGImageSourceThumbnailMaxPixelSize,那么是否進(jìn)行解碼操作,取決于kCGImageSourceShouldCacheImmediately對應(yīng)的值是kCFBooleanTure還是kCFBooleanFalse。
ImageI/O中使用CGImageSourceCreateThumbnailAtIndex創(chuàng)建縮略圖方法的結(jié)論就是如上所述。那么這里又有另一個(gè)思考,如果是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
};
實(shí)驗(yàn)如下:
情況一結(jié)論:設(shè)置kCGImageSourceShouldCacheImmediately為kCFBooleanFalse時(shí),PNG和JPEG都沒有進(jìn)行解碼。
情況二:
CFDictionaryRef options = (__bridge CFDictionaryRef) @{
(__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanTrue,
(__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse
};
實(shí)驗(yàn)如下:
情況二結(jié)論:設(shè)置kCGImageSourceShouldCacheImmediately為kCFBooleanTrue時(shí),PNG和JPEG都進(jìn)行了解碼。
總結(jié):在使用CGImageSourceCreateImageAtIndex方法創(chuàng)建CGImageRef時(shí),kCGImageSourceShouldCacheImmediately值會影響是否開啟解碼操作。ImageI/O默認(rèn)的kCGImageSourceShouldCacheImmediately為kCFBooleanFalse,也就是說創(chuàng)建圖片時(shí)候不解碼,會等到圖片被渲染的時(shí)候才進(jìn)行解碼。
以上部分就明確了CGImageSourceCreateImageAtIndex和CGImageSourceCreateThumbnailAtIndex時(shí)系統(tǒng)底層具體的實(shí)現(xiàn)。
當(dāng)然,在使用這個(gè)- (UIImage*)resizeWithData:(NSData*)data scaleSize:(CGSize)size
方法時(shí)候也采坑了。因?yàn)橐话愦蟛糠智闆r下(參考圖片縮放使用UIKIt、Core Graphics、Core Foundation等情況下的方法),是為UIImage添加一個(gè)分類,使用分類方法進(jìn)行縮放。那么既然如此,為什么不同樣使用分類呢?嗯,不錯(cuò)的想法,筆者剛開始是這樣做的:
- (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;
}
干凈漂亮,直接跑起來,美滋滋。
但是!!!結(jié)果返回的圖片為nil。
為什么?百思不得其解,詳細(xì)了解了下CGDataProvider的一系列API,CGimage的dataProvider,指的是CGImageCreate時(shí)候,傳入的承載了Bitmap Buffer數(shù)組的一個(gè)提供者,可以是一個(gè)內(nèi)存中的buffer,也可以是一個(gè)callback來實(shí)現(xiàn)惰性解碼。也就是說,這個(gè)傳入的Bitmap Buffer數(shù)組,必須是未經(jīng)過解壓縮的數(shù)據(jù)。如果是經(jīng)過了解壓縮的圖片數(shù)據(jù),那么傳給ImageI/0是沒有意義的。
問題真的出在這里嗎?這兩個(gè)方法參數(shù)不同之處是,一個(gè)是使用UIImage *image = [UIImage imageWithContentsOfFile:path],傳入U(xiǎn)IImage對象,而另一個(gè)是通過NSData *data = [NSData dataWithContentsOfFile:path],傳入的NSData對象。
那么這兩個(gè)方法本質(zhì)的區(qū)別到底是什么呢?為什么造成不同的結(jié)局呢?系統(tǒng)在這兩個(gè)方法里面具體都干了什么呢?
首先,拿JPEG格式的做實(shí)驗(yàn),看看[UIImage imageWithContentsOfFile:]都做了什么。
可以看到,在該方法中系統(tǒng)調(diào)用了CGImageSourceCreateImageAtIndex方法,在該方法中,系統(tǒng)使用了AppleJPEGPlugin庫的一些方法,但是并沒有發(fā)現(xiàn)decode相關(guān)的函數(shù),所以這里應(yīng)該沒有進(jìn)行解碼,而只是將圖片進(jìn)行了解壓縮(decompress)。這也就解釋了為什么使用CGImageGetDataProvider獲取的CGDataProvider對象是無效的了。
那么同時(shí)可以看下PNG格式的圖片,在使用[UIImage imageWithContentsOfFile:]時(shí)系統(tǒng)都做了什么。
PNG格式的圖片,同樣是調(diào)用CGImageSourceCreateImageAtIndex等方法,同時(shí)可以看到使用的是PNGPlugin庫相關(guān)的方法,PNGReadPlugin讀取文件數(shù)據(jù)進(jìn)行解壓縮。
有興趣的同學(xué)可以看下[UIImage imageWithNamed:]方法創(chuàng)建的UIImage對象,至于[UIImage imageWithNamed:]和[UIImage imageWithContentsOfFile:]的具體區(qū)別,會另起一篇文章進(jìn)行分析。