美文网首页objc源码探究
SDWebImage源码阅读-图片处理(图片解压缩)

SDWebImage源码阅读-图片处理(图片解压缩)

作者: HoooChan | 来源:发表于2019-10-16 18:58 被阅读0次

    解码

    SDWebImageDownloaderOperationdidCompleteWithError中图片下载完成,开始解析图片:

          ......
          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通道
    • kCGImageAlphaNoneSkipLastkCGImageAlphaNoneSkipFirst :有alpha通道,但是忽略了alpha值,即透明度不起作用。两者的区别是alpha通道所在的位置
    • kCGImageAlphaLastkCGImageAlphaFirst:有alpha通道,且alpha通道起作用,两者的区别是alpha通道所在的位置不同
    • kCGImageAlphaPremultipliedLastkCGImageAlphaPremultipliedFirst :有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
    

    看看不同的像素格式下,一个像素是被如何表示的:

    image

    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

    
    

    相关文章

      网友评论

        本文标题:SDWebImage源码阅读-图片处理(图片解压缩)

        本文链接:https://www.haomeiwen.com/subject/ncnumctx.html