iOS閱讀理解-SDWebImage

SDWebImage是作為iOS開(kāi)發(fā)肯定會(huì)使用的一個(gè)框架,關(guān)于它的原理和介紹的文章也看了不少,但是始終模模糊糊,不能形成系統(tǒng)的知識(shí)體系,果然只有自己DEBUG過(guò)一遍才能印象深刻。

正篇

SDWebImage框架圖

圖片編碼解碼

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.

平時(shí)使用的PNG、JPEG等格式的圖片是經(jīng)過(guò)一定算法編碼壓縮后的位圖,UIImage使用時(shí)需要解壓,而系統(tǒng)默認(rèn)解碼工作在主線程,所以在大量加載圖片的時(shí)候會(huì)有卡頓就是這個(gè)原因,但是具體卡在哪里需要對(duì)哪里優(yōu)化還是需要了解圖片加載的過(guò)程,在這之前先簡(jiǎn)單捋一遍需要了解的前置知識(shí)。(下面闡述并不一定保證正確)

平時(shí)通過(guò)操作UIViewaddSubView、insertSubView建立視圖之間的聯(lián)系,這種關(guān)系用一個(gè)樹(shù)的結(jié)構(gòu)保存起來(lái)稱為視圖樹(shù)。
每一個(gè)UIview都持有一個(gè)CALayer實(shí)例,也就是所謂的(支持圖層)backing layer,UIView的職責(zé)就是創(chuàng)建并管理這個(gè)圖層,平時(shí)使用的frame、bounds這些屬性只是對(duì)CALayer的一層封裝。而圖層與圖層之間的聯(lián)系也有個(gè)樹(shù)結(jié)構(gòu)叫圖層樹(shù)。
CALayerUIView最大的不同是CALayer并不清楚具體的響應(yīng)鏈(iOS通過(guò)視圖層級(jí)關(guān)系用來(lái)傳送觸摸事件的機(jī)制),即使它提供了一些方法來(lái)判斷是否一個(gè)觸點(diǎn)在圖層的范圍之內(nèi)。

Core Animation是iOS和OS X上可用的圖形渲染和動(dòng)畫基礎(chǔ)框架,從圖像可以看出Core Animation負(fù)責(zé)把上層需要顯示的圖像做了某些處理過(guò)渡到下層渲染引擎。

當(dāng)改變一個(gè)圖層的屬性,屬性值的確是立刻更新的,但是屏幕上并沒(méi)有馬上發(fā)生改變。這是因?yàn)槟阍O(shè)置的屬性并沒(méi)有直接調(diào)整圖層的外觀,只是定義了圖層動(dòng)畫結(jié)束之后將要變化的外觀。

Core Animation扮演了一個(gè)控制器的角色,并且負(fù)責(zé)根據(jù)圖層行為和事務(wù)設(shè)置去不斷更新視圖的這些屬性在屏幕上的狀態(tài)。

這意味著CALayer除了“真實(shí)”值(就是你設(shè)置的值)之外,必須要知道當(dāng)前顯示在屏幕上的屬性值的記錄。事實(shí)上每個(gè)圖層屬性的顯示值都被存儲(chǔ)在一個(gè)叫做呈現(xiàn)圖層(presentationLayer)的獨(dú)立圖層當(dāng)中,這個(gè)呈現(xiàn)圖層實(shí)際上是模型圖層(modelLayer)[我們改變CALayer的各種屬性時(shí)實(shí)際上就是modelLayer的]的復(fù)制,但是它的屬性值代表了在任何指定時(shí)刻當(dāng)前外觀效果。換句話說(shuō),可以通過(guò)呈現(xiàn)圖層的值來(lái)獲取當(dāng)前屏幕上真正顯示出來(lái)的值。
圖層樹(shù)中所有圖層的呈現(xiàn)圖層之間的樹(shù)關(guān)系稱為呈現(xiàn)樹(shù)。

Core Animation把需要渲染的圖層和數(shù)據(jù)是打包一個(gè)專門的進(jìn)程進(jìn)行渲染,叫渲染進(jìn)程(Render Server)

這整個(gè)流程大致分為4個(gè)階段:

  • 布局 - 這是準(zhǔn)備你的視圖/圖層的層級(jí)關(guān)系,以及設(shè)置圖層屬性(位置,背景色,邊框等等)的階段。
  • 顯示 - 這是圖層的寄宿圖片被繪制的階段。繪制有可能涉及你的-drawRect:-drawLayer:inContext:方法的調(diào)用路徑。
  • 準(zhǔn)備 - 這是Core Animation準(zhǔn)備發(fā)送動(dòng)畫數(shù)據(jù)到渲染服務(wù)的階段。這同時(shí)也是Core Animation將要執(zhí)行一些別的事務(wù)例如解碼動(dòng)畫過(guò)程中將要顯示的圖片的時(shí)間點(diǎn)。
  • 提交 - 這是最后的階段,Core Animation打包所有圖層和動(dòng)畫屬性,然后通過(guò)IPC(內(nèi)部處理通信)發(fā)送到渲染服務(wù)進(jìn)行顯示。

這是事情是發(fā)生在APP內(nèi),但是我們只能控制前兩個(gè)階段。一旦打包的圖層和動(dòng)畫到達(dá)渲染服務(wù)進(jìn)程,他們會(huì)被反序列化來(lái)形成另一個(gè)叫做渲染樹(shù)的圖層樹(shù)。

一張圖片的加載過(guò)程主要是加載->解碼->渲染,對(duì)于PNG圖片來(lái)說(shuō),加載會(huì)比JPEG更長(zhǎng),因?yàn)槲募赡芨?,但是解碼會(huì)相對(duì)較快,而且Xcode會(huì)把PNG圖片進(jìn)行解碼優(yōu)化之后引入工程。JPEG圖片更小,加載更快,但是解壓的步驟要消耗更長(zhǎng)的時(shí)間,因?yàn)镴PEG解壓算法比基于zip的PNG算法更加復(fù)雜。

