最近在做ImageI/O的相關(guān)調(diào)研凭语,在使用CGImageSourceCreateImageAtIndex方法創(chuàng)建UIImage對象,和使用CGImageSourceCreateThumbnailAtIndex創(chuàng)建UIImage對象縮略圖時扑庞,引發(fā)了一系列的問題和思考探究,主要是關(guān)于ImageI/O的使用以及解碼過程。
正常情況下砚著,當(dāng)你用 UIImage 或 CGImageSource 的那幾個方法創(chuàng)建圖片時祷蝌,圖片數(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ù)棧為間隔一段時間統(tǒng)計(jì)一次吁津,導(dǎo)致沒有記錄下所有函數(shù)的調(diào)用而且每次函數(shù)棧還可能不一致,所以沒法精確判斷函數(shù)棧是如何調(diào)用的堕扶,但是可以大概推測出每步做了什么碍脏。
那么我們看下正常情況下圖片解碼時候,系統(tǒng)都是如何做的稍算。首先是PNG格式的圖片:
CGContextDrawImageWithOptions方法中典尾,調(diào)用了PNGPlugin庫中的一系列方法,沒有明顯看到帶有decode關(guān)鍵字的方法糊探,猜測png_read_IDAT_dataApple就是執(zhí)行的解碼過程钾埂。
接著看下JPEG格式的圖片:
CGContextDrawImageWithOptions方法中,調(diào)用了AppleJPEGPlugin庫中的一系列方法科平,可以看到帶有decode關(guān)鍵字的方法FigPhotoJPEGDecodeJPEGIntoRGBSurface褥紫,這個應(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;
}
這里有幾個參數(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(圖像解碼將在渲染時發(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
同時為了測試圖片的解碼過程,我們將代碼中kCGImageSourceShouldCacheImmediately對應(yīng)的值修改為kCFBooleanTrue啦撮,也就是創(chuàng)建圖片過程中進(jìn)行解碼谭网。
根據(jù)Time Profiler我們查看下系統(tǒng)都在這個函數(shù)里面做了什么,調(diào)用結(jié)果如下:
可以看到赃春,- (UIImage)resizeWithData:(NSData)data scaleSize:(CGSize)size方法執(zhí)行了48.00ms愉择,其中CGImageSourceCreateThumbnailAtIndex執(zhí)行了大約45.00ms,大部分耗時都在這里织中。我們看下里面究竟做了什么锥涕。過程中系統(tǒng)調(diào)用了CGContextDrawImageWithOptions。因?yàn)槲覀兦懊嬖O(shè)置了kCGImageSourceShouldCacheImmediately對應(yīng)的值修改為kCFBooleanTrue狭吼,也就是需要解碼层坠,所以這里系統(tǒng)調(diào)用了CGContextDrawImageWithOptions方法,會將圖片渲染到畫布搏嗡,這個過程是會解碼的窿春。那么接著往下看拉一,具體解碼的步驟在哪里【善颍可以看到接下來最耗時的操作分別在img_interpolate_extent和img_interpolate_read兩個函數(shù)蔚润。然后分別看看這兩個函數(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,這個方法上面已經(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)過解碼時的調(diào)用過程。
將kCGImageSourceShouldCacheImmediately對應(yīng)的值修改為kCFBooleanFalse稽荧,也就是創(chuàng)建圖片過程中不進(jìn)行解碼橘茉。
首先還是看下PNG格式的圖片,512x384.png
然后就震驚了R陶伞3┳俊!有沒有s瘛N膛恕!居然跟之前強(qiáng)制解碼的一模一樣<哒0萋怼渗勘!展開圖中標(biāo)出的兩個方法。
真的是也會解碼A┟АM埂!這究竟是為什么呢扮超?
難道是
CFDictionaryRef options = (__bridge CFDictionaryRef) @{
(__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,
(__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,
(__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,
(__bridgeid)kCGImageSourceThumbnailMaxPixelSize: [NSNumbernumberWithFloat:maxPixelSize]
};
這個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出刷,同時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崩侠,同時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,同時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裸删,同時kCGImageSourceShouldCacheImmediately設(shè)置為kCFBooleanTure,那么不管是PNG還是JPEG格式的圖片阵赠,也都進(jìn)行了解碼操作涯塔。
總結(jié):
1、在使用CGImageSourceCreateThumbnailAtIndex方法時清蚀,如果設(shè)置了kCGImageSourceThumbnailMaxPixelSize匕荸,那么肯定會進(jìn)行解碼操作,生成對應(yīng)的新圖CGImageRef枷邪。
2榛搔、如果不設(shè)置kCGImageSourceThumbnailMaxPixelSize,那么是否進(jìn)行解碼操作东揣,取決于kCGImageSourceShouldCacheImmediately對應(yīng)的值是kCFBooleanTure還是kCFBooleanFalse践惑。
ImageI/O中使用CGImageSourceCreateThumbnailAtIndex創(chuàng)建縮略圖方法的結(jié)論就是如上所述。那么這里又有另一個思考嘶卧,如果是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時芥吟,PNG和JPEG都沒有進(jìn)行解碼侦铜。
情況二:
CFDictionaryRef options = (__bridge CFDictionaryRef) @{
(__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanTrue,
(__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse
};
實(shí)驗(yàn)如下:
情況二結(jié)論:設(shè)置kCGImageSourceShouldCacheImmediately為kCFBooleanTrue時专甩,PNG和JPEG都進(jìn)行了解碼。
總結(jié):在使用CGImageSourceCreateImageAtIndex方法創(chuàng)建CGImageRef時钉稍,kCGImageSourceShouldCacheImmediately值會影響是否開啟解碼操作涤躲。ImageI/O默認(rèn)的kCGImageSourceShouldCacheImmediately為kCFBooleanFalse,也就是說創(chuàng)建圖片時候不解碼嫁盲,會等到圖片被渲染的時候才進(jìn)行解碼篓叶。
以上部分就明確了CGImageSourceCreateImageAtIndex和CGImageSourceCreateThumbnailAtIndex時系統(tǒng)底層具體的實(shí)現(xiàn)。
當(dāng)然羞秤,在使用這個- (UIImage*)resizeWithData:(NSData*)data scaleSize:(CGSize)size
方法時候也采坑了缸托。因?yàn)橐话愦蟛糠智闆r下(參考圖片縮放使用UIKIt、Core Graphics瘾蛋、Core Foundation等情況下的方法)俐镐,是為UIImage添加一個分類,使用分類方法進(jìn)行縮放哺哼。那么既然如此佩抹,為什么不同樣使用分類呢?嗯取董,不錯的想法棍苹,筆者剛開始是這樣做的:
- (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;
}
干凈漂亮,直接跑起來茵汰,美滋滋枢里。
但是!u逦纭栏豺!結(jié)果返回的圖片為nil。
為什么豆胸?百思不得其解奥洼,詳細(xì)了解了下CGDataProvider的一系列API,CGimage的dataProvider晚胡,指的是CGImageCreate時候灵奖,傳入的承載了Bitmap Buffer數(shù)組的一個提供者,可以是一個內(nèi)存中的buffer搬泥,也可以是一個callback來實(shí)現(xiàn)惰性解碼桑寨。也就是說,這個傳入的Bitmap Buffer數(shù)組忿檩,必須是未經(jīng)過解壓縮的數(shù)據(jù)尉尾。如果是經(jīng)過了解壓縮的圖片數(shù)據(jù),那么傳給ImageI/0是沒有意義的燥透。
問題真的出在這里嗎沙咏?這兩個方法參數(shù)不同之處是辨图,一個是使用UIImage *image = [UIImage imageWithContentsOfFile:path],傳入UIImage對象肢藐,而另一個是通過NSData *data = [NSData dataWithContentsOfFile:path]故河,傳入的NSData對象。
那么這兩個方法本質(zhì)的區(qū)別到底是什么呢吆豹?為什么造成不同的結(jié)局呢鱼的?系統(tǒng)在這兩個方法里面具體都干了什么呢?
首先痘煤,拿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對象是無效的了瘪松。
那么同時可以看下PNG格式的圖片,在使用[UIImage imageWithContentsOfFile:]時系統(tǒng)都做了什么。
PNG格式的圖片,同樣是調(diào)用CGImageSourceCreateImageAtIndex等方法猬仁,同時可以看到使用的是PNGPlugin庫相關(guān)的方法,PNGReadPlugin讀取文件數(shù)據(jù)進(jìn)行解壓縮先誉。
有興趣的同學(xué)可以看下[UIImage imageWithNamed:]方法創(chuàng)建的UIImage對象,至于[UIImage imageWithNamed:]和[UIImage imageWithContentsOfFile:]的具體區(qū)別的烁,會另起一篇文章進(jìn)行分析褐耳。