SDWebImage-解碼、壓縮圖像

1、簡單介紹html

研究了下SDWebImage的源碼,借鑑了別人的一些資料,感受逐漸的明白的一些原理,如今就來記錄下。算法

在咱們使用 UIImage 的時候,建立的圖片一般不會直接加載到內存,而是在渲染的時候默認在主線程上再進行解碼並加載到內存。這就會致使 UIImage 在渲染的時候效率上不是那麼高效。爲了提升效率因此在SDWebImage中就採起在子線程中進行解碼圖片。安全

這裏再介紹下爲何建立圖像的時候是須要解碼的由於通常下載的圖片或者是咱們手動拖進工程的圖片都是PNG 或者JPEG或者是其餘格式的圖片,這些圖片都是通過編碼壓縮後的圖片數據,並非咱們的控件能夠直接顯示的位圖,若是咱們直接使用加載渲染圖片到手機上的時候,系統默認會在主線程當即進行圖片的解碼工做,這個過程就是把圖片數據解碼成能夠供給控件直接顯示的位圖數據,因爲這個解碼操做比較耗時,而且默認是在主線程進行,因此若是加載過多的圖片的話確定是會發生卡頓現象的。app

2、源碼分析框架

首先介紹的是根據data來解碼成一個UIImage對象的方法ide

- (UIImage *)decodedImageWithData:(NSData *)data {
    if (!data) {
        return nil;
    }
    
    UIImage *image = [[UIImage alloc] initWithData:data];
    //若是是MAC端就直接返回image
#if SD_MAC
    return image;
#else
    if (!image) {
        return nil;
    }
    //否則的話就要去獲取數據中圖片的方向
    UIImageOrientation orientation = [[self class] sd_imageOrientationFromImageData:data];
    //若是圖片的方向不是默認向上的話就要去根據其圖片信息的方向來從新建立圖片
    if (orientation != UIImageOrientationUp) {
        image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:orientation];
    }
    
    return image;
#endif
}

這裏再介紹下關於獲取圖片方向的sd_imageOrientationFromImageData方法,這裏面其實就是根據imageData去建立CGImageSourceRef而後去讀取其圖像的屬性函數

+ (UIImageOrientation)sd_imageOrientationFromImageData:(nonnull NSData *)imageData {
    UIImageOrientation result = UIImageOrientationUp;
    //建立從Core Foundation 數據對象中讀取的圖像源
    /**
     參數1:
     參數2:指定額外建立option字典。咱們能夠在options字典中包含的鍵來建立圖像源。
     好比說
     kCGImageSourceTypeIdentifierHint
     kCGImageSourceShouldAllowFloat
     kCGImageSourceShouldCache
     kCGImageSourceCreateThumbnailFromImageIfAbsent
     kCGImageSourceCreateThumbnailFromImageAlways
     kCGImageSourceThumbnailMaxPixelSize
     kCGImageSourceCreateThumbnailWithTransform
     */
    CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
    if (imageSource) {
        
        //調用CGImageSourceCopyPropertiesAtIndex的時候會纔去讀取圖像元數據
        //返回圖像源中指定位置的圖像的屬性。
        /**
         參數1:一個圖像的來源
         參數2:你想要得到的屬性的索引。該指數是從零開始的。index參數設置獲取第幾張圖像
         參數3:能夠用來請求其餘選項的字典。
         返回包含與圖像相關聯的屬性的字典。請參見CGImageProperties,以得到能夠在字典中使用的屬性列表。
         CGImageProperties引用定義了表明圖像I/O框架使用的圖像特徵的常量。
         */
        CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
       //判斷屬性存不存在,若是不存在就用默認的UIImageOrientationUp方向
       if (properties) {
            
            //typedef const void *CFTypeRef;
            CFTypeRef val;
            NSInteger exifOrientation;
            
            //返回與給定鍵關聯的值,這裏就是返回方向鍵值所對應的內容
            val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
            //若是其存在的話,就去獲取
            if (val) {
                //將CFNumber對象轉換爲指定類型的值
                /**
                 參數1:要檢查的CFNumber對象。
                 參數2:指定要返回的數據類型的常量。請參閱CFNumberType以得到可能的值列表。
                 參數3:返回的時候包含數字的值
                 */
                CFNumberGetValue(val, kCFNumberNSIntegerType, &exifOrientation);
                
                //轉換exif中信息的方向到iOS裏面的方向
                result = [SDWebImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation];
            } // else - if it's not set it remains at up
            CFRelease((CFTypeRef) properties);
        } else {
            //NSLog(@"NO PROPERTIES, FAIL");
        }
     //釋放這個圖像源
     CFRelease(imageSource);
    }
  //返回結果  
  return result;
}