平時(shí)使用加載圖片的方式主要是imageNamed(加載Assets圖片)和imageWithContentsOfFile(加載Bundle圖片),使用imageNamed加載圖片時(shí)系統(tǒng)會(huì)立刻進(jìn)行解碼并且對(duì)解碼后的數(shù)據(jù)進(jìn)行緩存;而使用imageWithContentsOfFile使用ImageIO創(chuàng)建CGImageRef內(nèi)存映射數(shù)據(jù),通過(guò)隱式CATransaction捕獲這些層樹(shù)修改,在主運(yùn)行循環(huán)的下一次迭代中,Core Animation提交隱式事務(wù),這可能涉及創(chuàng)建已設(shè)置為層內(nèi)容的任何圖像的副本。根據(jù)圖像,復(fù)制它涉及以下部分或全部步驟:
1. 緩沖區(qū)被分配用于管理文件IO和解壓縮操作。
2. 文件數(shù)據(jù)從磁盤讀入內(nèi)存。
3. 壓縮的圖像數(shù)據(jù)被解碼成其未壓縮的位圖形式,這通常是CPU密集型操作。
4. 然后,Core Animation使用未壓縮的位圖數(shù)據(jù)來(lái)渲染圖層。

下面做了個(gè)測(cè)驗(yàn):

UIImage *img = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image.png" ofType:nil]];
UIImageView *v = [[UIImageView alloc] initWithFrame:self.view.bounds];
v.image = img;
[self.view addSubview:v];

用Time Profile查看結(jié)果



在下一次runloop循環(huán)迭代里面的CA:Layer:prepare_commit也就是prepare階段中的png_read_IDAT_dataApple操作耗時(shí),所以猜測(cè)是在這里面進(jìn)行解碼,這個(gè)過(guò)程是在主線程,而在SDWebImage中,當(dāng)圖片下載完成后就立刻在后臺(tái)線程進(jìn)行解碼,主要是圖片繪制到 CGBitmapContext ,利用位圖創(chuàng)建圖片。

SDWebImageImageIOCoder

SDWebImageCoder是提供自定義圖像解碼/編碼的圖像編碼器的協(xié)議;SDWebImageProgressiveCoder繼承于SDWebImageCoder,提供自定義的漸進(jìn)圖像解碼的協(xié)議, 可以我們自己實(shí)現(xiàn)也可以使用默認(rèn)自帶解碼器,自帶解碼器的工作是由
SDWebImageImageIOCoder、SDWebImageGIFCoderSDWebImageWebPCoder提供, 在這里主要看SDWebImageImageIOCoder的幾個(gè)核心方法。

sd_decompressedImageWithImage
static const size_t kBytesPerPixel = 4;
static const size_t kBitsPerComponent = 8;

- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
    //image為nil或者image為動(dòng)圖時(shí)不需解碼
    if (![[self class] shouldDecodeImage:image]) {
        return image;
    }
    @autoreleasepool{
        CGImageRef imageRef = image.CGImage;
        CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
        BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);//判斷是否含有alpha通道
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;//小端模式32位主機(jī)字節(jié)模式[1]
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;//通過(guò)判斷image是否含有alpha通道來(lái)使用不同的位圖布局信息[2]
        
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        // CGBitmapContextCreate不支持kCGImageAlphaNone.
        CGContextRef context = CGBitmapContextCreate(NULL,
                                                     width,
                                                     height,
                                                     kBitsPerComponent,
                                                     0,
                                                     colorspaceRef,
                                                     bitmapInfo);//[3]
        if (context == NULL) {
            return image;
        }
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];//[4]
        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);
        
        return imageWithoutAlpha;
    }
}
  • kCGBitmapByteOrder32Host這個(gè)宏完整定義為
#ifdef __BIG_ENDIAN__
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
#else    /* Little endian. */
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
#endif

Intel x86處理器存儲(chǔ)一個(gè)雙字節(jié)整數(shù),其中最低有效字節(jié)優(yōu)先,后跟最高有效字節(jié)。這稱為little-endian(小端序)字節(jié)排序。其他CPU(例如PowerPC CPU)存儲(chǔ)一個(gè)雙字節(jié)整數(shù),其最高有效字節(jié)優(yōu)先,后跟最低有效字節(jié)。這稱為big-endian(大端序)字節(jié)排序。

通俗來(lái)說(shuō)大端序是指數(shù)據(jù)的高字節(jié)保存在內(nèi)存的低地址中,數(shù)據(jù)的低字節(jié),保存在內(nèi)存的高地址中。小端序則相反,數(shù)據(jù)的高字節(jié)保存在內(nèi)存的高地址中,數(shù)據(jù)的低字節(jié)保存在內(nèi)存的低地址中。
iPhone設(shè)備使用的是32位小端模式,使用這個(gè)宏可以避免硬編碼。


可以看的出來(lái)在32bits下RGBA每個(gè)通道是8位,即使上面的kBitsPerComponent;

  • CGBitmapContextCreate創(chuàng)建bitmap上下文
CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
    size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
    CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)

data:指向要呈現(xiàn)繪圖的內(nèi)存中的目標(biāo)的指針。這個(gè)內(nèi)存塊的大小至少應(yīng)該是(bytesPerRow*height)字節(jié)。當(dāng)它為NULL時(shí),系統(tǒng)會(huì)自動(dòng)分配內(nèi)存;
bitsPerComponent:即上面的kBitsPerComponent;
bytesPerRow:位圖的每一行要使用的內(nèi)存字節(jié)數(shù)。如果數(shù)據(jù)參數(shù)為NULL,則傳遞一個(gè)值0會(huì)自動(dòng)計(jì)算該值;之前的版本中使用的是kBytesPerPixel即每行像素4bytes,但是現(xiàn)在根據(jù)蘋果文檔推薦已經(jīng)改為0讓系統(tǒng)自動(dòng)計(jì)算,而且還會(huì)進(jìn)行cache line alignment的優(yōu)化;
space:顏色空間即使用的是RGB;
bitmapinfo:位圖布局信息;

  • 雖然名字還是叫imageWithoutAhlpa,但是在這次commit中已經(jīng)修復(fù)成不管圖片是否帶alpha通道都不會(huì)成為影響強(qiáng)制解碼的因素了(截止今天為止是這樣的),所以說(shuō)解碼出來(lái)的圖片不是一定不帶alpha通道的圖片。
sd_decompressedAndScaledDownImageWithImage

sd_decompressedAndScaledDownImageWithImage這個(gè)方法中是在解壓縮的基礎(chǔ)上加個(gè)縮放的功能,為了理解這個(gè)鬼玩意我把這個(gè)方法拿出來(lái)DEBUG了一遍,sourceImageRef是一張寬7033px高10110px大小為8.3M的圖片;

static const size_t kBytesPerPixel = 4;
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
static const CGFloat kDestImageSizeMB = 60.0f;
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;
static const CGFloat kSourceImageTileSizeMB = 20.0f;
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;

