標籤:
ImageIO對外開放的對象有CGImageSourceRef、CGImageDestinationRef,不對外開放的對象有CGImageMetadataRef。CoreGraphics中經常與imageIO打交道的對象有CGImageRef和CGDataProvider,接下來看看這五個對象在建立一個UIImage中擔任了哪些角色。
用TimeProfiler一步一步來看建立UIImage過程中內部調用的函數可以協助我們解決問題,由於TimeProfiler統計函數棧為間隔一段時間統計一次,導致沒有記錄下所有函數的調用而且每次函數棧還可能不一致,所以沒法精確判斷函數棧是如何調用的,但是可以大概推測出每步做了什麼。
從CFDataRef到UIImage代碼如下
NSString *resource = [[NSBundle mainBundle] pathForResource:@"the_red_batman" ofType:@"png"];
NSData *data = [NSData dataWithContentsOfFile:resource options:0 error:nil];
CFDataRef dataRef = (__bridge CFDataRef)data;
CGImageSourceRef source = CGImageSourceCreateWithData(dataRef, nil);
CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil);
UIImage *image = [UIImage imageWithCGImage:cgImage];
CGImagSourceCreateWithData
調用了內建函式_CGImageReadCreate,也就是說CGImageSourceRef跟讀取映像資料有關。
CGImageSourceCreateImageAtIndex
調用了_cg_png_read_info和CGImageMetadataCreateMutable,在構建CGImageRef時,讀取了圖片的基礎資料和中繼資料,基礎資料中包括Image的header chunk,比如png的IHDR。中繼資料是由CGImageMetadataRef來抽象的。並且沒有讀取圖片的其他資料,更沒有做解碼的動作。
有趣的是,如果調用CGImageSourceCopyPropertiesAtIndex
CGImageSourceCopyPropertiesAtIndex的內建函式調用了CGImageMetadataRef,如果再加上ImageIO/CGImageMetadata.h檔案的注釋
@description CGImageMetadata APIs allow clients to view and modify metadata
for popular image formats. ImageIO supports the EXIF, IPTC, and XMP
metadata specifications. Please refer to CGImageSource.h for functions to
read metadata from a CGImageSource, and CGImageDestination.h for functions to
write metadata to a CGImageDestination. CGImageDestinationCopyImageSource can
be used to modify metadata without recompressing the image.
說明CGImageMetadataRef抽象出圖片中EXIF、IPTC、XMP格式的中繼資料插入欄位,而若想獲得CGImageMetadataRef必須要通過CGImageSourceRef。
同樣,看看有關CGDataProviderRef的內建函式調用,代碼如下
//將CGImageSourceRef改由CGDataProviderRef建立
CFDataRef dataRef = (__bridge CFDataRef)data;
CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData(dataRef);
CGImageSourceRef source = CGImageSourceCreateWithDataProvider(dataProvider, nil);
//測試由CGImageRef擷取CGDataProviderRef和由CGDataProviderRef建立CGImageRef
CGDataProviderRef newDataProvider = CGImageGetDataProvider(cgImage);
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
CGColorSpaceRef space = CGImageGetColorSpace(cgImage);
size_t bitsPerComponent = CGImageGetBitsPerComponent(cgImage);
size_t bitsPerPixel = CGImageGetBitsPerPixel(cgImage);
size_t bytesPerRow = CGImageGetBytesPerRow(cgImage);
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage);
CGDataProviderRef newDataProvider = CGImageGetDataProvider(cgImage);
CGImageRef newImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, newDataProvider, NULL, false, kCGRenderingIntentDefault);
很可惜,沒有找出有關CGDataProviderRef的函數調用。無法得出CGDataProviderRef做了什麼。
看看有關CGImageDestinationRef的內建函式調用,代碼和內建函式調用如下
CFMutableDataRef buffer = CFDataCreateMutable(kCFAllocatorDefault, 0); CGImageDestinationRef destination = CGImageDestinationCreateWithData(buffer, kUTTypePNG, 1, NULL);
CGImageDestinationAddImage(destination, cgImage, nil);
CGImageDestinationFinalize(destination);
CGImageDestinationRef將圖片資料寫入目的地,並且負責做圖片編碼或者說圖片壓縮。
測試結論
CGImageSourceRef抽象了對讀映像資料的通道,讀取映像要通過它,它自己本身不讀取映像的任何資料,在你調用CGImageSourceCopyPropertiesAtIndex的時候會才去讀取映像中繼資料。
CGImageMetadataRef抽象出圖片中EXIF、IPTC、XMP格式的中繼資料,通過CGImageSourceRef擷取。
CGImageRef抽象了映像的基本資料和中繼資料,建立的時候會通過CGImageSourceRef去讀取映像的基礎資料和中繼資料,但沒有讀取映像的其他資料,沒有做圖片解碼的動作。
CGDataProviderRef沒有得出有用資訊。
CGImageDestinationRef抽象寫入映像資料的通道,寫入映像要通過它,在寫入圖片的時候還負責圖片的編碼。
Image解碼
可以看到從CFDataRef直到建立出UIImage,都沒有調用過對映像解碼的函數,唯讀取了一些映像基礎資料和中繼資料。
Image解碼發生在什麼時候?在ImageIO/CGImageSource.h檔案中kCGImageSourceShouldCache的上面其實有明確注釋。
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).
如果不手動設定Image只會等到在螢幕上渲染時再解碼。經過測試,確實如此。這個kCGImageSourceShouldCacheImmediately還不如起名為kCGImageSourceShouldDecodeImmediately。
Image解碼到底在哪裡做的?
如果在畫布上渲染圖片,圖片一定是會被解碼的。下列代碼再跑測試
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
CGImageRef newImage = CGBitmapContextCreateImage(context);
所有調用的函數名中沒有明顯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再次渲染的時候仍沒有進行解碼,這有些奇怪。如果有同學詳細知道怎麼回事,還請賜教。
最後附上ImageIO的全家福
iOS中的imageIO與image解碼