SDWebImage主線之圖片解碼

SDWebImage主線設計的解碼大致有兩種:普通解碼和漸進式解碼。本文只對普通解碼進行解析。

普通解碼又分為正常解碼和大圖解碼。


普通解碼

普通解碼從 -[SDWebImageDownloaderOperation URLSession:task:didCompleteWithError:] 發起

更多解析請參考SDWebImage主線梳理(二)

  1. 同步鎖加持,發送兩個通知:SDWebImageDownloadStopNotification, SDWebImageDownloadFinishNotification

  2. 如果 self.callbackBlocks 有 LoaderCompletedBlock(key=kCompletedCallbackKey), 繼續

  3. self.imageData(NSMutableData) 在 didReceiveData: 方法中一點一點拼接的可變data

  4. 異步串行隊列(self.coderQueue),調用 SDImageLoaderDecodeImageData() 解碼imageData,輸出UIImage

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

解碼函數 SDImageLoaderDecodeImageData()

SDImageLoaderDecodeImageData() 函數在 SDImageLoader.m

  1. SDImageScaleFactorForKey(NSString * key),返回一個屏幕倍數因子。

    1. 如果key是普通名字,判斷key包不包含"@2x."或者"@3x.",包含就返回這個倍數因子。
    2. 如果key是一個URL,百分號編碼的下的@ = %40,判斷key包不包含"%402x."或者"%403x."。
  2. 如果不是“僅解碼第一幀”并且是動圖,則裝載動圖的所有幀

  3. 如果不是動圖,-[SDImageCodersManager(sharedManager) decodedImageWithData:imageData options:],參考詳解一

  4. 動圖不解碼

  5. 如果應該解碼,判斷是否應該按比例縮小圖片

  6. 按比例縮小圖片:-[SDImageCoderHelper decodedAndScaledDownImageWithImage:limitBytes:],參考詳解二

  7. 不縮小圖片:-[SDImageCoderHelper decodedImageWithImage:],參考詳解三

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

詳解一

+[SDImageCodersManager sharedManager] 方法

調用 -[SDImageCodersManager new]-[SDImageCodersManager init]

+ (nonnull instancetype)sharedManager {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}


-[SDImageCodersManager init] 方法
  1. 創建可變數組,初始化賦值 SDImageIOCoder, SDImageGIFCoder, SDImageAPNGCoder; 都是調用 sharedCoder 方法獲取, 待展開
  2. 創建信號量線程鎖,保存在 SDImageCodersManager 的 _codersLock 屬性中
- (instancetype)init {
    if (self = [super init]) {
        // initialize with default coders
        _imageCoders = [NSMutableArray arrayWithArray:@[[SDImageIOCoder sharedCoder], [SDImageGIFCoder sharedCoder], [SDImageAPNGCoder sharedCoder]]];
        _codersLock = dispatch_semaphore_create(1);
    }
    return self;
}


-[SDImageCodersManager decodedImageWithData:options:] 方法
  1. 取出(加鎖) _imageCoders, SDImageIOCoder, SDImageGIFCoder, SDImageAPNGCoder;

  2. 反轉遍歷 _imageCoders ,順序為 SDImageAPNGCoder -> SDImageGIFCoder -> SDImageIOCoder

  3. 每個 coder 都調用 canDecodeFromData: 方法,判斷是否是可以解碼的格式

    1. -[SDImageIOAnimatedCoder canDecodeFromData:],取兩個值(圖片格式)進行對比
      1. +[NSData(ImageContentType) sd_imageFormatForImageData:] 獲取data中包含的圖片格式
        1. -[NSData getBytes:ength:] 獲取的一個字節(第一個字節)可以區分圖片格式
      2. (self.class.imageFormat 也就是 +[SDImageAPNGCoder imageFormat]) == SDImageFormatPNG
  4. 如果返回值為YES則 coder 調用 decodedImageWithData:options: 方法,輸出 image。

    1. -[SDImageIOAnimatedCoder decodedImageWithData:options:] ::: 此處并不是解碼的地方,只是將壓縮的圖片二進制流讀取到UIImage中。
  5. 打斷for循環,返回image

// -[SDImageCodersManager decodedImageWithData:options:]
- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
    if (!data) {
        return nil;
    }
    UIImage *image;
    NSArray<id<SDImageCoder>> *coders = self.coders;
    for (id<SDImageCoder> coder in coders.reverseObjectEnumerator) {
        if ([coder canDecodeFromData:data]) {
            image = [coder decodedImageWithData:data options:options];
            break;
        }
    }
    
    return image;
}
// -[SDImageIOAnimatedCoder canDecodeFromData:]
- (BOOL)canDecodeFromData:(nullable NSData *)data {
    return ([NSData sd_imageFormatForImageData:data] == self.class.imageFormat);
}
@implementation NSData (ImageContentType)