- (nullable UIImage *)sd_decompressedAndScaledDownImageWithImage:(nullable UIImage *)image {
    ...//判斷image
    CGContextRef destContext;
    @autoreleasepool {
        CGImageRef sourceImageRef = image.CGImage;
        CGSize sourceResolution = CGSizeZero;
        sourceResolution.width = CGImageGetWidth(sourceImageRef);
        sourceResolution.height = CGImageGetHeight(sourceImageRef);
        float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
        float imageScale = kDestTotalPixels / sourceTotalPixels;
        CGSize destResolution = CGSizeZero;
        destResolution.width = (int)(sourceResolution.width*imageScale);
        destResolution.height = (int)(sourceResolution.height*imageScale);
        ... get color space & get bitmapinfo
        destContext = CGBitmapContextCreate(NULL,
                                            destResolution.width,
                                            destResolution.height,
                                            kBitsPerComponent,
                                            0,
                                            colorspaceRef,
                                            bitmapInfo);
        
        if (destContext == NULL) {
            return image;
        }
        CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
        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;
        float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
        CGImageRef sourceTileImageRef;
        int iterations = (int)( sourceResolution.height / sourceTile.size.height );
        int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
        if(remainder) {
            iterations++;
        }
        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 );
            }
        }
        CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
        ... create image & release 
        return destImage;
    }
}
  • step1 先了解各種常量
    kBytesPerMB:每MB=1024 * 1024 bytes
    kPixelsPerMB: 每MB=1024 * 1024 / 4 = 262144像素
    kDestImageSizeMB:目標(biāo)圖片60MB
    kDestTotalPixels:目標(biāo)像素=60 * 262144
    kSourceImageTileSizeMB:定義用于解碼圖像的“塊”的最大大小(以MB為單位);
    kTileTotalPixels:每“塊”所占的像素;
    kDestSeemOverlap:重疊的“塊”的像素?cái)?shù)量

  • step2 DEBUG

CGImageRef sourceImageRef = image.CGImage;

<po sourceImageRef <CGImage 0x7fb628d00aa0> (IP) <<CGColorSpace 0x600001f66880> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1)> width = 7033, height = 10110, bpc = 8, bpp = 32, row bytes = 28132 kCGImageAlphaNoneSkipLast | 0 (default byte order) | kCGImagePixelFormatPacked

//獲取寬高
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);

<po sourceResolution (width = 7033, height = 10110);

//總像素大小
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;

<po sourceTotalPixels 71103632px;

//確定要應(yīng)用于輸入圖像的比例比,從而得到定義大小的輸出圖像。
float imageScale = kDestTotalPixels / sourceTotalPixels;

<po imageScale 0.221207261 kDestTotalPixels(最大像素即60MB大小的 = 15728640px);

//使用圖像比例尺計(jì)算output圖像的寬度、高度
CGSize destResolution = CGSizeZero;
destResolution.width = (int)(sourceResolution.width*imageScale);
destResolution.height = (int)(sourceResolution.height*imageScale);

<po destResolution (width = 1555, height = 2236)

//這里和上面方法一樣 創(chuàng)建output image的context
 CGColorSpaceRef colorspaceRef = CGColorSpaceCreateDeviceRGB();
 BOOL hasAlpha = ssCGImageRefContainsAlpha(sourceImageRef);
 CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
 bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
 destContext = CGBitmapContextCreate(NULL,
                                     destResolution.width,
                                     destResolution.height,
                                     kBitsPerComponent,
                                     0,
                                     colorspaceRef,
                                     bitmapInfo);
if (destContext == NULL) {
     return image;
}

<po destContext <CGContext 0x600000e60e40> (kCGContextTypeBitmap) <<CGColorSpace 0x600001f667c0> (kCGColorSpaceDeviceRGB)> width = 1555, height = 2236, bpc = 8, bpp = 32, row bytes = 6240 kCGImageAlphaNoneSkipFirst | kCGImageByteOrder32Little

//context的插值質(zhì)量設(shè)置為最高
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
//定義用于從輸入圖像到輸出圖像的增量blit的矩形的大小。
//由于iOS從磁盤檢索圖像數(shù)據(jù)的方式,我們使用的源圖塊寬度等于源圖像的寬度。
//iOS必須以全寬“波段”從磁盤解碼圖像,即使當(dāng)前圖形上下文被剪切為該波段內(nèi)的子圖形。因此,我們通過(guò)將我們的圖塊大小設(shè)置為輸入圖像的整個(gè)寬度來(lái)充分利用由解碼操作產(chǎn)生的所有像素?cái)?shù)據(jù)。
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
//源圖塊高度是動(dòng)態(tài)的。 由于我們以MB為單位指定了源圖塊的大小,因此請(qǐng)查看輸入圖像寬度可以設(shè)置多少像素的行。
sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
sourceTile.origin.x = 0.0f;

<po sourceTile (origin = (x = 0, y = 0), size = (width = 7033, height = 745))
因?yàn)閣idth * height=pixels所以反過(guò)來(lái)能計(jì)算出該塊的高(寬是固定的高是動(dòng)態(tài)的,為什么呢?上面的注釋中解釋了需要將圖塊(tile)設(shè)置為input image的整個(gè)width);

//輸出圖塊與輸入圖塊的比例相同,但縮放為圖像比例。
CGRect destTile;
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;

<po destTile (origin = (x = 0, y = 5.2150177063252833E-310), size = (width = 1555, height = 164.79940950870514));destTile.height同樣也是動(dòng)態(tài)的;

//input image重疊與output image重疊的比例。 這是我們組裝輸出圖像時(shí)每個(gè)圖塊重疊的像素?cái)?shù)量。
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);

<po sourceSeemOverlap 9這個(gè)什么用暫時(shí)不理

CGImageRef sourceTileImageRef;
//計(jì)算生成output圖像所需的讀/寫操作的數(shù)量。
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
//如果平鋪的高度不能平均分割圖像的高度,那么再增加一次迭代來(lái)計(jì)算剩余的像素。
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) {
    iterations++;
}

<po iterations 14 將input image分14塊區(qū)域掃描生成output bitmap

//添加圖塊重疊的部分,但保存原始圖塊高度以進(jìn)行y坐標(biāo)計(jì)算。
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += kDestSeemOverlap;

< po sourceTileHeightMinusOverlap 745

