SDWebImage源碼學習基于版本4.0
源碼注釋: SDWebImage4.0
以前看見別人的輪子感覺太高深,給了自己懶惰的借口~~無奈一次次的碰壁讓我覺得,I'm just a little bit caught in the middle.
閱讀之前對于SDWebImage的了解僅僅處于UIImageView+WebCache這個類別加載圖片方法(sd_setImageWithURL:...)的使用和SDWebImageDownloader對于圖片進行下載(downloadImageWithURL...)操作,然后就是為了面試膚淺的對于其緩存機制的實現(xiàn)原理的了解.
學習源碼的心得體驗:
- 圖片壓縮,解碼
- 網(wǎng)絡請求,多線程
- 緩存機制
- 框架構建思想
SDWebImage簡介
SDWebImage是一個異步的圖片下載框架,它支持緩存,并使用了類別(UIImageView, UIButton, MKAnnotationView)可以很方便的進行使用.
- 類別(UIImageView, UIButton, MKAnnotationView)用來加載網(wǎng)絡圖片并且對網(wǎng)絡圖片的緩存進行管理
- 采用異步方式來下載網(wǎng)絡圖片
- 采用異步方式,使用memory+disk來緩存網(wǎng)絡圖片元旬,自動管理緩存匀归。
- 后臺進行圖片解壓縮
- 保證相同URL的網(wǎng)絡圖片不會被重復下載
- 保證失效的URL不會被無限重試
- 不會阻塞主線程
- 性能
- 使用GCD和ARC
- 支持多種圖片格式(JPEG,PNG,GIF,WebP,TIFF...)
頭文件
SDWebImageCompat
一般項目我們都會定義一個頭文件,包含一些宏定義,常量,或者常用的方法,它存在的意義無需多說.
SDWebImageCompat中定義了一些相關平臺適配的宏,簡單的一些常量和內聯(lián)函數(shù),block等.
關于宏定義的知識:
-
#
if 表達式 程序段1 #else 程序段2 #endif 含義:如果表達式成立,執(zhí)行程序段1,否則執(zhí)行程序段2 -
#
if 表達式1 #elif 表達式2 #endif 類似于if(){} else if() {} - 防止重復聲明,頭文件重復包含和編譯 #ifndef 標識符 #define 標識符 程序段1 #else 程序段2 #endif 含義:如果標識符沒有定義過(if not define) 進行宏定義,執(zhí)行程序段1,否則執(zhí)行程序段2.
- 取消宏定義:#undef
- 枚舉:從枚舉定義來看袱贮,NS_ENUM和NS_OPTIONS本質是一樣的攒巍,僅僅從字面上來區(qū)分其用途窑业。NS_ENUM是通用情況,NS_OPTIONS一般用來定義具有位移操作或特點的情況搀擂。
枚舉宏定義:
#ifndef NS_ENUM
#define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type
#endif
//使用
typedef NS_ENUM(int,EnumName){
EnumNameType1,
EnumNameType2
};
//展開
typedef enum EnumName:int EnumName;
enum EnumName:int {
EnumNameType1,
EnumNameType2
};
內聯(lián)函數(shù)處理圖片尺寸
extern UIImage *SDScaledImageForKey(NSString *key, UIImage *image);
inline UIImage *SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullable image) {
if (!image) {
return nil;
}
#if SD_MAC
return image;
#elif SD_UIKIT || SD_WATCH
//動圖
if ((image.images).count > 0) {
NSMutableArray<UIImage *> *scaledImages = [NSMutableArray array];
for (UIImage *tempImage in image.images) {
//遞歸調用
[scaledImages addObject:SDScaledImageForKey(key, tempImage)];
}
//用一組圖片創(chuàng)建一個動態(tài)圖片
return [UIImage animatedImageWithImages:scaledImages duration:image.duration];
}
else {
#if SD_WATCH
if ([[WKInterfaceDevice currentDevice] respondsToSelector:@selector(screenScale)]) {
#elif SD_UIKIT
if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
#endif
//根據(jù)后綴給scale賦值
CGFloat scale = 1;
if (key.length >= 8) {
NSRange range = [key rangeOfString:@"@2x."];
if (range.location != NSNotFound) {
scale = 2.0;
}
range = [key rangeOfString:@"@3x."];
if (range.location != NSNotFound) {
scale = 3.0;
}
}
//使用initWithCGImage來根據(jù)Core Graphics的圖片構建UIImage。
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
image = scaledImage;
}
return image;
}
#endif
}
線程安全:dispatch_main_async_safe
/*判斷當前隊列是否是主隊列,如果是直接執(zhí)行,不是就通過dispatch_async(dispatch_get_main_queue(), block)執(zhí)行,在主線程中執(zhí)行dispatch_async(dispatch_get_main_queue(), block)有可能會crash.*/
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif
圖片解碼
相關類或文件
- NSData+ImageContentType
- UIImage+GIF
- UIImage+MultiFormat
- SDWebImageDecoder
- NSImage+WebCache(MAC_OS)
圖片格式:NSData+ImageContentType
當文件使用二進制流作為傳輸時威恼,需要制定一套規(guī)范箫措,用來區(qū)分該文件到底是什么類型的斤蔓。實際上每個文件的前幾個字節(jié)都標識著文件的類型,對于一般的圖片文件驾锰,通過第一個字節(jié)(WebP需要12字節(jié))可以辨識出文件類型椭豫。
- JPEG (jpg)捻悯,文件頭:FFD8FFE1
- PNG (png)今缚,文件頭:89504E47
- GIF (gif)姓言,文件頭:47494638
- TIFF tif;tiff 0x49492A00
- TIFF tif;tiff 0x4D4D002A
- RAR Archive (rar)囱淋,文件頭:52617221
- WebP : 524946462A73010057454250
這個方法的實現(xiàn)思路是這樣的:
1.取data的第一個字節(jié)的數(shù)據(jù)妥衣,辨識出JPG/JPEG税手、PNG芦倒、GIF兵扬、TIFF這幾種圖片格式器钟,返回其對應的MIME類型。
2.如果第一個字節(jié)是數(shù)據(jù)為0x52狞谱,需要進一步檢測跟衅,因為以0x52為文件頭的文件也可能會是rar等類型(可以在文件頭查看)伶跷,而webp的前12字節(jié)有著固定的數(shù)據(jù):
// 根據(jù)二進制文件頭的第一個字節(jié)來判斷文件的類型()
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
if (!data) {
return SDImageFormatUndefined;
}
//獲取一個字節(jié)的數(shù)據(jù)
uint8_t c;
[data getBytes:&c length:1];
//一個字節(jié)兩個字符
switch (c) {
case 0xFF:
return SDImageFormatJPEG;
case 0x89:
return SDImageFormatPNG;
case 0x47:
return SDImageFormatGIF;
case 0x49:
case 0x4D:
return SDImageFormatTIFF;
case 0x52:
//WEBP
// R as RIFF for WEBP
if (data.length < 12) {
return SDImageFormatUndefined;
}
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
return SDImageFormatWebP;
}
}
return SDImageFormatUndefined;
}
支持顯示GIF:(只是顯示圖片的第一幀)UIImage+GIF
當圖片源有多個(gif格式)的時候,通過CGImageSourceRef獲取圖片的第一幀返回,這里主要涉及<Image I/0>框架的內容
+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
if (!data) {
return nil;
}
//獲取圖片源CGImageSourceRef
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
//獲取圖片源數(shù)量
size_t count = CGImageSourceGetCount(source);
UIImage *staticImage;
if (count <= 1) {
//只有一個圖片源,說明不是動畫,直接實例化返回
staticImage = [[UIImage alloc] initWithData:data];
} else {
//這里僅僅繪制gif的第一幀內容,如果支持GIF播放可以使用FLAnimatedImageView這個類別
#if SD_WATCH
CGFloat scale = 1;
scale = [WKInterfaceDevice currentDevice].screenScale;
#elif SD_UIKIT
CGFloat scale = 1;
scale = [UIScreen mainScreen].scale;
#endif
//獲取第一幀的CGImage
CGImageRef CGImage = CGImageSourceCreateImageAtIndex(source, 0, NULL);
#if SD_UIKIT || SD_WATCH
//繪制
UIImage *frameImage = [UIImage imageWithCGImage:CGImage scale:scale orientation:UIImageOrientationUp];
staticImage = [UIImage animatedImageWithImages:@[frameImage] duration:0.0f];
#elif SD_MAC
staticImage = [[UIImage alloc] initWithCGImage:CGImage size:NSZeroSize];
#endif
CGImageRelease(CGImage);
}
//釋放資源
CFRelease(source);
return staticImage;
}
UIImage <->NSData的相互轉換:UIImage+MultiFormat
- (nullable UIImage *)sd_imageWithData:(nullable NSData *)data;
+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data {
if (!data) {
return nil;
}
UIImage *image;
//格式判斷
SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:data];
if (imageFormat == SDImageFormatGIF) {
//gif 返回第一幀圖像
image = [UIImage sd_animatedGIFWithData:data];
}
#ifdef SD_WEBP
else if (imageFormat == SDImageFormatWebP)
{ //webp
image = [UIImage sd_imageWithWebPData:data];
}
#endif
else {
image = [[UIImage alloc] initWithData:data];
#if SD_UIKIT || SD_WATCH
//獲取方向信息
UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data];
//實例化 UIImageOrientationUp為默認,直接返回即可
if (orientation != UIImageOrientationUp) {
image = [UIImage imageWithCGImage:image.CGImage
scale:image.scale
orientation:orientation];
}
#endif
}
return image;
}
<Image I/O>獲取圖片信息(方向)
#if SD_UIKIT || SD_WATCH
//獲取圖片的方向
+(UIImageOrientation)sd_imageOrientationFromImageData:(nonnull NSData *)imageData {
UIImageOrientation result = UIImageOrientationUp;
//獲取圖片源數(shù)據(jù)
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
if (imageSource) {
//首幀圖片的屬性信息
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
if (properties) {
CFTypeRef val;
int exifOrientation;
//獲取屬性字典中的方向信息
val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
if (val) {
CFNumberGetValue(val, kCFNumberIntType, &exifOrientation);
//轉換
result = [self sd_exifOrientationToiOSOrientation:exifOrientation];
} // else - if it's not set it remains at up
CFRelease((CFTypeRef) properties);
} else {
//NSLog(@"NO PROPERTIES, FAIL");
}
CFRelease(imageSource);
}
return result;
}
UIImage -> NSData
-(nullable NSData *)sd_imageData;
-(nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat;
CGImageAlphaInfo是一個枚舉,表示alpha分量的位置及顏色分量是否做預處理:
- kCGImageAlphaLast:alpha分量存儲在每個像素中最不顯著的位置,如RGBA拢肆。
- kCGImageAlphaFirst:alpha分量存儲在每個像素中最顯著的位置郭怪,如ARGB鄙才。
- kCGImageAlphaPremultipliedLast:alpha分量存儲在每個像素中最不顯著的位置攒庵,但顏色分量已經(jīng)乘以了alpha值。
- kCGImageAlphaPremultipliedFirst:alpha分量存儲在每個像素中最顯著的位置,同時顏色分量已經(jīng)乘以了alpha值糖驴。
- kCGImageAlphaNoneSkipLast:沒有alpha分量贮缕。如果像素的總大小大于顏色空間中顏色分量數(shù)目所需要的空間,則最不顯著位置的位將被忽略定嗓。
- kCGImageAlphaNoneSkipFirst:沒有alpha分量宵溅。如果像素的總大小大于顏色空間中顏色分量數(shù)目所需要的空間恃逻,則最顯著位置的位將被忽略寇损。
- kCGImageAlphaNone:等于kCGImageAlphaNoneSkipLast矛市。
- (nullable NSData *)sd_imageData {
return [self sd_imageDataAsFormat:SDImageFormatUndefined];
}
//將UIImage對象轉換成二進制,有透明通道的返回PNG,否則返回JPEG
- (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat {
NSData *imageData = nil;
if (self) {
#if SD_UIKIT || SD_WATCH
int alphaInfo = CGImageGetAlphaInfo(self.CGImage);
//透明通道
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL usePNG = hasAlpha;
// the imageFormat param has priority here. But if the format is undefined, we relly on the alpha channel
if (imageFormat != SDImageFormatUndefined) {
usePNG = (imageFormat == SDImageFormatPNG);
}
if (usePNG) {
imageData = UIImagePNGRepresentation(self);
} else {
imageData = UIImageJPEGRepresentation(self, (CGFloat)1.0);
}
#else
NSBitmapImageFileType imageFileType = NSJPEGFileType;
if (imageFormat == SDImageFormatGIF) {
imageFileType = NSGIFFileType;
} else if (imageFormat == SDImageFormatPNG) {
imageFileType = NSPNGFileType;
}
imageData = [NSBitmapImageRep representationOfImageRepsInArray:self.representations
usingType:imageFileType
properties:@{}];
#endif
}
return imageData;
}
SDWebImageDecoder解碼
SDWebImageDecoder文件中是一個對UIImage添加的分類,主要針對內存較小的設備進行圖片解碼和壓縮處理.
常量
//用來說明每個像素占用內存多少個字節(jié)憨愉,在這里是占用4個字節(jié)配紫。(圖像在iOS設備上是以像素為單位顯示的)
static const size_t kBytesPerPixel = 4;
//表示每一個組件占多少位。舉例植袍,比方說RGBA于个,其中R(紅色)G(綠色)B(藍色)A(透明度)是4個組件厅篓,每個像素由這4個組件組成,那么我們就用8位來表示著每一個組件档押,所以這個RGBA就是8*4 = 32位令宿。
static const size_t kBitsPerComponent = 8;
/*
* 最大支持壓縮圖像源的大小
* Suggested value for iPad1 and iPhone 3GS: 60.
* Suggested value for iPad2 and iPhone 4: 120.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 30.
*/
static const CGFloat kDestImageSizeMB = 60.0f;
/*
* 原圖方塊的大小
* Suggested value for iPad1 and iPhone 3GS: 20.
* Suggested value for iPad2 and iPhone 4: 40.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 10.
*/
static const CGFloat kSourceImageTileSizeMB = 20.0f;
//1M有多少字節(jié)
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
//1M有多少個像素
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
//目標總像素
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;
//原圖放寬總像素
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;
//重疊像素大小
static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
私有方法
- 判斷是否需要解碼
- 判斷是否需要壓縮處理
- 獲取顏色空間
//gif png 不解碼
+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
// Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
if (image == nil) {
return NO;
}
// do not decode animated images
//動畫不進行解碼
if (image.images != nil) {
return NO;
}
CGImageRef imageRef = image.CGImage;
//透明通道信息
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
alpha == kCGImageAlphaLast ||
alpha == kCGImageAlphaPremultipliedFirst ||
alpha == kCGImageAlphaPremultipliedLast);
// do not decode images with alpha
if (anyAlpha) {
//有透明通道不進行解碼
return NO;
}
return YES;
}
//是否進行壓縮
+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image {
BOOL shouldScaleDown = YES;
CGImageRef sourceImageRef = image.CGImage;
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
//判斷目標總像素與最大支持壓縮的像素比(60MB)
float imageScale = kDestTotalPixels / sourceTotalPixels;
if (imageScale < 1) {
shouldScaleDown = YES;
} else {
shouldScaleDown = NO;
}
return shouldScaleDown;
}
//獲取顏色空間
+ (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef {
// current
CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
//不支持顏色空間
BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
imageColorSpaceModel == kCGColorSpaceModelCMYK ||
imageColorSpaceModel == kCGColorSpaceModelIndexed);
if (unsupportedColorSpace) {
//使用RGB模式的顏色空間
colorspaceRef = CGColorSpaceCreateDeviceRGB();
CFAutorelease(colorspaceRef);
}
return colorspaceRef;
}
解碼:
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
if (![UIImage shouldDecodeImage:image]) {
return image;
}
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool{
CGImageRef imageRef = image.CGImage;
CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
//每行的像素占用字節(jié)數(shù)
size_t bytesPerRow = kBytesPerPixel * width;
//這里創(chuàng)建的contexts是沒有透明因素的革娄。在UI渲染的時候拦惋,實際上是把多個圖層按像素疊加計算的過程首尼,需要對每一個像素進行 RGBA 的疊加計算软能。當某個 layer 的是不透明的查排,也就是 opaque 為 YES 時,GPU 可以直接忽略掉其下方的圖層砂代,這就減少了很多工作量刻伊。這也是調用 CGBitmapContextCreate 時 bitmapInfo 參數(shù)設置為忽略掉 alpha 通道的原因。
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (context == NULL) {
return image;
}
// Draw the image into the context and retrieve the new bitmap image without alpha
//繪制圖像 得到?jīng)]有透明通道的圖片
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
scale:image.scale
orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
}
}
壓縮
+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image {
//檢測圖像能否解碼
if (![UIImage shouldDecodeImage:image]) {
return image;
}
//檢查圖像應不應該壓縮撩鹿,原則是:如果圖像大于目標尺寸才需要壓縮
if (![UIImage shouldScaleDownImage:image]) {
return [UIImage decodedImageWithImage:image];
}
CGContextRef destContext;
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool {
//拿到數(shù)據(jù)信息 sourceImageRef
CGImageRef sourceImageRef = image.CGImage;
//計算原圖的像素 sourceResolution
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
//計算原圖總像素 sourceTotalPixels
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
// Determine the scale ratio to apply to the input image
// that results in an output image of the defined size.
// see kDestImageSizeMB, and how it relates to destTotalPixels.
//計算壓縮比例 imageScale
float imageScale = kDestTotalPixels / sourceTotalPixels;
//計算目標像素 destResolution
CGSize destResolution = CGSizeZero;
destResolution.width = (int)(sourceResolution.width*imageScale);
destResolution.height = (int)(sourceResolution.height*imageScale);
// current color space
//獲取當前的顏色空間 colorspaceRef
CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:sourceImageRef];
//計算并創(chuàng)建目標圖像的內存 destBitmapData
size_t bytesPerRow = kBytesPerPixel * destResolution.width;
// Allocate enough pixel data to hold the output image.
void* destBitmapData = malloc( bytesPerRow * destResolution.height );
if (destBitmapData == NULL) {
return image;
}
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
//創(chuàng)建目標上下文 destContext
destContext = CGBitmapContextCreate(destBitmapData,
destResolution.width,
destResolution.height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (destContext == NULL) {
free(destBitmapData);
return image;
}
//設置壓縮質量
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
// Now define the size of the rectangle to be used for the
// incremental blits from the input image to the output image.
// we use a source tile width equal to the width of the source
// image due to the way that iOS retrieves image data from disk.
// iOS must decode an image from disk in full width 'bands', even
// if current graphics context is clipped to a subrect within that
// band. Therefore we fully utilize all of the pixel data that results
// from a decoding opertion by achnoring our tile size to the full
// width of the input image.
//計算第一個原圖方塊 sourceTile,這個方塊的寬度同原圖一樣吼鳞,高度根據(jù)方塊容量計算
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
// The source tile height is dynamic. Since we specified the size
// of the source tile in MB, see how many rows of pixels high it
// can be given the input image width.
sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
sourceTile.origin.x = 0.0f;
// The output tile is the same proportions as the input tile, but
// scaled to image scale.
//計算目標圖像方塊 destTile
CGRect destTile;
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;
//計算原圖像方塊與方塊重疊的像素大小 sourceSeemOverlap
// The source seem overlap is proportionate to the destination seem overlap.
// this is the amount of pixels to overlap each tile as we assemble the ouput image.
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
CGImageRef sourceTileImageRef;
// 計算原圖像需要被分割成多少個方塊 iterations
// calculate the number of read/write operations required to assemble the
// output image.
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
// If tile height doesn't divide the image height evenly, add another iteration
// to account for the remaining pixels.
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) {
iterations++;
}
//根據(jù)重疊像素計算原圖方塊的大小后,獲取原圖中該方塊內的數(shù)據(jù)音诫,把該數(shù)據(jù)寫入到相對應的目標方塊中
// Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations.
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += kDestSeemOverlap;
for( int y = 0; y < iterations; ++y ) {
@autoreleasepool {
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
if( y == iterations - 1 && remainder ) {
float dify = destTile.size.height;
destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
dify -= destTile.size.height;
destTile.origin.y += dify;
}
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
CGImageRelease( sourceTileImageRef );
}
}
//返回目標圖像
CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
CGContextRelease(destContext);
if (destImageRef == NULL) {
return image;
}
UIImage *destImage = [UIImage imageWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(destImageRef);
if (destImage == nil) {
return image;
}
return destImage;
}
}
緩存機制
緩存配置信息SDImageCacheConfig
/**
* Decompressing images that are downloaded and cached can improve performance but can consume lot of memory.
* Defaults to YES. Set this to NO if you are experiencing a crash due to excessive memory consumption.
是否解壓縮圖片,默認為YES
*/
@property (assign, nonatomic) BOOL shouldDecompressImages;
/**
* disable iCloud backup [defaults to YES]
是否禁用iCloud備份香罐, 默認為YES
*/
@property (assign, nonatomic) BOOL shouldDisableiCloud;
/**
* use memory cache [defaults to YES]
是否緩存到內存中庇茫,默認為YES
*/
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
/**
* The maximum length of time to keep an image in the cache, in seconds
最大的緩存不過期時間, 單位為秒顷霹,默認為一周的時間
*/
@property (assign, nonatomic) NSInteger maxCacheAge;
/**
* The maximum size of the cache, in bytes.
最大的緩存尺寸淋淀,單位為字節(jié)
*/
@property (assign, nonatomic) NSUInteger maxCacheSize;
緩存SDImageCache
SDImageCache是一個緩存的抽象,主要功能包括緩存信息的配置,設置緩存路徑,進行緩存,查詢緩存,清除緩存等.它主要使用memory進行緩存,當然也可以選擇同時緩存在磁盤.緩存在磁盤的操作是異步進行的,不用擔心造成UI線程的延遲.
緩存在磁盤中的路徑默認是在沙盒路徑的Library/Caches/default/com.hackemist.SDWebImageCache.default路徑下,當然我們也可以自定義存儲路徑,圖片文件名是經(jīng)過圖片的URL進行MD5加密處理的字符串.
- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
const char *str = key.UTF8String;
if (str == NULL) {
str = "";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15], [key.pathExtension isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", key.pathExtension]];
return filename;
}
文件讀寫操作全部是在同一個串行隊列中進行的,保證了讀寫的安全性
//檢查隊列l(wèi)abel,保證IO操作在IOQueue隊列中執(zhí)行
- (void)checkIfQueueIsIOQueue {
const char *currentQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
const char *ioQueueLabel = dispatch_queue_get_label(self.ioQueue);
if (strcmp(currentQueueLabel, ioQueueLabel) != 0) {
NSLog(@"This method should be called from the ioQueue");
}
}
緩存操作:memory通過AutoPurgeCache進行緩存,其實就是NSCache的子類,只是在這里增加了內存緊張的監(jiān)聽,用于及時清理緩存.磁盤中的操作就是通過文件管理器NSFileManage寫入.
//最終方法
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
if (!image || !key) {
//為空
if (completionBlock) {
completionBlock();
}
return;
}
//允許memory緩存
if (self.config.shouldCacheImagesInMemory) {
//緩存,計算大小,cache
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
if (toDisk) {
//緩存到磁盤
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;
if (!data && image) {
//圖片格式,轉換為二進制
SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];
data = [image sd_imageDataAsFormat:imageFormatFromData];
}
[self storeImageDataToDisk:data forKey:key];
if (completionBlock) {
//回到主線程 進行回調
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
//不進行磁盤緩存 直接回調
if (completionBlock) {
completionBlock();
}
}
}
///存儲到磁盤
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
if (!imageData || !key) {
return;
}
//檢查當前隊列是否是IOQueue
[self checkIfQueueIsIOQueue];
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// get cache Path for image key
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
[_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
// disable iCloud backup
if (self.config.shouldDisableiCloud) {
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
磁盤緩存管理:清除過期的緩存文件和計算所有緩存文件的大小,通過NSDirectoryEnumerator迭代器遍歷獲取文件的信息進行處理和計算
//刪除過期的緩存
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
//真正的目錄(boolean NSNumber),資源的最后修改時間(NSDate),占用大小(NSNumber)
NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// This enumerator prefetches useful properties for our cache files.
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
//過期時間
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
//遍歷所有緩存目錄的文件,兩個目的:
//1:通過文件信息刪除過期的緩存
//2.計算所有文件的大小,根據(jù)最大緩存大小進行清除
//需要刪除的文件列表
NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSError *error;
//指定文件信息類型的字典
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
// Skip directories and errors.
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// Remove files that are older than the expiration date;
//根據(jù)文件修改時間將過期的文件添加到刪除列表中
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// Store a reference to this file and account for its total size.
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
//累加計算文件占用總大小
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
//儲存剩下文件的文件信息 NSURL:NSDictionay
cacheFiles[fileURL] = resourceValues;
}
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
// If our remaining disk cache exceeds a configured maximum size, perform a second
// size-based cleanup pass. We delete the oldest files first.
//所有文件占用大小超出
if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
// Target half of our maximum cache size for this cleanup pass.
//以最大限制大小的一半為基準
const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
// Sort the remaining cache files by their last modification time (oldest first).
//根據(jù)文件最后修改時間進行排序
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// Delete files until we fall below our desired cache size.
for (NSURL *fileURL in sortedFiles) {
if ([_fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
下載操作
- SDWebImageOperation
- SDWebImageDownloader
- SDWebImageDownloaderOperation
SDWebImageOperation
//協(xié)議 用于發(fā)送取消操作
@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end
SDWebImageDownloaderOperation
首先是定義一些通知,用于監(jiān)聽下載操作的全過程
//任務開始
extern NSString * _Nonnull const SDWebImageDownloadStartNotification;
//接收到數(shù)據(jù)
extern NSString * _Nonnull const SDWebImageDownloadReceiveResponseNotification;
//暫停
extern NSString * _Nonnull const SDWebImageDownloadStopNotification;
//完成
extern NSString * _Nonnull const SDWebImageDownloadFinishNotification;
然后是定義了一個接口(協(xié)議)SDWebImageDownloaderOperationInterface,由操作對象(NSOperation的子類)進行實現(xiàn).必須遵循這個協(xié)議.
@protocol SDWebImageDownloaderOperationInterface<NSObject>
//使用NSURLRequest,NSURLSession和SDWebImageDownloaderOptions初始化
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options;
//可以為每一個NSOperation自由的添加相應對象
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
///設置是否需要解壓圖片
- (BOOL)shouldDecompressImages;
- (void)setShouldDecompressImages:(BOOL)value;
//設置是否需要設置憑證
- (nullable NSURLCredential *)credential;
- (void)setCredential:(nullable NSURLCredential *)value;
@end
SDWebImageDownloaderOperation是NSOperation的子類,一般重寫其Start方法,執(zhí)行我們需要進行的業(yè)務.我們也可以自定義一個NSOperation的子類,但在這里需要注意的是,該類必須遵循SDWebImageDownloaderOperationInterface協(xié)議,用于配置一些信息和添加回調.
//開啟下載任務
- (void)start {
@synchronized (self) {
// 如果該任務已經(jīng)被設置為取消了,那么就無需開啟下載任務了,重置。
if (self.isCancelled) {
self.finished = YES;
[self reset];
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) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
//task開啟前的準備工作
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
//新建網(wǎng)絡會話對象
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
//delegateQueue設置為nil.代理方法將會在一個串行操作隊列中執(zhí)行
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
//開啟task 并處理回調
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
[self.dataTask resume];
if (self.dataTask) {
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
//下載進度
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
dispatch_async(dispatch_get_main_queue(), ^{
//發(fā)送開始下載通知
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
} else {
//任務開啟失敗
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
}
#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
}
dispatch_barrier_async,這個方法是向數(shù)組中添加數(shù)據(jù),barrier是柵欄的意思,當一個隊列中通過dispatch_barrier_async|sync添加一個任務后,這個任務就起到了一個攔截的作用,之后的任務必須等barrier_async|sync之前添加的任務完成才能夠執(zhí)行,而dispatch_barrier_async與dispatch_barrier_sync的區(qū)別也很簡單和直觀,dispatch_barrier_async不用等這個任務返回,就能夠執(zhí)行隊列后的任務,而dispatch_barrier_sync必須等自身返回,才能夠執(zhí)行隊列后的任務.往數(shù)組中添加數(shù)據(jù)截碴,對順序沒什么要求,采取dispatch_barrier_async就已經(jīng)能保證數(shù)據(jù)添加的安全性了隐岛。
//添加響應者
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
//回調信息字典
SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
dispatch_barrier_async(self.barrierQueue, ^{
[self.callbackBlocks addObject:callbacks];
});
return callbacks;
}
網(wǎng)絡請求:NSURLSession,主要是一些代理方法,和根據(jù)圖片數(shù)據(jù)的下載速度來進行圖片的繪制,可用于圖片的漸進式加載顯示.
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
//沒有收到響應碼或者響應碼小于400,但不等于304的時候,視為成功
if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode < 400 && ((NSHTTPURLResponse *)response).statusCode != 304)) {
//成功
NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
self.expectedSize = expected;
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, expected, self.request.URL);
}
self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
self.response = response;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
});
}
else {
NSUInteger code = ((NSHTTPURLResponse *)response).statusCode;
//stateCode=304,這個響應沒有變化齐帚,我們可以取消這個操作和返回緩存中的圖片數(shù)據(jù).
if (code == 304) {
[self cancelInternal];
} else {
[self.dataTask cancel];
}
dispatch_async(dispatch_get_main_queue(), ^{
//停止下載
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});
//下載失敗
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:((NSHTTPURLResponse *)response).statusCode userInfo:nil]];
[self done];
}
if (completionHandler) {
completionHandler(NSURLSessionResponseAllow);
}
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
//拼接數(shù)據(jù)
[self.imageData appendData:data];
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
// 獲取已經(jīng)下載的全部數(shù)據(jù)大小
const NSInteger totalSize = self.imageData.length;
// 創(chuàng)建圖片源對象
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
//沒有進行過賦值操作
if (width + height == 0) {
//獲取圖片信息
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);
#if SD_UIKIT || SD_WATCH
//Core graphics繪制時,會丟失方向信息.通過initWithCGImage獲取的對象的方向是錯誤的,所以在這里保存方向信息,而initWithData不會
//方向轉換
orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
#endif
}
}
//繪制圖片
if (width + height > 0 && totalSize < self.expectedSize) {
// Create the image
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
#if SD_UIKIT || SD_WATCH
// Workaround for iOS anamorphic image
if (partialImageRef) {
const size_t partialHeight = CGImageGetHeight(partialImageRef);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(colorSpace);
if (bmContext) {
CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
CGImageRelease(partialImageRef);
partialImageRef = CGBitmapContextCreateImage(bmContext);
CGContextRelease(bmContext);
}
else {
CGImageRelease(partialImageRef);
partialImageRef = nil;
}
}
#endif
if (partialImageRef) {
#if SD_UIKIT || SD_WATCH
UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
#elif SD_MAC
UIImage *image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize];
#endif
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
UIImage *scaledImage = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:scaledImage];
}
else {
image = scaledImage;
}
CGImageRelease(partialImageRef);
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
}
}
CFRelease(imageSource);
}
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
}
}
下載器SDWebImageDownloader
SDWebImageDownloader內部主要管理者一個操作隊列,用于執(zhí)行圖片下載的任務.比如設置隊列的執(zhí)行順序FIFO(先進先出),LIFO(后進先出),任務的最大并發(fā)量
還可以配置網(wǎng)絡請求參數(shù),設置請求頭,證書,網(wǎng)絡超時時間等
//下載操作 主要方法
//返回一個SDWebImageDownloadToken對象,可以用來取消下載任務
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
//創(chuàng)建操作對象完成之后 進行網(wǎng)絡參數(shù)配置和處理操作列表的執(zhí)行順序(添加依賴)
__strong __typeof (wself) sself = wself;
//超時時間
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
//為了防止?jié)撛诘闹貜瓦M行緩存(NSURLCache + SDImageCache),禁用了image requests
//SDWebImageDownloaderUseNSURLCache -> NSURLRequestUseProtocolCachePolicy
// cachePolicy:創(chuàng)建的request所使用的緩存策略剪菱,默認使用`NSURLRequestUseProtocolCachePolicy`旗们,該策略表示如果緩存不存在上渴,直接從服務端獲取稠氮。如果緩存存在隔披,會根據(jù)response中的Cache-Control字段判斷,下一步操作锹锰,如: Cache-Control字段為must-revalidata, 則 詢問服務端該數(shù)據(jù)是否有更新,無更新話直接返回給用戶緩存數(shù)據(jù)渺蒿,若已更新怠蹂,則請求服務端.
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
//HTTPShouldHandleCookies表示是否應該給request設置cookie并隨request一起發(fā)送出去,如果設置HTTPShouldHandleCookies為YES城侧,就處理存儲在NSHTTPCookieStore中的cookies
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
// HTTPShouldUsePipelining表示receiver(client客戶端)的下一個信息是否必須等到上一個請求回復才能發(fā)送嫌佑。如果為YES表示可以屋摇,NO表示必須等receiver收到先前的回復才能發(fā)送下個信息炮温。
request.HTTPShouldUsePipelining = YES;
//請求頭
if (sself.headersFilter) {
//將block做為參數(shù),對請求頭信息進行過濾或操作
request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = sself.HTTPHeaders;
}
//根據(jù)操作配置,請求參數(shù),網(wǎng)絡會話創(chuàng)建下載操作對象
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
//是否允許壓縮
operation.shouldDecompressImages = sself.shouldDecompressImages;
//認證
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
//新建認證對象
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
//操作對象優(yōu)先級
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
//添加到操作隊列
[sself.downloadQueue addOperation:operation];
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
//如果是后進先出,為上一個添加的操作對象添加依賴為當前的操作對象,然后重新賦值
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
SDWebImageManager核心類
SDWebImageManager是整個框架的核心管理者,內部管理著緩存處理和圖片下載任務.
主要接口
//全局單例對象
+ (nonnull instancetype)sharedManager;
//構造函數(shù)
- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader NS_DESIGNATED_INITIALIZER;
//下載圖片,返回遵循SDWebImageOperation協(xié)議的NSOperation對象,默認為SDWebImageDownloaderOperation
- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;
//緩存
- (void)saveImageToCache:(nullable UIImage *)image forURL:(nullable NSURL *)url;
//取消所有當前的操作
- (void)cancelAll;
//操作是否在運行
- (BOOL)isRunning;
//異步檢查Memory中是否有緩存,block在主線程中執(zhí)行
- (void)cachedImageExistsForURL:(nullable NSURL *)url
completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;
//異步檢查磁盤中是否有緩存,block在主線程中執(zhí)行
- (void)diskImageExistsForURL:(nullable NSURL *)url
completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;
核心方法
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
//沒有回調block是無意義的的操作 拋異常
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
//傳NSString(不會報錯) -> NSURL
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// 用NSNull代替一個非NSURL對象
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
BOOL isFailedUrl = NO;
if (url) {
//檢查是否在失效URL列表中
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
}
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
//文件不存在
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
return operation;
}
@synchronized (self.runningOperations) {
//添加到操作列表中
[self.runningOperations addObject:operation];
}
NSString *key = [self cacheKeyForURL:url];
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
if (operation.isCancelled) {
[self safelyRemoveOperationFromRunning:operation];
return;
}
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
if (cachedImage && options & SDWebImageRefreshCached) {
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
[self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
// download if no image or requested to refresh anyway, and download allowed by delegate
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
if (cachedImage && options & SDWebImageRefreshCached) {
// force progressive off if image already cached but forced refreshing
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
//下載
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
// Do nothing if the operation was cancelled
// See #699 for more details
// if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
} else if (error) {
//下載失敗,回調曬白信息,添加到失效列表中
[self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}
else {
if ((options & SDWebImageRetryFailed)) {
//禁用失效
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
} else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
//下載成功 && (不是gif或者SDWebImageTransformAnimatedImage) &&實現(xiàn)代理
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
// pass nil if the image was transformed, so we can recalculate the data from the image
//儲存緩存
[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
}
//回調
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
});
} else {
if (downloadedImage && finished) {
//緩存到內存中(內部同時緩存在磁盤中)
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
//回調
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
if (finished) {
//安全的移除操作對象
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
operation.cancelBlock = ^{
[self.imageDownloader cancel:subOperationToken];
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self safelyRemoveOperationFromRunning:strongOperation];
};
} else if (cachedImage) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
} else {
// Image not in cache and download disallowed by delegate
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
}
}];
return operation;
}