+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
    if (!data) {
        return SDImageFormatUndefined;
    }
    // File signatures table: http://www.garykessler.net/library/file_sigs.html
    uint8_t c;
    [data getBytes:&c length:1];
    switch (c) {
        case 0xFF:
            return SDImageFormatJPEG;
        case 0x89:
            return SDImageFormatPNG;
        case 0x47:
            return SDImageFormatGIF;
        case 0x49:
        case 0x4D:
            return SDImageFormatTIFF;
        case 0x52: {
            if (data.length >= 12) {
                //RIFF....WEBP
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
                if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                    return SDImageFormatWebP;
                }
            }
            break;
        }
        case 0x00: {
            if (data.length >= 12) {
                //....ftypheic ....ftypheix ....ftyphevc ....ftyphevx
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(4, 8)] encoding:NSASCIIStringEncoding];
                if ([testString isEqualToString:@"ftypheic"]
                    || [testString isEqualToString:@"ftypheix"]
                    || [testString isEqualToString:@"ftyphevc"]
                    || [testString isEqualToString:@"ftyphevx"]) {
                    return SDImageFormatHEIC;
                }
                //....ftypmif1 ....ftypmsf1
                if ([testString isEqualToString:@"ftypmif1"] || [testString isEqualToString:@"ftypmsf1"]) {
                    return SDImageFormatHEIF;
                }
            }
            break;
        }
    }
    return SDImageFormatUndefined;
}
// -[SDImageIOAnimatedCoder decodedImageWithData:options:]
- (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);
    }
    
#if SD_MAC
    SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data];
    NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
    imageRep.size = size;
    NSImage *animatedImage = [[NSImage alloc] initWithSize:size];
    [animatedImage addRepresentation:imageRep];
    return animatedImage;
#else
    
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    if (!source) {
        return nil;
    }
    size_t count = CGImageSourceGetCount(source);
    UIImage *animatedImage;
    
    BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
    if (decodeFirstFrame || count <= 1) {
        animatedImage = [[UIImage alloc] initWithData:data scale:scale];
    } else {
        NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
        
        for (size_t i = 0; i < count; i++) {
            CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL);
            if (!imageRef) {
                continue;
            }
            
            NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source];
            UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp];
            CGImageRelease(imageRef);
            
            SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration];
            [frames addObject:frame];
        }
        
        NSUInteger loopCount = [self.class imageLoopCountWithSource:source];
        
        animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
        animatedImage.sd_imageLoopCount = loopCount;
    }
    animatedImage.sd_imageFormat = self.class.imageFormat;
    CFRelease(source);
    
    return animatedImage;
#endif
}

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

詳解二-解碼大圖

+[SDImageCoderHelper decodedAndScaledDownImageWithImage:limitBytes:] 方法
  1. +[SDImageCoderHelper shouldDecodeImage:]

    • 已解碼過(判斷關聯屬性sd_isDecoded)的不再解碼;
    • 圖片為nil不解碼;
    • 動圖不解碼;
  2. +[SDImageCoderHelper shouldScaleDownImage:limitBytes:],判斷是否是要比例縮小,如果不需要則直接正常解碼圖片即可

    1. 限制圖片當超過多少字節時需要縮小,可以手動設置也可以走SD的默認值
    2. SD默認當圖片總像素數量超過 262144(60M所擁有的像素數量)需要縮小
    3. 只要限制的像素數量比源像素數量小即需要縮小。PS:太小了也不行,至少要超過(大于) 1M 才需要。
  3. 初始化目標總像素量(destTotalPixels,60M的總像素) 以及目標圖片的單片(tile)總像素量(tileTotalPixels,20M的總像素)

更多的解析看源碼中添加的注釋吧

