SDWebImage源碼解析(三)——SDWebImage圖片解碼/壓縮模塊

第三篇的寫在前面

SDWebImage提供了一個用于圖片解碼的類——SDWebImageDecoder。在上一篇文章中,也有提及到在diskImageForKey方法中使用了decoder類的decodedImageWithImage:image方法對圖片進行解壓縮后返回。本篇文章則重點分析這個模塊的源碼。

圖片解碼功能的實現依賴于Quartz 2D的圖像處理庫,如果對這些功能不熟悉的話可以參考一下Quartz 2D Programming Guide。本文章也會對一些知識點進行簡單的講解。

為何需要對圖片進行解碼

Avoiding Image Decompression Sickness這篇文章中描述了一種情況:

Imagine you have a UIScrollView that displays UIImageViews for the individual pages of a catalog or magazine style app. As soon as even one pixel of the following page comes on screen you instantiate (or reuse) a UIImageView and pop it into the scroll view’s content area. That works quite well in Simulator, but when you test this on the device you find that every time you try to page to the next page, there is a noticeable delay. This delay results from the fact that images need to be decompressed from their file incarnation to be rendered on screen. Unfortunately UIImage does this decompression at the very latest possible moment, i.e. when it is to be displayed.

因此,可以假設一種最簡單為一個UIImageView獲取網絡圖片的流程:

  1. 從網絡上請求到壓縮過的圖片(JPEG,PNG...)
  2. 使用這個壓縮過的圖片對UIImage對象進行初始化
  3. 當UIImage要被顯示到UIImageView上面的時候,UIImage上的圖片會被解壓縮,然后顯示到UIImageView上。

所以如何將這個解壓縮的過程提前,文章中指出了幾種思路:

Then there’s the question of “How fast can I get these pixels on screen?”. The answer to this is comprised of 3 main time intervals:

  • time to alloc/init the UIImage with data on disk
  • time to decompress the bits into an uncompressed format
  • time to transfer the uncompressed bits to a CGContext, potentially resizing, blending, anti-aliasing it

SDWebImage中使用以下策略:

  1. 當圖片從網絡中獲取到的時候就進行解壓縮。(未來會提到)
  2. 當圖片從磁盤緩存中獲取到的時候立即解壓縮。(上面已經提到了)

這篇文章中總結了為什么我們需要解碼:

在我們使用 UIImage 的時候,創建的圖片通常不會直接加載到內存,而是在渲染的時候再進行解壓并加載到內存。這就會導致 UIImage 在渲染的時候效率上不是那么高效。為了提高效率通過 decodedImageWithImage方法把圖片提前解壓加載到內存,這樣這張新圖片就不再需要重復解壓了,提高了渲染效率。這是一種空間換時間的做法。

接下來通過源碼對這個解碼過程進行分析。

DecodeWithImage 方法

這個方法傳入一副圖片對該圖片進行解碼,解碼結果是另一幅圖片。

static const size_t kBytesPerPixel = 4;
static const size_t kBitsPerComponent = 8;
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
    if (![UIImage 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 = [UIImage colorSpaceForImageRef:imageRef];
        
        //獲取高和寬
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        //static const size_t kBytesPerPixel = 4
        // 每個像素占4個字節大小 共32位 (RGBA)
        size_t bytesPerRow = kBytesPerPixel * width;

        // 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 上下文
        CGContextRef context = CGBitmapContextCreate(NULL,
                                                     width,
                                                     height,
                                                     kBitsPerComponent,
                                                     bytesPerRow,
                                                     colorspaceRef,
                                                     kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
        if (context == NULL) {
            return image;
        }
        
        // Draw the image into the context and retrieve the new bitmap image without alpha
        //將CGImageRef對象畫到上面生成的上下文中,且將alpha通道移除
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        //使用上下文創建位圖
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        //從位圖創建UIImage對象
        UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
                                                         scale:image.scale
                                                   orientation:image.imageOrientation];
        //釋放CG對象
        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);
        
        return imageWithoutAlpha;
    }
}

簡單來說,就是把UIImage繪制出來的圖像再保存起來就完成了這個解碼的過程。如果不考慮性能損耗,我們甚至可以用以下代碼完成這個任務:

- (void)decompressImage:(UIImage *)image
{
    UIGraphicsBeginImageContext(CGSizeMake(1, 1));
    [image drawAtPoint:CGPointZero];
    UIGraphicsEndImageContext();
}

關于CGImageRef

