解码
在SDWebImageDownloaderOperation
的didCompleteWithError
中图片下载完成,开始解析图片:
......
dispatch_async(self.coderQueue, ^{
@autoreleasepool {
UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
CGSize imageSize = image.size;
if (imageSize.width == 0 || imageSize.height == 0) {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
} else {
[self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
}
[self done];
}
});
......
coderQueue
是个串行队列:
_coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);
接下来调用图片解码的方法SDImageLoaderDecodeImageData
。
先不考虑动图的解码,这里首先获取图片的scale
。scale可以在请求图片的context
中通过SDWebImageContextImageScaleFactor
设置。如果没有特别指定,则通过SDImageScaleFactorForKey(cacheKey)
方法获取。cacheKey
默认就是图片的地址,SDImageScaleFactorForKey
根据图片地址中是否含有@2x、@3x、%402x、%403x来决定图片的scale,默认是1。
之后调用SDImageCodersManager
来解析图片,将NSData转为UIImage:
image = [[SDImageCodersManager sharedManager] decodedImageWithData:imageData options:coderOptions];
SDImageCodersManager
中有一个数组来保存Decoder:_imageCoders
。_imageCoders的初始化:
_imageCoders = [NSMutableArray arrayWithArray:@[[SDImageIOCoder sharedCoder], [SDImageGIFCoder sharedCoder], [SDImageAPNGCoder sharedCoder]]];
默认定义了三个ImageCoder。SDImageCodersManager在解析图片时会先询问ImageCoder是否能解码该格式的图片:
if ([coder canDecodeFromData:data]) {
......
SDImageIOCoder
除了WebP之外基本都能解析。
看看SDImageIOCoder
解析图片的方法:
- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
if (!data) {
return nil;
}
CGFloat scale = 1;
NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
if (scaleFactor != nil) {
scale = MAX([scaleFactor doubleValue], 1) ;
}
UIImage *image = [[UIImage alloc] initWithData:data scale:scale];
image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
return image;
}
基本上就是调用了系统利用NSData
创建UIImage的方法。
解压缩
到这里图片解码已经完成,得到了UIImage,但这时UIImage还不是位图,如果要显示到UIImageView上面,还要经过一次解压缩。如果我们直接把这个UIImage传给UIImageView,那UIImageView会帮我们做这个解压缩的操作,但有可能会卡主线程。如果我们解压缩完成之后再传给UIImageView,那图片显示的效率会高很多。所以接下来SDWebImage开始对图片进行解压缩。我们也可以设置context中的SDWebImageAvoidDecodeImage
来禁止自动解压缩。另外如果是动图也不会进行解压缩:
BOOL shouldDecode = (options & SDWebImageAvoidDecodeImage) == 0;
if ([image conformsToProtocol:@protocol(SDAnimatedImage)]) {
// `SDAnimatedImage` do not decode
shouldDecode = NO;
} else if (image.sd_isAnimated) {
// animated image do not decode
shouldDecode = NO;
}
......
解压缩在SDImageCoderHelper
中的decodedImageWithImage
方法进行。首先判断是否需要进行解压缩。解压缩过的或者动图都不需要解压。SDWebImage用sd_isDecoded
来标记解压缩过的图片。
接下来来到下面这个方法,进行解压缩的操作:
+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
......
这个方法不是很长,核心的函数就是CGBitmapContextCreate
,这个函数用于创建一个位图上下文,用来绘制一张宽 width
像素,高 height
像素的位图:
CGContextRef CGBitmapContextCreate(void *data, size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, CGColorSpaceRef space, uint32_t bitmapInfo);
我们在这个context上绘制的UIImage会被渲染成位图。
位图
位图就是像素数组
,每个像素有固定的格式,称为像素格式
,它由以下三个参数决定:
颜色空间
一个像素中每个独立的颜色分量使用的 bit 数(Bits per component)
透明值(CGBitmapInfo)
颜色空间
颜色空间
是对色彩的一种描述方式,主要有6种:RGB、CMY/CMYK、HSV/HSB、HSI/HSL、Lab、YUV/YCbCr。
比如RGB是通过红绿蓝三原色来描述颜色的颜色空间,R=Red、G=Green、B=Blue。RGB颜色空间下,一个像素由R、G、B三个颜色分量表示,每个分量使用的bit 数就是bpc。若每个分量用8位,那么一个像素共用24位表示,24就是像素的深度
。
最常用的就是RGB和CMYK。同一个色值在不同的颜色空间下表现出来是不同的颜色。
比如我们拿一个RGB格式的图片去打印,会发现打印出来的颜色和我们在电脑上面看到的有色差,这就是因为颜色空间不同导致的,因为打印机的颜色空间是CMYK。
PBC
然后这个的PBC
就是一个像素中每个独立的颜色分量
使用的 bit 数。
颜色分量是什么?比如RGB是通过红绿蓝三原色来描述颜色的颜色空间,R=Red、G=Green、B=Blue,也就是红绿蓝。RGB颜色空间下,一个像素就由R、G、B三个颜色分量表示,这个就是颜色分量。每个分量使用的bit 数就是bpc。
如果每个分量用8位,那么一个像素共用24位表示,24就是像素的深度
。再加上如果有透明度信息,那就是8888,一共有32位也就是4个字节,就是我们前面说的iOS中每个像素所占的字节数。
BitmapInfo
然后还有BitmapInfo
。BitmapInfo就是用来说明每个像素中的bits包含了哪些信息。有以下三个方面:
- 是否包含Alpha通道,如果包含 alpha ,那么 alpha 信息所处的位置,在像素的最低有效位,比如 RGBA ,还是最高有效位,比如 ARGB ;
- 如果包含 alpha ,那么每个颜色分量是否已经乘以 alpha 的值,这种做法可以加速图片的渲染时间,因为它避免了渲染时的额外乘法运算。比如,对于 RGB 颜色空间,用已经乘以 alpha 的数据来渲染图片,每个像素都可以避免 3 次乘法运算,红色乘以 alpha ,绿色乘以 alpha 和蓝色乘以 alpha
- 颜色分量是否为浮点数
iOS中,alpha通道的布局信息是一个枚举值 CGImageAlphaInfo
,有以下几种情况:
typedef CF_ENUM(uint32_t, CGImageAlphaInfo) {
kCGImageAlphaNone, /* For example, RGB. */
kCGImageAlphaPremultipliedLast, /* For example, premultiplied RGBA */
kCGImageAlphaPremultipliedFirst, /* For example, premultiplied ARGB */
kCGImageAlphaLast, /* For example, non-premultiplied RGBA */
kCGImageAlphaFirst, /* For example, non-premultiplied ARGB */
kCGImageAlphaNoneSkipLast, /* For example, RBGX. */
kCGImageAlphaNoneSkipFirst, /* For example, XRGB. */
kCGImageAlphaOnly /* No color data, alpha data only */
};
-
kCGImageAlphaNone
: 无alpha通道 -
kCGImageAlphaOnly
:无颜色数据,只有alpha通道 -
kCGImageAlphaNoneSkipLast
、kCGImageAlphaNoneSkipFirst
:有alpha通道,但是忽略了alpha值,即透明度不起作用。两者的区别是alpha通道所在的位置 -
kCGImageAlphaLast
、kCGImageAlphaFirst
:有alpha通道,且alpha通道起作用,两者的区别是alpha通道所在的位置不同 -
kCGImageAlphaPremultipliedLast
、kCGImageAlphaPremultipliedFirst
:有alpha通道,且alpha通道起作用。这两个值的区别是alpha通道坐在的位置不同。和kCGImageAlphaLast、kCGImageAlphaFirst的区别是:带有Premultiplied,在解压缩的时候就将透明度乘到每个颜色分量上,这样渲染的时候就不用再处理alpha通道,提高了渲染的效率。
对于位图来说,像素格式并不是随意组合的,目前只支持以下有限的 17 种特定组合:
iOS支持的只有8种,除去无颜色空间的和灰色的之外,只剩下RGB的5种,所有iOS 并不支持 CMYK 的颜色空间。
根据苹果官方文档的介绍,如果图片无alpha通道,则应该使用kCGImageAlphaNoneSkipFirst
,如果图片含alpha通道,则应该使用kCGImageAlphaPremultipliedFirst
。
如果我们拿不在列表里面的像素格式去创建位图上下文会创建失败,比如下面这中,bpc为8,但bpp为16:
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder16Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
这是就会得到以下提示:
CGBitmapContextCreate: unsupported parameter combination: set CGBITMAP_CONTEXT_LOG_ERRORS environmental variable to see the details
看看不同的像素格式下,一个像素是被如何表示的:
CGBitmapContextCreate
现在回到系统的CGBitmapContextCreate
函数,看看它的参数分别有什么含义:
CGContextRef CGBitmapContextCreate(void *data, size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, CGColorSpaceRef space, uint32_t bitmapInfo);
data
:一个指针,它应该指向一块大小至少为 bytesPerRow * height
字节的内存。如果 为 NULL
,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL
即可。
**bytesPerRow
**:位图的每一行使用的字节数,大小至少为 width * bytes per pixel
字节。
这里为什么需要指定每行所占的字节数呢?因为大家可能觉得直接就是宽度乘以每个像素所占的直接数就行了。但是这里涉及到一个CPU缓存行对齐的问题。
缓存行对齐。每次内存和CPU缓存之间交换数据都是固定大小,cache line就表示这个固定的长度,一般为64个字节。如果我们的数据是它的倍速,那数据的读取效率就会快很多。
当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行Cache Line Alignment
的优化。
比如我们看一个解压缩完成的图片:
这里的row bytes不是540 * 4 = 2160,而是2176,而且2176刚好能被64整除。
space
就是颜色空间,前面提到过了,这里就是RGB,因为iOS只支持RGB。
然后就是bitmapInfo
。这个参数除了要指定alpha的信息外,就是前面提到的ARGB还是RGBA,另外还需要指定字节顺序
。
字节顺序分为两种:小端模式和大端模式。它是由枚举值 CGImageByteOrderInfo
来表示的:
typedef CF_ENUM(uint32_t, CGImageByteOrderInfo) {
kCGImageByteOrderMask = 0x7000,
kCGImageByteOrderDefault = (0 << 12),
kCGImageByteOrder16Little = (1 << 12),
kCGImageByteOrder32Little = (2 << 12),
kCGImageByteOrder16Big = (3 << 12),
kCGImageByteOrder32Big = (4 << 12)
} CG_AVAILABLE_STARTING(10.0, 2.0);
在iOS中使用的是小端模式,在macOS中使用的是大端模式,为了兼容,使用kCGBitmapByteOrder32Host
,32位字节顺序,该宏在不同的平台上面会自动组装换成不同的模式。32是指数据以32bit为单位(字节顺序)。字节顺序也以32bit为单位排序。
#ifdef __BIG_ENDIAN__
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
#else /* Little endian. */
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
#endif
下面是SD解压缩图片的源码,拿到位图的上下文CGContextRef之后,调用CGContextDrawImage
进行绘制,然后就可以通过CGBitmapContextCreateImage
拿到位图。
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
if (!context) {
return NULL;
}
// Apply transform
CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
CGContextConcatCTM(context, transform);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height
CGImageRef newImageRef = CGBitmapContextCreateImage(context);
CGContextRelease(context);
到这里图片的解压缩就结束了。
再回到SDWebImage,这里图片解压缩结束。
如果再在context中设置了SDWebImageScaleDownLargeImages
,那在解压缩的时候就要做进一步缩放处理。一般来说SDWebImage会保持图片的原始尺寸,但如果图片过大且设置了SDWebImageScaleDownLargeImages
,则会对图片进行缩小。这时候会边解压缩边缩小。具体的实现在下面这个方法:
```objectivec
- (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes {
......
`limitBytes`可以限制图片的大小,如果传入0则使用默认值,就是解压缩后不超过60MB。这里传入的就是0。
先看看下面这段代码:
```objectivec
......
CGFloat destTotalPixels;
CGFloat tileTotalPixels;
if (bytes > 0) {
destTotalPixels = bytes / kBytesPerPixel;
tileTotalPixels = destTotalPixels / 3;
} else {
destTotalPixels = kDestTotalPixels;
tileTotalPixels = kTileTotalPixels;
}
......
这里图片最大限制为60MB,每个像素占4个字节,1MB就有1024 * 1024个字节,那1MB有1024 * 1024 / 4个像素,所以kDestTotalPixels
为1024 * 1024 / 4 * 60,即输出图片的像素。
接着根据目标总像素和原图像素计算目标图片的尺寸:
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;
// Determine the scale ratio to apply to the input image
// that results in an output image of the defined size.
// see kDestImageSizeMB, and how it relates to destTotalPixels.
CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels);
CGSize destResolution = CGSizeZero;
destResolution.width = (int)(sourceResolution.width * imageScale);
destResolution.height = (int)(sourceResolution.height * imageScale);
然后调用前面提到的CGBitmapContextCreate
创建位图上下文。
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
设置图像插值的质量为高。
接下来开始图片缩小,算法的基本流程如下:
基本的思想就是每次压缩一小部分,然后绘制到输出的上下文中。
每次读取的大小定义在kSourceImageTileSizeMB
中:
/*
* Defines the maximum size in MB of a tile used to decode image when the flag `SDWebImageScaleDownLargeImages` is set
* Suggested value for iPad1 and iPhone 3GS: 20.
* Suggested value for iPad2 and iPhone 4: 40.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 10.
*/
static const CGFloat kSourceImageTileSizeMB = 20.f;
知道每次读取的大小,就可以计算每次读取的像素,接着就可以得到读取的矩形区域:
// Now define the size of the rectangle to be used for the
// incremental blits from the input image to the output image.
// we use a source tile width equal to the width of the source
// image due to the way that iOS retrieves image data from disk.
// iOS must decode an image from disk in full width 'bands', even
// if current graphics context is clipped to a subrect within that
// band. Therefore we fully utilize all of the pixel data that results
// from a decoding opertion by achnoring our tile size to the full
// width of the input image.
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
// The source tile height is dynamic. Since we specified the size
// of the source tile in MB, see how many rows of pixels high it
// can be given the input image width.
sourceTile.size.height = (int)(tileTotalPixels / sourceTile.size.width );
sourceTile.origin.x = 0.0f;
// The output tile is the same proportions as the input tile, but
// scaled to image scale.
CGRect destTile;
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;
这里为了防止有空隙,每次会重叠两个像素:
static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
......
// The source seem overlap is proportionate to the destination seem overlap.
// this is the amount of pixels to overlap each tile as we assemble the ouput image.
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
......
// Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations.
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += kDestSeemOverlap;
接下来进入缩小循环:
for( int y = 0; y < iterations; ++y ) {
@autoreleasepool {
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
if( y == iterations - 1 && remainder ) {
float dify = destTile.size.height;
destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
dify -= destTile.size.height;
destTile.origin.y += dify;
}
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
CGImageRelease( sourceTileImageRef );
}
}
主要是两个关键函数:CGImageCreateWithImageInRect( sourceImageRef, sourceTile )
、CGContextDrawImage( destContext, destTile, sourceTileImageRef )
。这里因为坐标系的原因,destTile和sourceTile起始点是相反的。
之后就是调用CGBitmapContextCreateImage(destContext)
来得到位图,再创建UIImage即可。
到这里SDWebImageDownloaderOperation
中的图片解压缩和缩小就结束了。这时结果被返回到SDWebImageManager
。
SDWebImageManager
在写入缓存之前,会对图片做进一步变换处理。我们可以通过context的SDWebImageContextImageTransformer
来指定图片的变换,包括修改图片大小、圆角剪裁、模糊处理等等。SDWebImage提供了一些默认的变换:
SDImageRoundCornerTransformer
SDImageResizingTransformer
SDImageCroppingTransformer
SDImageFlippingTransformer
SDImageRotationTransformer
SDImageTintTransformer
SDImageBlurTransformer
SDImageFilterTransformer`
还可以用`SDImagePipelineTransformer`组合多个变换。
变换完成后,SDWebImage要把转换后的UIImage转为NSData并写入缓存,此时需要SDImageCodersManager对图片进行编码。先看看SDImageIOCoder如何对图片进行编码。
在SDImageCoder的encodedDataWithImage方法中:
首先调用以下方法的得到imageDestination:
CGImageDestinationRef CGImageDestinationCreateWithData(CFMutableDataRef data, CFStringRef type, size_t count, CFDictionaryRef options);
参数注释:
data
:The data object to write to. For more information on data objects, see CFData and Data Objects.type
:The uniform type identifier (UTI) of the resulting image file. See Uniform Type Identifiers Overview for a list of system-declared and third-party UTIs.count
:The number of images (not including thumbnail images) that the image file will contain.options
:Reserved for future use. Pass NULL.
接着设置图片的方向和压缩质量,
NSMutableDictionary *properties = [NSMutableDictionary dictionary];
#if SD_UIKIT || SD_WATCH
CGImagePropertyOrientation exifOrientation = [SDImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation];
#else
CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp;
#endif
properties[(__bridge NSString *)kCGImagePropertyOrientation] = @(exifOrientation);
double compressionQuality = 1;
if (options[SDImageCoderEncodeCompressionQuality]) {
compressionQuality = [options[SDImageCoderEncodeCompressionQuality] doubleValue];
}
properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(compressionQuality);
最后调用CGImageDestinationAddImage
压缩图片:
CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties);
到这里图片转为NSData就结束了。
接下来看看动图的解析
这次是SDImageGIFCoder