+ (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes {
#if SD_MAC
    return image;
#else
    if (![self shouldDecodeImage:image]) {
        return image;
    }
    
    if (![self shouldScaleDownImage:image limitBytes:bytes]) {
        return [self decodedImageWithImage:image];
    }
    
    CGFloat destTotalPixels;
    CGFloat tileTotalPixels;
    if (bytes > 0) {
        destTotalPixels = bytes / kBytesPerPixel;
        tileTotalPixels = destTotalPixels / 3;
    } else {
        destTotalPixels = kDestTotalPixels;
        tileTotalPixels = kTileTotalPixels;
    }
    CGContextRef destContext;
    
    // autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
    // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
    @autoreleasepool {
        CGImageRef sourceImageRef = image.CGImage;
        
        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.
        // 此處開方,假設destTotalPixels = 60M(擁有的像素量), sourceTotalPixels = 240M, imageScale = 1/2。
        // 因為dest的總像素量需要寬高相乘才能得到,所以需要比例值需要開方。
        CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels);
        CGSize destResolution = CGSizeZero;
        destResolution.width = (int)(sourceResolution.width * imageScale);
        destResolution.height = (int)(sourceResolution.height * imageScale);
        
        // device color space
        CGColorSpaceRef colorspaceRef = [self colorSpaceGetDeviceRGB];
        BOOL hasAlpha = [self CGImageContainsAlpha:sourceImageRef];
        // iOS display alpha info (BGRA8888/BGRX8888)
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        
        // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
        // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipFirst
        // to create bitmap graphics contexts without alpha info.
        destContext = CGBitmapContextCreate(NULL,
                                            destResolution.width,
                                            destResolution.height,
                                            kBitsPerComponent,
                                            0,
                                            colorspaceRef,
                                            bitmapInfo);
        
        if (destContext == NULL) {
            return image;
        }
        CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
        
        // 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;
        // 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);
        CGImageRef sourceTileImageRef;
        // calculate the number of read/write operations required to assemble the
        // output image.
        int iterations = (int)( sourceResolution.height / sourceTile.size.height );
        // If tile height doesn't divide the image height evenly, add another iteration
        // to account for the remaining pixels.
        int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
        if(remainder) {
            iterations++;
        }
        // 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;
                    // 修正y值
                    dify -= destTile.size.height;
                    destTile.origin.y += dify;
                }
                CGContextDrawImage( destContext, destTile, sourceTileImageRef );
                CGImageRelease( sourceTileImageRef );
            }
        }
        
        CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
        CGContextRelease(destContext);
        if (destImageRef == NULL) {
            return image;
        }
        UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
        CGImageRelease(destImageRef);
        if (destImage == nil) {
            return image;
        }
        destImage.sd_isDecoded = YES;
        destImage.sd_imageFormat = image.sd_imageFormat;
        return destImage;
    }
#endif
}

這個解碼大圖不是SD的原創,是蘋果官方給出的解決方案,提供demo可下載。想要了解更多,就去《Large Image Downsizing》探索偉大航路吧!
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

詳解三-正常解碼

+[SDImageCoderHelper decodedImageWithImage:] 方法
  1. +[SDImageCoderHelper shouldDecodeImage:]:已解碼過(判斷關聯屬性sd_isDecoded)的不再解碼;圖片為nil不解碼;動圖不解碼;

  2. +[SDImageCoderHelper CGImageCreateDecoded:]

  3. +[SDImageCoderHelper CGImageCreateDecoded:orientation:]:輸入image.CGImage,輸出解碼的CGImageRef; 核心解碼函數:CGContextDrawImage()

    1. 獲取圖片的寬高,如果方向是左、右旋轉則交換寬高數據

    2. 判斷是否含有alpha信息

    3. 獲取32位字節順序(kCGBitmapByteOrder32Host這個宏避免考慮大小端問題),保存到位圖信息bitmapInfo

    4. bitmapInfo 按位或添加像素格式(alpha信息)。有alpha選擇kCGImageAlphaPremultipliedFirst(BGRA8888), 無alpha選擇kCGImageAlphaNoneSkipFirst

    5. 調用 +[SDImageCoderHelper colorSpaceGetDeviceRGB] 獲取顏色空間,一個單例

    6. 調用 CGBitmapContextCreate() 函數創建位圖上下文

    7. 調用 SDCGContextTransformFromOrientation() 獲取方向旋轉的 CGAffineTransform,調用 CGContextConcatCTM 關聯到位圖上下文,坐標系轉換

    8. ??解碼:CGContextDrawImage()??

      1. 傳入的 bytesPerRow 參數是 0,正常情況下應傳入 width * bytesPerPixel,但是這里傳入 0 系統會幫助計算,而且系統還加了點小優化。
      2. 可以明顯的看到Xcode檢測到的內存情況和CPU使用情況的變化
    9. 獲取位圖:CGBitmapContextCreateImage(),輸入context,輸出解碼后的CGImageRef。

    10. 釋放上下文context

  4. -[UIImage initWithCGImage:scale:orientation:],CGImage 轉為 UIImage

  5. UIImage 關聯屬性賦值:decodedImage.sd_isDecoded = YES; decodedImage.sd_imageFormat = image.sd_imageFormat;

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

另外再提一句

函數 SDImageCacheDecodeImageData() 也是普通的正常解碼,與我們前面說的解碼函數 SDImageLoaderDecodeImageData() 邏輯基本一致。