for( int y = 0; y < iterations; ++y ) {
    @autoreleasepool {
       sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
       destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
       //創(chuàng)建對(duì)源圖像的引用,并將其上下文剪切到參數(shù)rect。
       sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
       //如果這是最后一個(gè)圖塊,則其大小可能小于源圖塊高度(不足一塊)。
       //調(diào)整dest tile大小以考慮該差異。
       if( y == iterations - 1 && remainder ) {
            float dify = destTile.size.height;
            destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
            dify -= destTile.size.height;
            destTile.origin.y += dify;
       }
       //從input圖像到output圖像讀取和寫入像素大小的像素部分。
       CGContextDrawImage( destContext, destTile, sourceTileImageRef );
       CGImageRelease( sourceTileImageRef );
    }
}
在14次掃描中分別打印
sourceTile               destTile
{{0, 9}, {7033, 754}}    {{0, 2069.2005920410156}, {1555, 166.79940950870514}}
{{0, 754}, {7033, 754}}  {{0, 1904.4011840820312}, {1555, 166.79940950870514}}
{{0, 1499}, {7033, 754}} {{0, 1739.6017761230469}, {1555, 166.79940950870514}}
{{0, 2244}, {7033, 754}} {{0, 1574.8023681640625}, {1555, 166.79940950870514}}
{{0, 2989}, {7033, 754}} {{0, 1410.0029296875}, {1555, 166.79940950870514}}
{{0, 3734}, {7033, 754}} {{0, 1245.2035522460938}, {1555, 166.79940950870514}}
{{0, 4479}, {7033, 754}} {{0, 1080.4041748046875}, {1555, 166.79940950870514}}
{{0, 5224}, {7033, 754}} {{0, 915.604736328125}, {1555, 166.79940950870514}}
{{0, 5969}, {7033, 754}} {{0, 750.8052978515625}, {1555, 166.79940950870514}}
{{0, 6714}, {7033, 754}} {{0, 586.005859375}, {1555, 166.79940950870514}}
{{0, 7459}, {7033, 754}} {{0, 421.20654296875}, {1555, 166.79940950870514}}
{{0, 8204}, {7033, 754}} {{0, 256.4071044921875}, {1555, 166.79940950870514}}
{{0, 8949}, {7033, 754}} {{0, 91.607666015625}, {1555, 166.79940950870514}}
{{0, 9694}, {7033, 754}} {{0, -73.191650390625}, {1555, 166.79940950870514}}

sourceTile的origin.y剛好是以sourceTileHeightMinusOverlap遞增,destTile的origin.y是以164.79940950870514即destTile.height遞減;說(shuō)明從input image的最上面一個(gè)部分開(kāi)始(從上到下)生成bitmap寫入到output image最下面的區(qū)域里面(從下到上),最終生成一個(gè)完整的output bitmap context,為什么這樣做呢?猜測(cè)是因?yàn)镃oreGraphics框架坐標(biāo)系統(tǒng)(左下為坐標(biāo)原點(diǎn))和UIKit坐標(biāo)系統(tǒng)不一致(左上為坐標(biāo)原點(diǎn)),在這里也沒(méi)有使用CGContextTranslateCTM轉(zhuǎn)換一下;

//最后生成輸出圖片
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;
}
return destImage;

這就是SDWebImage縮放解壓縮大圖的全部?jī)?nèi)容,這樣做的好處就是分塊壓縮解碼避免內(nèi)存爆炸,其實(shí)這里也是參照蘋果的做法,官方DemoLargeImageDownsizing

incrementallyDecodedImageWithData
- (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished {
    if (!_imageSource) {
        _imageSource = CGImageSourceCreateIncremental(NULL);
    }
    UIImage *image;
    // 更新數(shù)據(jù),需要的是收到的所有數(shù)據(jù)而不是當(dāng)前傳入的data
    CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
    if (_width + _height == 0) {//第一次收到數(shù)據(jù)
        CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);//獲取該圖片各種屬性
        if (properties) {
            NSInteger orientationValue = 1;
            CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
            if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
            val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
            if (val) CFNumberGetValue(val, kCFNumberLongType, &_width);
            val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
            if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
            CFRelease(properties);
            //當(dāng)我們繪制到Core Graphics時(shí),我們會(huì)丟失方向信息,這意味著initWithCGIImage生成的圖像有時(shí)會(huì)導(dǎo)致錯(cuò)誤定向。 (與didCompleteWithError中的initWithData生成的圖像不同。)因此將其保存在此處并稍后傳遞。
            _orientation = [SDWebImageCoderHelper imageOrientationFromEXIFOrientation:orientationValue];
#endif
        }
    }
    if (_width + _height > 0) {
        CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
        if (partialImageRef) {
#if SD_UIKIT || SD_WATCH
            image = [[UIImage alloc] initWithCGImage:partialImageRef scale:1 orientation:_orientation];
#elif SD_MAC
            image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize];
#endif
            CGImageRelease(partialImageRef);
            image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
        }
    }
    if (finished) {//最后完成的時(shí)候再釋放_(tái)imageSource
        if (_imageSource) {
            CFRelease(_imageSource);
            _imageSource = NULL;
        }
    }
    return image;
}

因?yàn)榫W(wǎng)絡(luò)傳輸?shù)难舆t,圖片的數(shù)據(jù)不能一次性全部加載完,可能會(huì)一段一段地傳過(guò)來(lái),這種情況就需要漸進(jìn)式地解碼啦,ImageIO框架提供了專門用于漸進(jìn)式解碼的APICGImageSourceCreateIncremental(NULL),可以通過(guò)調(diào)用函數(shù)CGImageSourceUpdateDataProviderCGImageSourceUpdateData向其添加數(shù)據(jù)。

encodedDataWithImage
- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format {
    NSMutableData *imageData = [NSMutableData data];
    CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:format];
    CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL);
    if (!imageDestination) {
        // Handle failure.
        return nil;
    }
    NSMutableDictionary *properties = [NSMutableDictionary dictionary];
#if SD_UIKIT || SD_WATCH
    NSInteger exifOrientation = [SDWebImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation];
    [properties setValue:@(exifOrientation) forKey:(__bridge NSString *)kCGImagePropertyOrientation];
#endif
    // Add your image to the destination.
    CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties);
    // Finalize the destination.
    if (CGImageDestinationFinalize(imageDestination) == NO) {
        // Handle failure.
        imageData = nil;
    }
    CFRelease(imageDestination);
    return [imageData copy];
}

CGImageDestinationRef是ImageIO中負(fù)責(zé)寫入數(shù)據(jù)的抽象出來(lái)的對(duì)象,可以表示單個(gè)圖像或多個(gè)圖像,可以包含縮略圖圖像以及每個(gè)圖像的屬性。使用CGImageDestinationCreateWithDataimageData就是要寫入數(shù)據(jù)的對(duì)象,之后調(diào)用CGImageDestinationAddImage寫入數(shù)據(jù)和屬性,最后使用CGImageDestinationFinalize得到編碼數(shù)據(jù)。

