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

圖片編碼解碼
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ò)操作UIView的addSubView、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ù)。
CALayer和UIView最大的不同是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、SDWebImageGIFCoder和SDWebImageWebPCoder提供, 在這里主要看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ù)CGImageSourceUpdateDataProvider或CGImageSourceUpdateData向其添加數(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è)圖像的屬性。使用CGImageDestinationCreateWithData中imageData就是要寫入數(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)
- 線程安全;
- key不需要實(shí)現(xiàn)NSCopying,因?yàn)閷?duì)key是強(qiáng)引用并不是copy一份;
- 內(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];
}
- 在setObject的時(shí)候,如果啟用了使用弱緩存機(jī)制,weakCache內(nèi)部同時(shí)也緩存image,因?yàn)槭侨跻盟圆粨?dān)心該image出現(xiàn)釋放不掉內(nèi)存泄露的問(wèn)題;
- 在objectForKey時(shí),如果NSCache查找不到該緩存(被系統(tǒng)釋放掉了),則會(huì)在weakCache里面再查找該image,并再次恢復(fù)緩存,避免了要去磁盤查找或者重新去網(wǎng)絡(luò)下載;
- 當(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è)工作:
- 因?yàn)槎嗑€程所以需要加鎖保證線程安全;
- 當(dāng)operation進(jìn)入執(zhí)行狀態(tài)時(shí),先檢查isCancelled,如果被取消則重制Operation;
- 在dataTask開(kāi)始前,如果APP啟用后臺(tái)下載且后臺(tái)運(yùn)行時(shí)間到期了,需要取消operation;
- 如果持有該operation的外部manager沒(méi)有session,就在operation內(nèi)部創(chuàng)建個(gè)臨時(shí)session
- 設(shè)置
SDWebImageDownloaderIgnoreCachedResponse標(biāo)志位后會(huì)忽略NSURLCache的緩存,在這里獲取NSURLCache緩存的response用來(lái)判斷圖片下載完成后是否是從NSURLCache緩存中獲得的; - 創(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ī)制探究