最近在做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格式的图片:
CGContextDrawImageWithOptions方法中,调用了PNGPlugin库中的一系列方法,没有明显看到带有decode关键字的方法,猜测png_read_IDAT_dataApple就是执行的解码过程。
接着看下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我们查看下系统都在这个函数里面做了什么,调用结果如下:
可以看到,- (UIImage)resizeWithData:(NSData)data scaleSize:(CGSize)size方法执行了48.00ms,其中CGImageSourceCreateThumbnailAtIndex执行了大约45.00ms,大部分耗时都在这里。我们看下里面究竟做了什么。过程中系统调用了CGContextDrawImageWithOptions。因为我们前面设置了kCGImageSourceShouldCacheImmediately对应的值修改为kCFBooleanTrue,也就是需要解码,所以这里系统调用了CGContextDrawImageWithOptions方法,会将图片渲染到画布,这个过程是会解码的。那么接着往下看,具体解码的步骤在哪里。可以看到接下来最耗时的操作分别在img_interpolate_extent和img_interpolate_read两个函数。然后分别看看这两个函数做了什么。
这里系统调用了CGImageProviderCopyImageBlockSet,里面调用了PNGPlugin库的_cg_png_read_row和_cg_png_read_info方法,_cg_png_read_row方法调用了png_read_IDAT_dataApple,这个方法上面已经提到了,是进行的解码操作。
img_interpolate_read里面调用了img_decide_read,猜测应该是读取解码完成的数据。
好了,PNG格式的图片如何解码我们大致推理出来了,那么再看看JPEG格式的图片,512x384.jpg
JPEG图片的- (UIImage)resizeWithData:(NSData)data scaleSize:(CGSize)size方法执行了55.00ms,其中CGImageSourceCreateThumbnailAtIndex执行了大约53.00ms,这里系统同样调用了CGContextDrawImageWithOptions方法,与PNG不同的是,JPEG里面调用了img_decode_stage和img_interpolate_read方法。
可以看到img_decode_stage方法里面同样调用了CGImageProviderCopyImageBlockSet方法,然后调用了AppleJPEGPlugin库的FigPhotoJPEGDecodeJPEGIntoRGBSurface方法,这里进行了解码。
img_interpolate_read里面调用了img_decode_read,跟PNG图片的一模一样,应该也是对解码完成的数据进行读取。
以上就是解码过程的剖析,那么作为对比试验,我们接下来看下不经过解码时的调用过程。
将kCGImageSourceShouldCacheImmediately对应的值修改为kCFBooleanFalse,也就是创建图片过程中不进行解码。
首先还是看下PNG格式的图片,512x384.png
然后就震惊了!!!有没有!!!居然跟之前强制解码的一模一样!!!展开图中标出的两个方法。
真的是也会解码!!!这究竟是为什么呢?
难道是
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]
};
实验结果如下:
情况一结论:如果不设置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]
};
实验结果如下:
情况二结论:如果不设置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]
};
实验结果如下:
情况三结论:如果设置了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]
};
实验结果如下:
情况四结论:如果设置了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
};
实验如下:
情况一结论:设置kCGImageSourceShouldCacheImmediately为kCFBooleanFalse时,PNG和JPEG都没有进行解码。
情况二:
CFDictionaryRef options = (__bridge CFDictionaryRef) @{
(__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanTrue,
(__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse
};
实验如下:
情况二结论:设置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:]都做了什么。
可以看到,在该方法中系统调用了CGImageSourceCreateImageAtIndex方法,在该方法中,系统使用了AppleJPEGPlugin库的一些方法,但是并没有发现decode相关的函数,所以这里应该没有进行解码,而只是将图片进行了解压缩(decompress)。这也就解释了为什么使用CGImageGetDataProvider获取的CGDataProvider对象是无效的了。
那么同时可以看下PNG格式的图片,在使用[UIImage imageWithContentsOfFile:]时系统都做了什么。
PNG格式的图片,同样是调用CGImageSourceCreateImageAtIndex等方法,同时可以看到使用的是PNGPlugin库相关的方法,PNGReadPlugin读取文件数据进行解压缩。
有兴趣的同学可以看下[UIImage imageWithNamed:]方法创建的UIImage对象,至于[UIImage imageWithNamed:]和[UIImage imageWithContentsOfFile:]的具体区别,会另起一篇文章进行分析。