SDWebImageCodersManager

管理所有編碼器的manager,也是編碼器模塊的唯一入口,這樣做了一層中間層好處在于以后無(wú)論是修改還是添加刪除只需要下層處理即可,上層無(wú)需做任何修改;可以使用自定義的編碼器,默認(rèn)使用SDWebImageImageIOCoder。

SDImageCache

SDImageCache的分為內(nèi)存緩存和磁盤緩存,內(nèi)存緩存任務(wù)主要由SDMemoryCache負(fù)責(zé)。

內(nèi)存緩存
@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType>
@end
@interface SDMemoryCache <KeyType, ObjectType> ()
@property (nonatomic, strong, nonnull) SDImageCacheConfig *config;
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; // strong-weak cache
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock; // a lock to keep the access to `weakCache` thread-safe
@end

SDMemoryCache基礎(chǔ)于NSCache,所以自帶以下特點(diǎn)

  1. 線程安全;
  2. key不需要實(shí)現(xiàn)NSCopying,因?yàn)閷?duì)key是強(qiáng)引用并不是copy一份;
  3. 內(nèi)存不足時(shí)自動(dòng)釋放對(duì)象;
self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];

weakCache對(duì)Key(URL)強(qiáng)引用對(duì)Value(Image)弱引用,這個(gè)weakCache有什么用呢,先看他的方法。

- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
    [super setObject:obj forKey:key cost:g];
    if (!self.config.shouldUseWeakMemoryCache) {
        return;
    }
    //如果啟用弱內(nèi)存緩存配置(默認(rèn)啟用)
    if (key && obj) {
        LOCK(self.weakCacheLock);
        [self.weakCache setObject:obj forKey:key];//weakCache同時(shí)也持有一份Image(弱引用)
        UNLOCK(self.weakCacheLock);
    }
}

- (id)objectForKey:(id)key {
    id obj = [super objectForKey:key];
    if (!self.config.shouldUseWeakMemoryCache) {
        return obj;
    }
    if (key && !obj) {
        // Check weak cache
        LOCK(self.weakCacheLock);
        obj = [self.weakCache objectForKey:key];//查找weakCache
        UNLOCK(self.weakCacheLock);
        if (obj) {
            // Sync cache
            NSUInteger cost = 0;
            if ([obj isKindOfClass:[UIImage class]]) {
                cost = SDCacheCostForImage(obj);
            }
            [super setObject:obj forKey:key cost:cost];//再次同步到cache
        }
    }
    return obj;
}

- (void)didReceiveMemoryWarning:(NSNotification *)notification {
    //只清理Cache,但是保留weakCache
    [super removeAllObjects];
}
  1. 在setObject的時(shí)候,如果啟用了使用弱緩存機(jī)制,weakCache內(nèi)部同時(shí)也緩存image,因?yàn)槭侨跻盟圆粨?dān)心該image出現(xiàn)釋放不掉內(nèi)存泄露的問(wèn)題;
  2. 在objectForKey時(shí),如果NSCache查找不到該緩存(被系統(tǒng)釋放掉了),則會(huì)在weakCache里面再查找該image,并再次恢復(fù)緩存,避免了要去磁盤查找或者重新去網(wǎng)絡(luò)下載;
  3. 當(dāng)收到MemoryWarning時(shí)只清理內(nèi)存,并不會(huì)清理weakCache,因?yàn)槭侨跻盟援?dāng)有其他實(shí)例(例如UIImageView)持有image的時(shí)候并不會(huì)remove掉;
    這種方式在某些情況下有有用,比如應(yīng)用程序進(jìn)入后臺(tái)并清除內(nèi)存時(shí),再次進(jìn)入前臺(tái)時(shí)可能因?yàn)榍謇砭彺鎸?dǎo)致的image顯示閃爍問(wèn)題(重新下載或者從磁盤讀取),可以從弱緩存中同步回來(lái)。
    4.這里緩存大小計(jì)算是以像素為單位的。