這裏再介紹的介紹下imageOrientationFromEXIFOrientation的方法,這個方法就是轉換一個EXIF信息中圖像方向到iOS中的方向說白了就是從NSInteger轉換爲UIImageOrientationUp這樣的枚舉值。源碼分析

// Convert an EXIF image orientation to an iOS one.
+ (UIImageOrientation)imageOrientationFromEXIFOrientation:(NSInteger)exifOrientation {
    // CGImagePropertyOrientation is available on iOS 8 above. Currently kept for compatibility
    UIImageOrientation imageOrientation = UIImageOrientationUp;
    switch (exifOrientation) {
        case 1:
            imageOrientation = UIImageOrientationUp;
            break;
        case 3:
            imageOrientation = UIImageOrientationDown;
            break;
        case 8:
            imageOrientation = UIImageOrientationLeft;
            break;
        case 6:
            imageOrientation = UIImageOrientationRight;
            break;
        case 2:
            imageOrientation = UIImageOrientationUpMirrored;
            break;
        case 4:
            imageOrientation = UIImageOrientationDownMirrored;
            break;
        case 5:
            imageOrientation = UIImageOrientationLeftMirrored;
            break;
        case 7:
            imageOrientation = UIImageOrientationRightMirrored;
            break;
        default:
            break;
    }
    return imageOrientation;
}

接下來開始講下解碼圖像,在這裏面其實剛開始先判斷能不能解碼圖片,這個方法是這樣的優化

+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
    // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
    //若是圖片都爲空,那確定返回的是NO
    if (image == nil) {
        return NO;
    }
    //不能編碼動畫圖片
    // do not decode animated images
    if (image.images != nil) {
        return NO;
    }
    
    CGImageRef imageRef = image.CGImage;
    
    BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
    //不支持解碼含有透明度的圖片
    // do not decode images with alpha
    if (hasAlpha) {
        return NO;
    }
    
    return YES;
}

回到這個方法,其實主要的過程就是CGBitmapContextCreate建立一個位圖上下文→CGContextDrawImage繪製原始位圖到上下文→CGBitmapContextCreateImage建立解碼後的新位圖。動畫

- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
    if (![[self class] shouldDecodeImage:image]) {
        return image;
    }
    
    // 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];
    //新建自動釋放池,將bitmap context和臨時變量都添加到池中在方法末尾自動釋放以防止內存警告
    @autoreleasepool{
        
        //獲取傳入的UIImage對應的CGImageRef(位圖)
        CGImageRef imageRef = image.CGImage;
        //獲取彩色空間
        CGColorSpaceRef colorspaceRef = [[self class] colorSpaceForImageRef:imageRef];
        
        //獲取高和寬
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        
        // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
        // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
        // to create bitmap graphics contexts without alpha info.
        
        //初始化bitmap graphics context 上下文
        /*
         參數1:指向要呈現繪圖的內存中目標的指針。這個內存塊的大小至少應該是(bytesPerRow*height)字節。
         若是但願此函數爲位圖分配內存,則傳遞NULL。這將使您沒必要管理本身的內存,從而減小內存泄漏問題。
        參數2:所需寬度,以像素爲單位
        參數3:所需高度
        參數4:用於內存中一個像素的每一個組件的比特數
        參數5:位圖中每一行使用的內存字節數。若是數據參數爲NULL,傳遞值爲0,則會自動計算值。
        參數6:顏色空間
        參數7:指定位圖是否應該包含一個alpha通道、alpha通道在一個像素中的相對位置,以及關於像素組件是浮點數仍是整數值的信息。
              指定alpha通道信息的常量使用CGImageAlphaInfo類型聲明,能夠安全地傳遞給該參數。
              您還能夠傳遞與CGBitmapInfo類型相關聯的其餘常量。
              例如,如何指定顏色空間、每一個像素的位元、每一個像素的位元以及位圖信息,請參閱圖形上下文。
         */
        //kCGBitmapByteOrderDefault 是默認模式,對於iPhone 來講,採用的是小端模式
        CGContextRef context = CGBitmapContextCreate(NULL,
                                                     width,
                                                     height,
                                                     kBitsPerComponent,
                                                     0,
                                                     colorspaceRef,
                                                     kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
        //若是上下文爲NULL,就返回image
        if (context == NULL) {
            return image;
        }
        
        
        // Draw the image into the context and retrieve the new bitmap image without alpha
        /**
        這裏建立的contexts是沒有透明因素的。在UI渲染的時候,其實是把多個圖層按像素疊加計算的過程,須要對每個像素進行 RGBA 的疊加計算。
        當某個 layer 的是不透明的,也就是 opaque 爲 YES 時,GPU 能夠直接忽略掉其下方的圖層,這就減小了不少工做量。
        這也是調用 CGBitmapContextCreate 時 bitmapInfo 參數設置爲忽略掉 alpha 通道的緣由。並且這裏主要針對的就是解碼圖片成位圖
        */
     
        //將CGImageRef對象畫到上面生成的上下文中,且將alpha通道移除
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        
        //使用上下文建立位圖
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        //從位圖建立UIImage對象,返回含有指定的方向和scale的圖片
        UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
        //釋放CG對象
        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);
        
        return imageWithoutAlpha;
    }
}