函數實現在 SDImageCacheDefine.m 文件中,此文件僅僅只寫了這一個函數,其它什么都沒有。

UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSString * _Nonnull cacheKey, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
    UIImage *image;
    BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
    NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor];
    CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);
    SDImageCoderOptions *coderOptions = @{SDImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDImageCoderDecodeScaleFactor : @(scale)};
    if (context) {
        SDImageCoderMutableOptions *mutableCoderOptions = [coderOptions mutableCopy];
        [mutableCoderOptions setValue:context forKey:SDImageCoderWebImageContext];
        coderOptions = [mutableCoderOptions copy];
    }
    
    if (!decodeFirstFrame) {
        Class animatedImageClass = context[SDWebImageContextAnimatedImageClass];
        // check whether we should use `SDAnimatedImage`
        if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)]) {
            image = [[animatedImageClass alloc] initWithData:imageData scale:scale options:coderOptions];
            if (image) {
                // Preload frames if supported
                if (options & SDWebImagePreloadAllFrames && [image respondsToSelector:@selector(preloadAllFrames)]) {
                    [((id<SDAnimatedImage>)image) preloadAllFrames];
                }
            } else {
                // Check image class matching
                if (options & SDWebImageMatchAnimatedImageClass) {
                    return nil;
                }
            }
        }
    }
    if (!image) {
        image = [[SDImageCodersManager sharedManager] decodedImageWithData:imageData options:coderOptions];
    }
    if (image) {
        BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage);
        if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
            // `SDAnimatedImage` do not decode
            shouldDecode = NO;
        } else if (image.sd_isAnimated) {
            // animated image do not decode
            shouldDecode = NO;
        }
        if (shouldDecode) {
            BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages);
            if (shouldScaleDown) {
                image = [SDImageCoderHelper decodedAndScaledDownImageWithImage:image limitBytes:0];
            } else {
                image = [SDImageCoderHelper decodedImageWithImage:image];
            }
        }
    }
    
    return image;
}

FAQ

-[SDWebImageDownloaderDecryptor decryptedDataWithData:response:] 內部調用私有block處理imageData,并返回NSData賦值給imageData。

  • Q1: 尋找 decryptor 初始化的位置,以及 block 的實現在哪
  • A1: 沒有實現block的地方,默認情況下 decryptor 是nil,此為開發者來指定的屬性

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  • Q2: SDImageLoader.h 和 SDImageLoader.m 文件的構成
  • A2: SDImageLoader.h 只有兩個函數的EXPORT聲明,以及一個協議的聲明;SDImageLoader.m 僅僅是兩個函數的實現。

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  • Q3: SDImageCacheDefine 文件和 SDImageLoader 文件
  • A3: 解碼代碼基本一致,聲明的協議不同。
    解碼:
    SDImageLoader 的解碼實現是 SDImageLoaderDecodeImageData()
    SDImageCacheDefine 的解碼實現是 SDImageCacheDecodeImageData()
    協議:
    SDImageLoader 聲明的協議是 @protocol SDImageLoader
    SDImageCacheDefine 聲明的協議是 @protocol SDImageCache

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  • Q4: 有多少種Coder?
  • A4: Coder大致分兩類:蘋果內建的和SD自己實現的;一共5種;
  1. SDImageIOCoder 蘋果內建,支持PNG, JPEG, TIFF解碼,以及 GIF 第一幀的靜態圖。

    • 使用 UIImage *image = [[UIImage alloc] initWithData:data scale:scale]; 進行解碼
  2. SDImageIOAnimatedCoder 下面三種的基類

  3. SDImageGIFCoder 繼承自 SDImageIOAnimatedCoder

  4. SDImageAPNGCoder 繼承自 SDImageIOAnimatedCoder

  5. SDImageHEICCoder 繼承自 SDImageIOAnimatedCoder

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  • Q5: CGContextSetInterpolationQuality()是在設置什么?
  • A5: 在設置“插值質量”。對圖片的尺寸進行縮放時,由于尺寸不同,所以在生成新圖的過程中像素不可能是一一對應,因此會有插值操作。所謂插值即根據原圖和目標圖大小比例,結合原圖像素信息生成的新的像素的過程。常見的插值算法有線性插值,雙線性插值,立方卷積插值等。

一點點圖片相關小知識

以下內容均來自蘋果官方文檔和官方示例,地址:Quartz 2D Programming Guide.

首先普及一下圖片的關鍵小知識:

顏色空間(ColorSpace, CS)

顏色空間最直白的說就是平時所見到的例如 RGB、CMYK 這些個東西
顏色空間文檔地址