磁盤緩存
- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    ...內(nèi)存緩存
    if (toDisk) {
        dispatch_async(self.ioQueue, ^{
            @autoreleasepool {
                NSData *data = imageData;
                if (!data && image) {
                    SDImageFormat format;
                    //通過(guò)判斷圖片是否有alpha通道來(lái)判斷是png還是jpeg
                    if (SDCGImageRefContainsAlpha(image.CGImage)) {
                        format = SDImageFormatPNG;
                    } else {
                        format = SDImageFormatJPEG;
                    }
                    //將image編碼
                    data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
                }
                //磁盤儲(chǔ)存的是image編碼后的數(shù)據(jù)
                [self _storeImageDataToDisk:data forKey:key];
            }
     }
     ...回調(diào)
}

- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
    ...create directory
    NSString *cachePathForKey = [self defaultCachePathForKey:key];
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    //寫入文件
    [imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
    // skip iCloud備份
    if (self.config.shouldDisableiCloud) {
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}

所有io操作都是在一個(gè)專門的串行隊(duì)列里面完成,保證了線程的安全性和數(shù)據(jù)的準(zhǔn)確性。

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    //先檢查內(nèi)存緩存
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
    if (shouldQueryMemoryOnly) {//只讀取內(nèi)存緩存
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
    //利用operation cancel特性
    NSOperation *operation = [NSOperation new];
    void(^queryDiskBlock)(void) =  ^{
        if (operation.isCancelled) {
            return;
        }
        @autoreleasepool {
            //獲取image編碼數(shù)據(jù)
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            SDImageCacheType cacheType = SDImageCacheTypeNone;
            if (image) {
                // the image is from in-memory cache
                diskImage = image;
                cacheType = SDImageCacheTypeMemory;
            } else if (diskData) {
                cacheType = SDImageCacheTypeDisk;
                // decode image data only if in-memory cache missed
                //解碼
                diskImage = [self diskImageForKey:key data:diskData options:options];
                if (diskImage && self.config.shouldCacheImagesInMemory) {
                    NSUInteger cost = SDCacheCostForImage(diskImage);
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
            }
            if (doneBlock) {
                if (options & SDImageCacheQueryDiskSync) {//這個(gè)options可以強(qiáng)制同步執(zhí)行
                    doneBlock(diskImage, diskData, cacheType);
                } else {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        doneBlock(diskImage, diskData, cacheType);
                    });
                }
            }
        }
    };
    if (options & SDImageCacheQueryDiskSync) {//這個(gè)options可以強(qiáng)制同步執(zhí)行
        queryDiskBlock();
    } else {
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
    return operation;
}

queryCacheOperationForKey提供異步查詢緩存功能,先查詢內(nèi)存緩存,如果緩存丟失則跑去磁盤緩存檢索,加上從磁盤緩存檢索出來(lái)的是image編碼后的數(shù)據(jù),需要被解碼又可能還要壓縮,所以去磁盤讀取圖片是非常耗時(shí)的操作,由于使用operation可以取消操作的特性,返回operation供上層使用者決定后續(xù)操作。

- (void)backgroundDeleteOldFiles {
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
    __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
    [self deleteOldFilesWithCompletionBlock:^{
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
}

注冊(cè)進(jìn)入后臺(tái)執(zhí)行的方法,這里需要注意啟動(dòng)后臺(tái)任務(wù)時(shí)要記得調(diào)用endBackgroundTask來(lái)標(biāo)記停止任務(wù),調(diào)用兩次的原因是app可能在后臺(tái)180s活動(dòng)時(shí)間內(nèi)完成也可能未完成,這里只會(huì)走到兩者其一。

SDWebImageDownloaderOperation

- (void)start {
    @synchronized (self) {//給當(dāng)前operation加鎖保證多線程的安全
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];//釋放回調(diào)和session
            return;
        }

#if SD_UIKIT
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;
                if (sself) {
                    //啟用后臺(tái)下載,如果在后臺(tái)存活時(shí)間到期的時(shí)候operation還未執(zhí)行完成則取消該操作
                    [sself cancel];
                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif
        //unownedSession是由外部manager持有的所以是weak;這里避免unownedSession運(yùn)行到一半的時(shí)候被外部釋放需要捕捉一份 
        NSURLSession *session = self.unownedSession;
        if (!session) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            //delegateQueue為nil時(shí)將創(chuàng)建一個(gè)串行操作隊(duì)列
            session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                    delegate:self
                                               delegateQueue:nil];
            //如果外部manager沒(méi)有持有session,由operation創(chuàng)建個(gè)ownedSession
            self.ownedSession = session;
        }
        //從NSURLCache獲取緩存data
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
            NSURLCache *URLCache = session.configuration.URLCache;
            if (!URLCache) {
                URLCache = [NSURLCache sharedURLCache];
            }
            NSCachedURLResponse *cachedResponse;
            // NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483
            @synchronized (URLCache) {
                cachedResponse = [URLCache cachedResponseForRequest:self.request];
            }
            if (cachedResponse) {
                self.cachedData = cachedResponse.data;
            }
        }
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }
    if (self.dataTask) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
        if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
            if (self.options & SDWebImageDownloaderHighPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityHigh;
            } else if (self.options & SDWebImageDownloaderLowPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityLow;
            }
        }
#pragma clang diagnostic pop
        [self.dataTask resume];
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
        });
    } else {
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
        [self done];
        return;
    }

#if SD_UIKIT
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
        UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
        [app endBackgroundTask:self.backgroundTaskId];
        self.backgroundTaskId = UIBackgroundTaskInvalid;
    }
#endif
}

覆寫start方法表明是個(gè)并行隊(duì)列,主要做了以下幾個(gè)工作:

  1. 因?yàn)槎嗑€程所以需要加鎖保證線程安全;
  2. 當(dāng)operation進(jìn)入執(zhí)行狀態(tài)時(shí),先檢查isCancelled,如果被取消則重制Operation;
  3. 在dataTask開(kāi)始前,如果APP啟用后臺(tái)下載且后臺(tái)運(yùn)行時(shí)間到期了,需要取消operation;
  4. 如果持有該operation的外部manager沒(méi)有session,就在operation內(nèi)部創(chuàng)建個(gè)臨時(shí)session
  5. 設(shè)置SDWebImageDownloaderIgnoreCachedResponse標(biāo)志位后會(huì)忽略NSURLCache的緩存,在這里獲取NSURLCache緩存的response用來(lái)判斷圖片下載完成后是否是從NSURLCache緩存中獲得的;
  6. 創(chuàng)建NSURLSessionTask開(kāi)始任務(wù)
  • NSURLSessionDataDelegate
//receive response callback
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;
    NSInteger expected = (NSInteger)response.expectedContentLength;
    expected = expected > 0 ? expected : 0;
    self.expectedSize = expected;
    self.response = response;
    NSInteger statusCode = [response respondsToSelector:@selector(statusCode)] ? ((NSHTTPURLResponse *)response).statusCode : 200;
    BOOL valid = statusCode < 400;
    //當(dāng)服務(wù)器響應(yīng)304并命中URLCache時(shí),URLSession當(dāng)前行為將返回狀態(tài)碼200。當(dāng)返回304時(shí)卻沒(méi)有緩存數(shù)據(jù)時(shí)
    if (statusCode == 304 && !self.cachedData) {
        valid = NO;
    }
    if (valid) {
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, expected, self.request.URL);
        }
    } else {
        //狀態(tài)碼無(wú)效,并標(biāo)記為已取消。不需要調(diào)用 [self.dataTask cancel] ',這可能會(huì)增加URLSession的生命周期
        disposition = NSURLSessionResponseCancel;
    }
    __weak typeof(self) weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:weakSelf];
    });
    if (completionHandler) {
        completionHandler(disposition);
    }
}

//receive data callback 
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    if (!self.imageData) {
        //根據(jù)HTTP response中的expectedContentLength創(chuàng)建imageData
        self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
    }
    [self.imageData appendData:data];//拼接數(shù)據(jù)
    //漸進(jìn)式顯示圖片
    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
        __block NSData *imageData = [self.imageData copy];
        const NSInteger totalSize = imageData.length;
        BOOL finished = (totalSize >= self.expectedSize);
        
        if (!self.progressiveCoder) {
            //為漸進(jìn)解碼創(chuàng)建一個(gè)新實(shí)例,以避免沖突
            for (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedInstance].coders) {
                if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
                    [((id<SDWebImageProgressiveCoder>)coder) canIncrementallyDecodeFromData:imageData]) {
                    //就是之前的SDWebImageImageIO,每個(gè)operation持有一個(gè),生命周期結(jié)束后釋放
                    self.progressiveCoder = [[[coder class] alloc] init];
                    break;
                }
            }
        }
        //逐步解碼編碼器隊(duì)列中的圖像
        dispatch_async(self.coderQueue, ^{
            @autoreleasepool {
                //解碼
                UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
                if (image) {
                    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                    image = [self scaledImageForKey:key image:image];
                    if (self.shouldDecompressImages) {
                        image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
                    }
                    //即使當(dāng)finished=YES時(shí),也不保留漸進(jìn)式解碼圖像。因?yàn)樗鼈兪怯糜谝晥D呈現(xiàn)的,而不是從downloader選項(xiàng)中獲取完整的功能。而一些編碼器的實(shí)現(xiàn)可能無(wú)法保持漸進(jìn)式譯碼與常規(guī)譯碼的一致性。
                    [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
                }
            }
        });
    }
    //進(jìn)度callback
    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
        progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
    }
}