下面再介紹下關於SDWebImage中的圖片壓縮的算法,其實簡單來講就是將圖像矩陣按照規則分割成小型子矩陣進行壓縮,而後插值拼接,並且這個算法也是借鑑蘋果的,官方Demo連接:https://developer.apple.com/library/content/samplecode/LargeImageDownsizing/Introduction/Intro.html  關於這個算法,蘋果的定義就是此代碼示例演示了一種支持在有限的內存環境中顯示超大圖像的方法,方法是將磁盤上的大圖像轉換爲內存中較小的圖像。這在原始圖像太大而沒法按照要顯示的要求放入內存的狀況下頗有用 我目前也只能理解個大概,還有些細節方面還沒想到,它是怎麼進行優化的,代碼都有註釋。如今先簡單的介紹下回用到的宏吧。

// 每一個像素佔4個字節大小 共32位
static const size_t kBytesPerPixel = 4;
//每一個通道由8位組成
static const size_t kBitsPerComponent = 8;
 
/*
 * Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set
 * Suggested value for iPad1 and iPhone 3GS: 60.
 * Suggested value for iPad2 and iPhone 4: 120.
 * Suggested value for iPhone 3G and iPod 2 and earlier devices: 30.
 該參數用於設置內存佔用的最大字節數。默認爲60MB,下面給出了一些舊設備的參考數值。若是圖片大小大於該值,則將圖片以該數值爲目標進行壓縮。
 */
static const CGFloat kDestImageSizeMB = 60.0f;
 
/*
 * 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.
 設置壓縮時對於源圖像使用到的*塊*的最大字節數。原圖方塊的大小,這個方塊將會被用來分割原圖,默認設置爲20M。
 */
static const CGFloat kSourceImageTileSizeMB = 20.0f;
//1M有多少字節
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
//1M有多少像素 262144個像素
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
//目標總像素kDestImageSizeMB爲60MB
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;
//目標圖像的像素點個數
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;
//目標重疊像素大小
static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to overlap the seems where tiles meet.

關於算法的描述,位圖其實簡單能夠描述爲是由像素組成的矩陣,因此下面其實就是把圖像當作一個矩陣(或多個矩陣的組合)來進行處理的。

