SDWebImage源碼閱讀-圖片處理(圖片解壓縮)

解碼

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


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。