//cache response callback
//所有觸發(fā)條件都滿足才觸發(fā)
請(qǐng)求的是HTTP或HTTPS URL(或您自己的支持緩存的自定義網(wǎng)絡(luò)協(xié)議)。
請(qǐng)求成功(狀態(tài)碼在200-299范圍內(nèi))。
提供的響應(yīng)來(lái)自服務(wù)器,而不是來(lái)自緩存。
session配置的緩存策略允許緩存。
提供的URLRequest對(duì)象的緩存策略(如果適用)允許緩存。
服務(wù)器響應(yīng)中與緩存相關(guān)的頭文件(如果存在)允許緩存。
響應(yīng)大小足夠小,可以合理地適應(yīng)緩存。(例如,如果提供磁盤緩存,響應(yīng)必須不大于磁盤緩存大小的5%)。
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
    NSCachedURLResponse *cachedResponse = proposedResponse;
    //默認(rèn)情況下,request阻止使用NSURLCache。使用此標(biāo)志時(shí),NSURLCache將與默認(rèn)策略一起使用。
    if (!(self.options & SDWebImageDownloaderUseNSURLCache)) {
        // 防止緩存響應(yīng)
        cachedResponse = nil;
    }
    if (completionHandler) {
        completionHandler(cachedResponse);
    }
}

//complete callback
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    ... 發(fā)送通知
    if (error) {
        [self callCompletionBlocksWithError:error];
        [self done];
    } else {
        if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
            __block NSData *imageData = [self.imageData copy];
            if (imageData) {
                //如果指定僅通過(guò)“SDWebImageDownloaderIgnoreCachedResponse”忽略NSURLCache而使用SDWebImageCache緩存的數(shù)據(jù),那么我們應(yīng)該檢查緩存的數(shù)據(jù)是否等于圖像數(shù)據(jù)
                if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
                    //cachedData=imageData時(shí)表示該imageData是從NSURLCache緩存中獲取的
                    //設(shè)置SDWebImageDownloaderIgnoreCachedResponse標(biāo)志后當(dāng)獲取到緩存callback中image和imageData都設(shè)置為nil
                    [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
                    [self done];
                } else {
                    // 解碼編碼器隊(duì)列中的圖像
                    dispatch_async(self.coderQueue, ^{
                        @autoreleasepool {
                            UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
                            NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                            image = [self scaledImageForKey:key image:image];
                            BOOL shouldDecode = YES;
                            //不強(qiáng)制解碼gif和WebPs
                            if (image.images) {
                                shouldDecode = NO;
                            } else {
#ifdef SD_WEBP
                                SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
                                if (imageFormat == SDImageFormatWebP) {
                                    shouldDecode = NO;
                                }
#endif
                            }
                            //下面都是解碼和回調(diào)了
                            if (shouldDecode) {
                                if (self.shouldDecompressImages) {
                                    BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
                                    image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
                                }
                            }
                            CGSize imageSize = image.size;
                            if (imageSize.width == 0 || imageSize.height == 0) {
                                [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
                            } else {
                                [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
                            }
                            [self done];
                        }
                    });
                }
            } else {
                [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
                [self done];
            }
        } else {
            [self done];
        }
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
    /*
     NSURLSessionAuthChallengePerformDefaultHandling:默認(rèn)方式處理
     NSURLSessionAuthChallengeUseCredential:使用指定的證書
     NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消權(quán)限認(rèn)證
     */
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;
    // 從服務(wù)器返回的受保護(hù)空間(就是證書)中拿到證書的類型
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {//判斷服務(wù)器返回的證書是否是服務(wù)器信任的
        if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        } else {
            //創(chuàng)建證書
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            disposition = NSURLSessionAuthChallengeUseCredential;
        }
    } else {
        if (challenge.previousFailureCount == 0) {
            if (self.credential) {
                credential = self.credential;
                disposition = NSURLSessionAuthChallengeUseCredential;
            } else {
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
        }
    }
    //安裝證書
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

SDWebImageDownloaderOperation主要的工作就是處理Operation具體的下載任務(wù),收到downloader分發(fā)的request之后創(chuàng)建下載任務(wù)到收到圖片數(shù)據(jù)后的處理,在下載圖片數(shù)據(jù)完成后馬上在子線程對(duì)原始圖片數(shù)據(jù)解碼然后通過(guò)生成bitmap創(chuàng)建image。

SDWebImageDownloader

SDWebImageDownloader的工作就是管理Operation,接收到上層URL后開(kāi)啟一個(gè)下載任務(wù),待任務(wù)完成后再轉(zhuǎn)交image給上層。

- (NSOperation<SDWebImageDownloaderOperationInterface> *)createDownloaderOperationWithUrl:(nullable NSURL *)url
                                                                                  options:(SDWebImageDownloaderOptions)options {
    NSTimeInterval timeoutInterval = self.downloadTimeout;
    if (timeoutInterval == 0.0) {
        timeoutInterval = 15.0;
    }
    //為了防止?jié)撛诘闹貜?fù)緩存(NSURLCache + SDImageCache),禁用了緩存的圖像請(qǐng)求
    NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                cachePolicy:cachePolicy
                                                            timeoutInterval:timeoutInterval];
    //默認(rèn)運(yùn)行處理cookies
    request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
    request.HTTPShouldUsePipelining = YES;
    if (self.headersFilter) {
        request.allHTTPHeaderFields = self.headersFilter(url, [self allHTTPHeaderFields]);
    }
    else {
        request.allHTTPHeaderFields = [self allHTTPHeaderFields];
    }
    NSOperation<SDWebImageDownloaderOperationInterface> *operation = [[self.operationClass alloc] initWithRequest:request inSession:self.session options:options];
    operation.shouldDecompressImages = self.shouldDecompressImages;
    if (self.urlCredential) {
        operation.credential = self.urlCredential;
    } else if (self.username && self.password) {
        operation.credential = [NSURLCredential credentialWithUser:self.username password:self.password persistence:NSURLCredentialPersistenceForSession];
    }
    if (options & SDWebImageDownloaderHighPriority) {
        operation.queuePriority = NSOperationQueuePriorityHigh;
    } else if (options & SDWebImageDownloaderLowPriority) {
        operation.queuePriority = NSOperationQueuePriorityLow;
    }
    if (self.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
        [self.lastAddedOperation addDependency:operation];
        self.lastAddedOperation = operation;
    }
    return operation;
}

這個(gè)方法主要是根據(jù)url創(chuàng)建request并設(shè)置request的一些基礎(chǔ)配置和創(chuàng)建operation和傳遞外部的配置,這里值得一提的就是HTTPShouldUsePipelining,了解這個(gè)之前首先要了解HTTP長(zhǎng)連接和HTTP Pipelining

HTTP1.1規(guī)定了默認(rèn)保持長(zhǎng)連接(HTTP persistent connection ,也有翻譯為持久連接),數(shù)據(jù)傳輸完成了保持TCP連接不斷開(kāi)(不發(fā)RST包、不四次握手),等待在同域名下繼續(xù)用這個(gè)通道傳輸數(shù)據(jù);相反的就是短連接。
使用了HTTP長(zhǎng)連接(HTTP persistent connection )之后的好處,包括可以使用HTTP 流水線技術(shù)(HTTP pipelining,也有翻譯為管道化連接),它是指,在一個(gè)TCP連接內(nèi),多個(gè)HTTP請(qǐng)求可以并行,下一個(gè)HTTP請(qǐng)求在上一個(gè)HTTP請(qǐng)求的應(yīng)答完成之前就發(fā)起。從 wiki上了解到這個(gè)技術(shù)目前并沒(méi)有廣泛使用,使用這個(gè)技術(shù)必須要求客戶端和服務(wù)器端都能支持,目前有部分瀏覽器完全支持,而服務(wù)端的支持僅需要:按 HTTP請(qǐng)求順序正確返回Response(也就是請(qǐng)求&響應(yīng)采用FIFO模式),wiki里也特地指出,只要服務(wù)器能夠正確處理使用HTTP pipelinning的客戶端請(qǐng)求,那么服務(wù)器就算是支持了HTTP pipelining。
摘自 HTTP的長(zhǎng)連接和短連接

開(kāi)啟HTTPShouldUsePipelining后,在上個(gè)Request收到Response之前就能繼續(xù)發(fā)送Request,好處是可以提高網(wǎng)絡(luò)請(qǐng)求的效率,壞處則是可能需要服務(wù)器的支持(需要保證HTTP按正確正確的順序返回Response);而蘋果默認(rèn)是關(guān)閉的,官方文檔中表明就算設(shè)置為YES也不保證HTTP Pipelining行為(比如POST請(qǐng)求是無(wú)效的,HTTP<1.1不work,HTTP>=1.1&&<2中服務(wù)器不支持也會(huì)不work,HTTP2采用的是二進(jìn)制流傳輸數(shù)據(jù)且是多路復(fù)用所以也不需要管道流水技術(shù)),而AFNetWorking曾經(jīng)在GET和HEAD請(qǐng)求中默認(rèn)開(kāi)啟過(guò),但是后來(lái)因?yàn)檫@個(gè)issue關(guān)閉了,綜上來(lái)看似乎這種優(yōu)化并不會(huì)有帶來(lái)很大的收益甚至用不好還可能出問(wèn)題,但是在SDWebImage中一直啟用貌似也沒(méi)有人反饋過(guò)問(wèn)題,可能原因是SDWebImage默認(rèn)支持最大并發(fā)數(shù)為6并不會(huì)形成此類問(wèn)題。


- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    //URL將用作回調(diào)字典的鍵,所以它不能是nil。如果是nil,立即調(diào)用沒(méi)有圖像或數(shù)據(jù)的callback。
    LOCK(self.operationsLock);
    NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
    if (!operation || operation.isFinished) {
        //創(chuàng)建operation
        operation = [self createDownloaderOperationWithUrl:url options:options];
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            LOCK(sself.operationsLock);
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        [self.URLOperations setObject:operation forKey:url];
        [self.downloadQueue addOperation:operation];
    }
    UNLOCK(self.operationsLock);
    //operation持有completeBlock 所以當(dāng)下載完成后由各自的operation喚起block
    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;
    return token;
}