下面引用蘋果開發者文檔中的描述:

  • Bitmap images and image masks are like any drawing primitive in Quartz. Both images and image masks in Quartz are represented by the CGImageRef data type.A bitmap image (or sampled image) is an array of pixels (or samples).
  • Each pixel represents a single point in the image. JPEG, TIFF, and PNG graphics files are examples of bitmap images.
  • Each sample in a bitmap contains one or more color components in a specified color space, plus one additional component that specifies the alpha value to indicate transparency. Each component can be from 1 to as many as 32 bits.

CGImageRef就是位圖(bitmap image)在Quartz 框架中的具體數據結構。位圖(樣本)是像素的矩形陣列(Rectangular Array),每個像素對應在特定的彩色空間(color space)中的一個或多個彩色元素。關于彩色空間,請參考蘋果開發者文檔中的Color Management Guide。一般我們常用有灰度空間(Gray Spaces)和RGB空間。

關于Bitmap Graphics Context

Bitmap Graphics Context即位圖上下文。用于接收存儲了位圖數據的緩存的指針,當我們位圖上下文進行繪制時,緩存會進行更新。

A bitmap graphics context accepts a pointer to a memory buffer that contains storage space for the bitmap. When you paint into the bitmap graphics context, the buffer is updated. After you release the graphics context, you have a fully updated bitmap in the pixel format you specify.

因此,當我們需要自己繪制一個bitmap圖片時,只需要初始化一個位圖上下文,并在上面繪制自己的圖形,最后從上下文中獲取我們想要的bitmap圖形或者數據即可。

上面說了這么多,其實就是為了解釋decodedImageWithImage:image方法對原始圖片進行了什么樣的操作——將圖片原始圖片繪制到位圖上下文,然后將位圖上下文保存為新的位圖后返回。

圖片壓縮

SDWebImageDecoder還提供了另外一個核心功能——圖片壓縮。如果圖片的體積大于特定值,則decoder會對圖片進行壓縮,防止內存溢出。這部分源碼比較長,其中的壓縮的算法稍微有些復雜,需要仔細閱讀。下面先給出源碼,再做具體的說明。

*
 * Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set
該參數用于設置內存占用的最大字節數。默認為60MB,下面給出了一些舊設備的參考數值。如果圖片大小大于該值,則將圖片以該數值為目標進行壓縮。
 * 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.
 */
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.
 */
