ImageIO对外开放的对象有CGImageSourceRef、CGImageDestinationRef,不对外开放的对象有CGImageMetadataRef。CoreGraphics中经常与imageIO打交道的对象有CGImageRef和CGDataProvider,接下来看看这五个对象在创建一个UIImage中担任了哪些角色。
用TimeProfiler一步一步来看创建UIImage过程中内部调用的函数可以帮助我们解决问题,由于TimeProfiler统计函数栈为间隔一段时间统计一次,导致没有记录下所有函数的调用而且每次函数栈还可能不一致,所以没法精确判断函数栈是如何调用的,但是可以大概推测出每步做了什么。
从CFDataRef到UIImage代码如下
CGImagSourceCreateWithData
调用了内部函数_CGImageReadCreate,也就是说CGImageSourceRef跟读取图像数据有关。
CGImageSourceCreateImageAtIndex
调用了_cg_png_read_info和CGImageMetadataCreateMutable,在构建CGImageRef时,读取了图片的基础数据和元数据,基础数据中包括Image的header chunk,比如png的IHDR。元数据是由CGImageMetadataRef来抽象的。并且没有读取图片的其他数据,更没有做解码的动作。
有趣的是,如果调用CGImageSourceCopyPropertiesAtIndex
CGImageSourceCopyPropertiesAtIndex的内部函数调用了CGImageMetadataRef,如果再加上ImageIO/CGImageMetadata.h文件的注释
说明CGImageMetadataRef抽象出图片中EXIF、IPTC、XMP格式的元数据插入字段,而若想获得CGImageMetadataRef必须要通过CGImageSourceRef。
同样,看看有关CGDataProviderRef的内部函数调用,代码如下
很可惜,没有找出有关CGDataProviderRef的函数调用。无法得出CGDataProviderRef做了什么。
看看有关CGImageDestinationRef的内部函数调用,代码和内部函数调用如下
CGImageDestinationRef将图片数据写入目的地,并且负责做图片编码或者说图片压缩。
测试结论
CGImageSourceRef抽象了对读图像数据的通道,读取图像要通过它,它自己本身不读取图像的任何数据,在你调用CGImageSourceCopyPropertiesAtIndex的时候会才去读取图像元数据。
CGImageMetadataRef抽象出图片中EXIF、IPTC、XMP格式的元数据,通过CGImageSourceRef获取。
CGImageRef抽象了图像的基本数据和元数据,创建的时候会通过CGImageSourceRef去读取图像的基础数据和元数据,但没有读取图像的其他数据,没有做图片解码的动作。
CGDataProviderRef没有得出有用信息。
CGImageDestinationRef抽象写入图像数据的通道,写入图像要通过它,在写入图片的时候还负责图片的编码。
Image解码
可以看到从CFDataRef直到创建出UIImage,都没有调用过对图像解码的函数,只读取了一些图像基础数据和元数据。
Image解码发生在什么时候?在ImageIO/CGImageSource.h文件中kCGImageSourceShouldCache的上面其实有明确注释。
如果不手动设置Image只会等到在屏幕上渲染时再解码。经过测试,确实如此。这个kCGImageSourceShouldCacheImmediately还不如起名为kCGImageSourceShouldDecodeImmediately。
Image解码到底在哪里做的?
如果在画布上渲染图片,图片一定是会被解码的。下列代码再跑测试
所有调用的函数名中没有明显decompress或者decode字眼。而上面四个函数调用的频率是最高的。根据CGImageDestinationRef调用的png_compress_IDAT,猜测png_read_IDAT_data是做解码的函数。
以上是以png图片作为测试用例,下面看看jpeg图片的
很明显AppleJPEG的decode方法是做解码的函数。jpeg与png调用了两个同样函数,而不同的图片调了不同的解码函数。在画布上画图片的时候,会调用ImageProviderCopyImageBlockSetCallback设置callback,然后调用copyImageBlock,再调用设置的callback,但是解码函数是由copyImageBlock的调用的还是由callback调用的无法验证。
那ImageProviderCopyImageBlockSetCallback与CGDataProviderCopyData是否有关系?经过测试,CGDataProviderCopyData内部也会调用ImageProviderCopyImageBlockSetCallback和copyImageBlock。而且CGDataProviderCopyData得到的CFDataRef是解码过的像素数组。
结论:Image解码发生在CGDataProviderCopyData函数内部调用ImageProviderCopyImageBlockSetCallback设置的callback或者copyImageBlock函数,根据不同的图片格式调用的不同的方法中。
Image的初始化方法
imageWithData从内部函数的调用来看,通过CGImageSourceRef访问图像数据,创建CGImageRef。
imageWithContentsOfFile内部调用如下
文件通过mmap到内存然后通过CGImageSourceRef访问图像数据,创建CGImageRef。
imageNamed先从Bundle里找到资源路径,然后同样也是将文件mmap到内存,再通过CGImageSourceRef访问图像数据,创建CGImageRef。
Image的缓存
通过调用不同的UIImage初始化方法然后创建UIImageVIew展示到屏幕上,来看看不同方法是否有缓存的行为。
imageNamed在第二次展示相同image的时候没有调用imageIO的任何方法。
imageWithData和imageWithContentsOfFile在第二次在展示相同image的时候均调用了imageIO的解码方法。
而imageWithData和imageWithContentsOfFile初始化方法创建的UIImage只要不被释放,再次渲染不会调用imageIO解码方法。
结论为UIImage有两种缓存,一种是UIImage类的缓存,这种缓存保证imageNamed初始化的UIImage只会被解码一次。另一种是UIImage对象的缓存,这种缓存保证只要UIImage没有被释放,就不会再解码。
CGImageSourceCreateImageAtIndex方法的kCGImageSourceShouldCache选项指的是第二种缓存,而如果设置为false,我测试出来image再次渲染的时候仍没有进行解码,这有些奇怪。如果有同学详细知道怎么回事,还请赐教。