這里的downloadOperationCancelToken實(shí)際上就是儲(chǔ)存兩個(gè)block的字典,當(dāng)調(diào)用cancel時(shí)會(huì)把兩個(gè)block釋放,當(dāng)該operation不再持有任何block時(shí)就認(rèn)為需要取消該操作。

SDWebImageManager

SDWebImageManager的主要工作就是將異步下載器(SDWebImageDownloader)與圖像緩存存儲(chǔ)(SDImageCache)綁定在一起。

- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock;

1.SDWebImageCombinedOperation繼承于SDWebImageOperation,并且持有downloader下載任務(wù)傳過(guò)來(lái)的 SDWebImageDownloadToken,提供一個(gè)對(duì)象讓上層調(diào)用者持有這個(gè)下載任務(wù)實(shí)例還有cancel方法取消下載操作。
2.通過(guò)緩存模塊在緩存中查找緩存數(shù)據(jù)
3.根據(jù)查找緩存結(jié)果和設(shè)置option決定是否下載或回調(diào)

UIImageView+WebCache

注意這里每個(gè)UIImageView同時(shí)只能關(guān)聯(lián)一個(gè)operation,如果有新的下載任務(wù)開(kāi)始,要先取消之前的下載任務(wù)。

總結(jié)

stay hungry, stay foolish

參考和一些延伸閱讀
iOS平臺(tái)圖片編解碼入門教程(Image/IO篇)
what is cginterpolationquality
談?wù)?iOS 中圖片的解壓縮
ios核心動(dòng)畫高級(jí)技巧
iOS性能優(yōu)化——圖片加載和處理
繪制像素到屏幕上
ios如何避免圖像解壓縮的時(shí)間開(kāi)銷
SDWebImage-解碼、壓縮圖像
Image Resizing Techniques
avoiding image decompression sickness
iOS 處理圖片的一些小 Tip
CFStringTransform
NSURLCache
Safari on iOS 5 Randomly Switches Images
iOS 保持界面流暢的技巧
iOS 視圖、動(dòng)畫渲染機(jī)制探究

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容