static const CGFloat kSourceImageTileSizeMB = 20.0f;
/**下面做算術題*/
//1MB中的字節數
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
//1MB大小圖像中有多少個像素
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
//壓縮的目標圖像的像素點個數
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 *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image {
    //1. 先對圖片解碼
    if (![UIImage shouldDecodeImage:image]) {
        return image;
    }
    //2. 判斷是否需要壓縮(以上面kDestImageSizeMB為標準)
    if (![UIImage shouldScaleDownImage:image]) {
        return [UIImage decodedImageWithImage:image];
    }
    //3. 聲明壓縮目標用的上下文
    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 {
        //4. 獲取源圖像位圖
        CGImageRef sourceImageRef = image.CGImage;
        //5. 源圖像尺寸,存儲在CGSize結構體中
        CGSize sourceResolution = CGSizeZero;
        sourceResolution.width = CGImageGetWidth(sourceImageRef);
        sourceResolution.height = CGImageGetHeight(sourceImageRef);
        //6. 計算源圖像總的像素點個數
        float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
        //7. 獲取原圖像和目標圖像的比例(以像素點個數為基準)
        float imageScale = kDestTotalPixels / sourceTotalPixels;
        //8. 使用scale計算目標圖像的寬高
        CGSize destResolution = CGSizeZero;
        destResolution.width = (int)(sourceResolution.width*imageScale);
        destResolution.height = (int)(sourceResolution.height*imageScale);
        

        //9. 進行圖像繪制前的準備工作
        // current color space
        CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:sourceImageRef];
        
        size_t bytesPerRow = kBytesPerPixel * destResolution.width;
        
        // Allocate enough pixel data to hold the output image.
        void* destBitmapData = malloc( bytesPerRow * destResolution.height );
        if (destBitmapData == NULL) {
            return image;
        }
        
        // 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(destBitmapData,
                                            destResolution.width,
                                            destResolution.height,
                                            kBitsPerComponent,
                                            bytesPerRow,
                                            colorspaceRef,
                                            kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
        
        if (destContext == NULL) {
            free(destBitmapData);
            return image;
        }
        //10. 設置圖像插值的質量為高
        CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
        
        //11. 定義一個稱為*塊*的增量矩形(incremental blits,即矩形大小在每一次迭代后都不斷增長/減小)用于計算從源圖像到目標圖像的輸出。
        //*塊*的寬度和圖片的寬度保持一致,高度動態變化
        CGRect sourceTile = CGRectZero;
        sourceTile.size.width = sourceResolution.width;
        //11.1  *塊*的計算:根據寬度計算動態的高度
        sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
       //11.2  *塊*的起始x值總是為0
        sourceTile.origin.x = 0.0f;
        // 12. 同樣的方式初始化目標圖像的塊
        //寬度 = 目標圖像的寬度
        //高度 = 源圖像塊的高度 * 縮放比例
        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.
        //13. 根據kDestSeemOverlap計算源塊的SeemOverlap常數
        // 計算公式: sourceSeemOverlap = (int)kDestSeemOverlap / imageScale 
        float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
        //14. 聲明源圖像塊的位圖,在循環中繪制在destContext中
        CGImageRef sourceTileImageRef;
        // calculate the number of read/write operations required to assemble the
        // output image.     
        //15. 計算循環次數 
        int iterations = (int)( sourceResolution.height / sourceTile.size.height );
        // 如果不能整除,有余數,則循環次數+1
        // 余數記錄下來
        int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
        if(remainder) {
            iterations++;
        }
        //16. 將overlap常量累加到塊的高度中,保存源圖像塊的高度到sourceTileHeightMinusOverlap
        float sourceTileHeightMinusOverlap = sourceTile.size.height;
        sourceTile.size.height += sourceSeemOverlap;
        destTile.size.height += kDestSeemOverlap;
        //17. 核心部分,開始循環做插值
        for( int y = 0; y < iterations; ++y ) {
            @autoreleasepool {
                //1. 每次循環sourceTile的坐標原點y值 + sourceTileHeightMinusOverlap
                //所以sourceTileHeightMinusOverlap在此作為固定增量存在
                sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
                //2. destTile的坐標原點y值 = 目標圖像的高度 - 固定增量
                destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
                //3. 使用sourceTile矩形內的源圖像初始化sourceTileImageRef
                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;
                }
                //4. 將sourceTileImageRef繪制到destTile矩形的destConext上下文
                // 注意上面我們為destContext設置了插值質量,此時圖像會進行縮放,因此會進行插值操作
                CGContextDrawImage( destContext, destTile, sourceTileImageRef );
                //5. 釋放臨時變量
                CGImageRelease( sourceTileImageRef );
            }
        }
        //18. 收尾工作,繪制圖片,返回UIImage對象
        CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
        CGContextRelease(destContext);
        if (destImageRef == NULL) {
            return image;
        }
        UIImage *destImage = [UIImage imageWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
        CGImageRelease(destImageRef);
        if (destImage == nil) {
            return image;
        }
        return destImage;
    }
}

壓縮算法說明

上述方法內部描述了一個比較隱晦的圖像壓縮算法(是的,沒有用現成的庫調用,所以我稱之為“隱晦”,我猜想可能是為了壓縮代碼量)。首先在上面提到了,位圖其實是由像素組成的矩陣,對于數字圖像處理有研究的話可以知道我們可以把圖像當做一個矩陣(或多個矩陣的組合)進行處理。在SDWebImage的壓縮方法中,使用了一個名為塊(tile/blit)的東西,實際上是就是圖像矩陣的一個子矩陣。由于種種原因,把塊的寬度固定為原圖像(original image not source image)的寬度。
那么這個塊的目的是什么?
先嘗試閱讀第17步中的第3,4步代碼:

//3. 使用sourceTile矩形內的源圖像初始化sourceTileImageRef
 sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
//4. 將sourceTileImageRef繪制到destTile矩形的destConext上下文
 // 注意上面我們為destContext設置了插值質量,此時圖像會進行縮放,因此會進行插值操作
 CGContextDrawImage( destContext, destTile, sourceTileImageRef );

destContext是最后我們要返回的上下文,上面繪制有壓縮后的圖像信息。

第三步:使用CGImageRef CGImageCreateWithImageInRect(CGImageRef image, CGRect rect)方法將源圖像sourceImageRefsourceTile塊內的值賦值給sourceTileImageRef。因此可以將sourceTile看做源圖像的一小塊。