- (nullable UIImage *)sd_decompressedAndScaledDownImageWithImage:(nullable UIImage *)image {
    
    //一、判斷圖片是否支持解碼
    if (![[self class] shouldDecodeImage:image]) {
        return image;
    }
    //二、判斷圖片是否支持縮小也就是壓縮,總像素要大於15728640才能壓縮,也就是kDestTotalPixels的大小
    if (![[self class] shouldScaleDownImage:image]) {
        return [self sd_decompressedImageWithImage:image];
    }
    
    //聲明壓縮目標用的上下文
    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 {
        //3. 獲取源圖像位圖
        CGImageRef sourceImageRef = image.CGImage;
        //4. 源圖像尺寸,存儲在CGSize結構體中
        CGSize sourceResolution = CGSizeZero;
        sourceResolution.width = CGImageGetWidth(sourceImageRef);
        sourceResolution.height = CGImageGetHeight(sourceImageRef);
        //5. 計算源圖像總的像素點個數
        float 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.
        
        //6. 獲取原圖像和目標圖像的比例(以像素點個數爲基準),這裏是以60MB的像素點爲標準了 60MB的總像素要除以原文件的總像素小於1的
        float imageScale = kDestTotalPixels / sourceTotalPixels;
        
        //7. 使用imagescale計算目標圖像的寬高,因此我目標圖像的目標就是到60MB
        CGSize destResolution = CGSizeZero;
        destResolution.width = (int)(sourceResolution.width*imageScale);
        destResolution.height = (int)(sourceResolution.height*imageScale);
        
        //8. 進行圖像繪製前的準備工做
        // current color space
        CGColorSpaceRef colorspaceRef = [[self class] colorSpaceForImageRef:sourceImageRef];
        
        // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
        // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
        // to create bitmap graphics contexts without alpha info.
        //建立位圖上下文
        destContext = CGBitmapContextCreate(NULL,
                                            destResolution.width,
                                            destResolution.height,
                                            kBitsPerComponent,
                                            0,
                                            colorspaceRef,
                                            kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
        
        if (destContext == NULL) {
            return image;
        }
        /*9. 設置圖像插值的質量爲高,設置圖形上下文的插值質量水平CGContextSetInterpolationQuality容許上下文以各類保真度水平內插像素。
         在這種狀況下,kCGInterpolationHigh經過最佳結果*/
        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.
        
        //因爲iOS從磁盤檢索圖像數據的方式,咱們使用源圖像寬度與源圖像的寬度相等
        // 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必須在全寬度的「波段」中從磁盤上解碼圖像,即便當前的圖形上下文被剪切到band內的一個subrect中。所以,咱們充分利用了全部的像素數據,
        這些數據是由解碼操做產生的,經過將咱們的平鋪大小與輸入圖像的寬度匹配。
        */
        // 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.
        //10. 定義一個稱爲*塊*的增量矩形(incremental blits,即矩形大小在每一次迭代後都不斷增加/減少)用於計算從源圖像到目標圖像的輸出。
        CGRect sourceTile = CGRectZero;
        
        //源塊的寬度等於源圖像的寬度,寬度要保持必定
        sourceTile.size.width = sourceResolution.width;
        
        //  塊的高度是動態的,咱們前面指定了源tile的值,也就是kTileTotalPixels目標圖像的像素點個數 根據寬度計算動態的高度
        // 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)(kTileTotalPixels / sourceTile.size.width );
        
        // *塊*的起始x值老是爲0
        sourceTile.origin.x = 0.0f;
        //輸出的tile與輸入的tile比例相同,但圖像按比例縮放。圖像按比例縮放就要用到插值運算了
        // 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.
        //十一、計算源圖像與壓縮後目標圖像重疊的像素大小。這裏就是按照sourceResolution.height和destResolution.height進行相比
        float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
        
        CGImageRef sourceTileImageRef;
        //計算組裝輸出圖像所需的讀/寫操做數
        // calculate the number of read/write operations required to assemble the
        // output image.
        //源圖像的高度除以分割源圖像的方塊的高度得出源圖像被分割成多少個方塊並賦值給 iterations,再作取餘運算取得分割的最大的整數
        int iterations = (int)( sourceResolution.height / sourceTile.size.height );
        
        //十二、若是tile 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 變量 sourceTitleHeightMinusOverlap 存放那個用來分割源圖像,大小爲 20 MB 的方塊的高度。
        float sourceTileHeightMinusOverlap = sourceTile.size.height;
        
         //用於切割源圖像大小爲 20 MB 的方塊的高度加上源圖像與源圖像分割方塊的像素重疊數
        sourceTile.size.height += sourceSeemOverlap;
        
        //destTile.size.height = sourceTile.size.height * imageScale;
        //目標圖像的分割方塊的高度加上 kDestSeemOverlap(像素重疊數賦值爲 2)
        destTile.size.height += kDestSeemOverlap;
        
        //1三、進行for 循環,y 從0開始,到小於源圖像被分割的塊數
        for( int y = 0; y < iterations; ++y ) {
            @autoreleasepool {
                //sourceTile 和 destTile 都是寬度和高度固定的,x 值爲 0,只有 y  值隨着循環的 y  值在變化,sourceTile 的 y 值在遞增,destTile 的 y 值在遞減,只有最最後一次循環中,若是有餘數那麼size就會發生變化,這是由於最後一次中去取源圖像的高度其實
                
                //sourceTileHeightMinusOverlap = sourceTile.size.height;
                sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
                destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
                /*1四、在用到在一張圖片中截取某一部分圖片中,用到CGImageRef類中的CGImageCreateWithImageInRect函數
                而後循環的從源圖像的 sourceImageRef 根據大小爲 20 MB 的分割塊的不一樣 CGRect 的矩形區域內獲取 sourceTileImageRef,這裏sourceTile的高度是根據kTileTotalPixels / sourceTile.size.width
                 */
                sourceTileImageRef = CGImageCreateWithImageInRect(sourceImageRef, sourceTile);
                
                //計算剩餘的像素,所採用的方法
                if( y == iterations - 1 && remainder ) {
                //destTile.size.height = sourceTile.size.height * imageScale;
                    float dify = destTile.size.height;
                  
                    destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
                    dify -= destTile.size.height;
                    destTile.origin.y += dify;
                }
                //1五、繪製圖像到圖形上下文指定的destTile範圍中
                CGContextDrawImage( destContext, destTile, sourceTileImageRef );
                CGImageRelease( sourceTileImageRef );
            }
        }
        
        CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
        CGContextRelease(destContext);
        if (destImageRef == NULL) {
            return image;
        }
    //1六、輸出圖像
    UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
        CGImageRelease(destImageRef);
        if (destImage == nil) {
            return image;
        }
        return destImage;
    }
}

壓縮過程簡單流程

相關文章
相關標籤/搜索