像素格式(Pixel format)

由三個數值組成: Bits per pixel (bpp)、Bits per component (bpc)、Bytes per row
像素格式文檔地址

顏色分量(Color Component);

顏色分量就是顏色空間的各個小組件。比如 RGB 顏色空間,R 是一個顏色分量,同理G和B都是顏色分量。


顏色分量與顏色空間
顏色空間對應的像素格式

每個顏色空間對應的像素格式是規定好的(定死的),即bits per pixel (bpp) 和 bits per component (bpc);下表就是各種顏色空間對應的像素格式:

ColorSpace Pixel format bitmap information constant Availability
Null 8 bpp, 8 bpc kCGImageAlphaOnly Mac OS X, iOS
Gray 8 bpp, 8 bpc kCGImageAlphaNone Mac OS X, iOS
Gray 8 bpp, 8 bpc kCGImageAlphaOnly Mac OS X, iOS
RGB 16 bpp, 5 bpc kCGImageAlphaNoneSkipFirst Mac OS X, iOS
RGB 32 bpp, 8 bpc kCGImageAlphaNoneSkipFirst Mac OS X, iOS
RGB 32 bpp, 8 bpc kCGImageAlphaNoneSkipLast Mac OS X, iOS
RGB 32 bpp, 8 bpc kCGImageAlphaPremultipliedFirst Mac OS X, iOS
RGB 32 bpp, 8 bpc kCGImageAlphaPremultipliedLast Mac OS X, iOS

解釋一下為什么上表中像素格式為啥缺了 Bytes per row 這個指標,是因為 Bytes per row 其實是通過計算確定的,Bytes per row = width * bytes per pixel。 所以有了 bpp 也就同時有了 Bytes per row

參考解碼大圖的官方示例

解碼大圖的官方示例中定義了許多宏,可以讓我們很快的熟悉圖片的相關知識。哪些是可以定死的,哪些是可以通過計算獲得的。

#define bytesPerMB 1048576.0f 
#define bytesPerPixel 4.0f
#define pixelsPerMB ( bytesPerMB / bytesPerPixel ) // 262144 pixels, for 4 bytes per pixel.
#define destTotalPixels kDestImageSizeMB * pixelsPerMB
#define tileTotalPixels kSourceImageTileSizeMB * pixelsPerMB
#define destSeemOverlap 2.0f // the numbers of pixels to overlap the seems where tiles meet.
#define kDestImageSizeMB 60.0f // The resulting image will be (x)MB of uncompressed image data. 
#define kSourceImageTileSizeMB 20.0f // The tile size will be (x)MB of uncompressed image data. 


解碼的核心函數 CGBitmapContextCreate()

翻譯自 CGBitmapContextCreate() 函數的頭文件注釋,俺自己翻的,水平有限,請見諒

原文:The number of components for each pixel is specified by space, which may also specify a destination color profile.
譯文:每個像素的顏色分量數量由“space”指定,它也可以指定目標顏色配置文件。

原文:Note that the only legal case when space can be NULL is when alpha is specified as kCGImageAlphaOnly.
譯文:注意,space為NULL只有一種情況是合法的,就是當alpha被指定為kCGImageAlphaOnly。

原文:The number of bits for each component of a pixel is specified by bitsPerComponent.
譯文:一個顏色分量的比特數由“bitsPerComponent”指定。

原文:The number of bytes per pixel is equal to (bitsPerComponent * number of components + 7)/8.
譯文:一像素擁有的字節數 = (bitsPerComponent * number of components + 7)/8

原文:Each row of the bitmap consists of bytesPerRow bytes, which must be at least width * bytes per pixel bytes; in addition, bytesPerRow must be an integer multiple of the number of bytes per pixel.
譯文:位圖每一行由bytesPerRow 字節構成,bytesPerRow 必須至少是width * bytes per pixel字節。另外,bytesPerRow必須是整型數字乘以一像素擁有的字節數。

原文:data, if non-NULL, points to a block of memory at least bytesPerRow * height bytes.
譯文:data如果非空,要指向一個至少是bytesPerRow * height 字節的內存塊。

原文:If data is NULL, the data for context is allocated automatically and freed when the context is deallocated.
譯文:如果data為空,context的data會被自動分配內存并且在context被銷毀的時候釋放。

原文:bitmapInfo specifies whether the bitmap should contain an alpha channel and how it's to be generated, along with whether the components are floating-point or integer
譯文:bitmapInfo 指定位圖是否應該包含一個alpha通道,以及如何生成它,以及顏色分量是浮點數還是整數。

CGBitmapContextCreate()

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容