第四步:將上面的塊圖像繪制到destContext上下文的destTile塊中。需要注意到的是,由于sourceTile的大小不等于destTile的大小,因此這里CGContextDrawImage方法會對圖像使用CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh)設置的插值質量進行插值處理。對此在NSHipster上有相關說明:

Next, CGContextSetInterpolationQuality allows for the context to interpolate pixels at various levels of fidelity. In this case, kCGInterpolationHigh is passed for best results. CGContextDrawImage allows for the image to be drawn at a given size and position, allowing for the image to be cropped on a particular edge or to fit a set of image features, such as faces. Finally, CGBitmapContextCreateImage creates a CGImage from the context.

如果對于插值精確度有疑問,可以參考這個問題

上面兩部是壓縮過程的主要內容。如果理解了這部分對整個算法的理解很重要。接下來說明在循環中這個塊(嚴格的說應該是兩個塊——sourceTile和destTile)在程序中如何進行操作。

源碼中定義了幾個與tile有關的常量在下面會使用到:

static const CGFloat kSourceImageTileSizeMB = 20.0f;
//destSeemOverlap
static const CGFloat kDestSeemOverlap = 2.0f;  
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB; 

然后初始化sourceTiledestTile的大小

        CGRect sourceTile = CGRectZero;
        sourceTile.size.width = sourceResolution.width;
        sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
        sourceTile.origin.x = 0.0f;

        CGRect destTile;
        destTile.size.width = destResolution.width;
        destTile.size.height = sourceTile.size.height * imageScale;
        destTile.origin.x = 0.0f;

所以tile的寬度是固定的,無論是source還是dest都與其對應的原圖片的寬度相等。接著初始化第二個用于計算的overlap變量。與上面的destOverlap一樣,不必在意其語義。

// 計算公式: sourceSeemOverlap = (int)kDestSeemOverlap / imageScale 
        float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);

接著作進入循環前的準備工作:計算循環次數;使用overlap更新tile塊的高度。

        //15. 計算循環次數 
        int iterations = (int)( sourceResolution.height / sourceTile.size.height );
        // 如果不能整除,有余數,則循環次數+1
        // 余數記錄下來
        int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
        if(remainder) {
            iterations++;
        }
        //16. 將overlap常量累加到塊的高度中,保存源圖像塊的高度到sourceTileHeightMinusOverlap
        float sourceTileHeightMinusOverlap = sourceTile.size.height;
        sourceTile.size.height += sourceSeemOverlap;
        destTile.size.height += kDestSeemOverlap;

進入循環,每次循環都更新塊的縱坐標。更新法則如下:

sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);

經過了上面這么多的鋪墊,現在可以看到tile是以一個什么方式來進行移動。顯而易見,每經過一次循環:

  1. sourceTiley值都以sourceTileHeightMinusOverlap為增量增加(假設在UIKit的坐標系上就是每次向下移動增量大小)
  2. destTiley值會逐漸從大變小。增量為sourceTileHeightMinusOverlap * imageScale
  3. 每次循環中tilesize保持固定(最后一次循環除外)

為了更直觀的理解,將這部分代碼提取了一下作為測試,加入了用于調試的Log。
輸入為一副3992 * 2442的圖片,在運行過程中控制臺輸出如下:

2017-05-24 10:53:16.430 SDWebDecoderTest[1007:65513] 循環次數:0
2017-05-24 10:53:16.430 SDWebDecoderTest[1007:65513] 在{{0, 6}, {3992, 71}} 內繪制sourceTileImageRef
2017-05-24 10:53:16.646 SDWebDecoderTest[1007:65513] 將sourceTileImageRef 繪制到 destTile: {{0, 634.96174430847168}, {1169, 21.038255661725998}}中
2017-05-24 10:53:16.646 SDWebDecoderTest[1007:65513] 循環次數:1
2017-05-24 10:53:16.647 SDWebDecoderTest[1007:65513] 在{{0, 71}, {3992, 71}} 內繪制sourceTileImageRef
2017-05-24 10:53:16.659 SDWebDecoderTest[1007:65513] 將sourceTileImageRef 繪制到 destTile: {{0, 615.92348861694336}, {1169, 21.038255661725998}}中
2017-05-24 10:53:16.659 SDWebDecoderTest[1007:65513] 循環次數:2
2017-05-24 10:53:16.659 SDWebDecoderTest[1007:65513] 在{{0, 136}, {3992, 71}} 內繪制sourceTileImageRef
2017-05-24 10:53:16.671 SDWebDecoderTest[1007:65513] 將sourceTileImageRef 繪制到 destTile: {{0, 596.88523483276367}, {1169, 21.038255661725998}}中
2017-05-24 10:53:16.671 SDWebDecoderTest[1007:65513] 循環次數:3
2017-05-24 10:53:16.671 SDWebDecoderTest[1007:65513] 在{{0, 201}, {3992, 71}} 內繪制sourceTileImageRef
2017-05-24 10:53:16.683 SDWebDecoderTest[1007:65513] 將sourceTileImageRef 繪制到 destTile: {{0, 577.84697723388672}, {1169, 21.038255661725998}}中
·····中間的省略······
2017-05-24 10:53:17.029 SDWebDecoderTest[1007:65513] 循環次數:31
2017-05-24 10:53:17.029 SDWebDecoderTest[1007:65513] 在{{0, 2021}, {3992, 71}} 內繪制sourceTileImageRef
2017-05-24 10:53:17.041 SDWebDecoderTest[1007:65513] 將sourceTileImageRef 繪制到 destTile: {{0, 44.77581787109375}, {1169, 21.038255661725998}}中
2017-05-24 10:53:17.041 SDWebDecoderTest[1007:65513] 循環次數:32
2017-05-24 10:53:17.041 SDWebDecoderTest[1007:65513] 在{{0, 2086}, {3992, 71}} 內繪制sourceTileImageRef
2017-05-24 10:53:17.053 SDWebDecoderTest[1007:65513] 將sourceTileImageRef 繪制到 destTile: {{0, 25.737548828125}, {1169, 21.038255661725998}}中
2017-05-24 10:53:17.053 SDWebDecoderTest[1007:65513] 循環次數:33
2017-05-24 10:53:17.054 SDWebDecoderTest[1007:65513] 在{{0, 2151}, {3992, 71}} 內繪制sourceTileImageRef
2017-05-24 10:53:17.065 SDWebDecoderTest[1007:65513] 將sourceTileImageRef 繪制到 destTile: {{0, 6.69927978515625}, {1169, 21.038255661725998}}中
2017-05-24 10:53:17.066 SDWebDecoderTest[1007:65513] 循環次數:34
2017-05-24 10:53:17.066 SDWebDecoderTest[1007:65513] 在{{0, 2216}, {3992, 71}} 內繪制sourceTileImageRef
2017-05-24 10:53:17.071 SDWebDecoderTest[1007:65513] 將sourceTileImageRef 繪制到 destTile: {{0, 1.0840253829956055}, {1169, 7.6153020858764648}}中

補充一個獲取圖片類型的代碼

在SDWebImage中的NSData+ImageContentType分類中使用以下方法獲取圖片類型:

+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
    if (!data) {
        return SDImageFormatUndefined;
    }
    
    uint8_t c;
    //Copies a number of bytes from the start of the receiver's data into a given buffer.
    [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:
            // R as RIFF for WEBP
            if (data.length < 12) {
                return SDImageFormatUndefined;
            }
            
            NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
            if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                return SDImageFormatWebP;
            }
    }
    return SDImageFormatUndefined;
}

圖片數據的第一個字節是固定的,一種類型的圖片第一個字節就是它的標識。

總結

SDWebImageDecoder提供了圖片解碼功能,同時還允許對圖片進行壓縮操作,防止解壓后內存暴漲。

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

推薦閱讀更多精彩內容

  • 第四篇 前言 首先,我們要弄明白一個問題? 為什么要對UIImage進行解碼呢?難道不能直接使用嗎? 其實不解碼也...
    老馬的春天閱讀 5,400評論 4 30
  • 5.SDWebImageDownloader 下面分析這個類看這個類的結構 這個類的屬性比較多。 先看這個類的pu...
    充滿活力的早晨閱讀 1,103評論 0 0
  • 我曾經無意間讀到過一句話,發人深省,“幸福感是衡量人生的唯一標準,是所有目標的最終目標。”幸福在我們的生活中如此重...
    作家格格閱讀 192評論 0 1
  • 忘記我們的曾經 失去了才知道要珍惜 總要有人來喚醒 一顆顆悲哀冷漠的心 我們還在徘徊不知所措 而你已經看見了終點的...
    愛Now閱讀 236評論 0 0
  • 有種說法叫做“混圈”,即,你想要做的事情或者要想成為什么樣的人首先得進入他的那個圈圈。最近,對這個詞有了深刻的體...
    夢在雨巷閱讀 401評論 0 0