介紹
github地址:
https://github.com/rs/SDWebImage
簡介
一個異步圖片下載及緩存的庫
特性:
- 一個擴展UIImageView分類的庫,支持加載網絡圖片並緩存圖片
- 異步圖片下載器
- 異步圖片緩存和自動圖片有效期管理
- 支持GIF動態圖片
- 支持WebP
- 背景圖片減壓
- 保證同一個URL不會再次下載
- 保證無效的URL不會重新加載
- 保證主線程不會死鎖
- 性能優越
- 使用GCD和ARC
- 支持ARM64位處理器
其他
- 3.0后不再支持5.1.1
- 支持Cocoapods
- 用法不說了 github上有,挺簡單的
原理
只要有圖片的url,就能下載到圖片,使用SDWebImage的好處就是緩存機制,每次取圖片先判斷是否在內存中,再到緩存中查找,找到了直接加載,在緩存中找不到才重新下載,url也會記錄,是否是失效的url,是則不會再嘗試。下載到的圖片會緩存,用於下次可以直接加載。圖片下載,解碼,轉換都異步進行,不會阻塞主線程。
類的作用
SDImageCache
設置緩存的類型,方式,路徑等
SDWebImageCompat
兼容類,定義了很多宏和一個轉換圖片的方法
SDWebImageDecoder
解碼器,讓圖片色彩轉換(涉及到color space)
SDWebImageDownloader
下載器,設置下載相關,要用到SDWebImageDownloaderOperation
SDWebImageDownloaderOperation
下載器的操作
SDWebImageManager
管理圖片下載,取消操作,判斷url是否已緩存等
SDWebImageOperation
圖片操作,后面很多類都要用到
SDWebImagePrefetcher
預抓取器,預先下載urls中的圖片
UIButton+WebCache
按鈕圖片的緩存
UIImage+GIF
緩存gif
NSData+ImageContentType
判斷圖片的類型,png/jpeg/gif/webp
UIImage+MultiFormat
緩存多種格式的圖片,要用到NSData+ImageContentType的判斷圖片類型方法和UIImage+GIF的判斷是否為gif圖片方法,以及ImageIO里面的方法
UIImageView+HighlightedWebCache
緩存高亮圖片
UIImageView+WebCache
主要用到這個,加載及緩存UIImageView的圖片
UIView+WebCacheOperation
緩存的操作,有緩存,取消操作,移除緩存
源碼解析
先講一些比較邊緣的方法
0.SDWebImageOperation
圖片操作,只有頭文件,定義了協議SDWebImageOperation,里面也只有取消方法
這個類后面很多類都要用到。
@protocol SDWebImageOperation <NSObject> - (void)cancel; @end
1.NSData+ImageContentType
這個文件是NSData的分類,只有一個方法,傳入圖片數據,根據圖片的頭標識來確定圖片的類型。頭標識都不一樣,只需獲取文件頭字節,對比十六進制信息,判斷即可。
| 圖片文件 | 頭標識 | 十六進制頭字節 |
|---|---|---|
| jpeg/jpg | FFD8 | 0xFF |
| png | 8950 | 0x89 |
| gif | 4749 | 0x47 |
| tiff | 4D4D / 4949 | 0x49/0x4D |
Webp格式開頭是0x52,但是還有可能是其他類型文件,所以要識別前綴為
52 49 46 46 對應 RIFF
后綴 57 45 42 50 對應 WEBP,符合這些條件的才是webp圖片文件
+ (NSString *)sd_contentTypeForImageData:(NSData *)data {
uint8_t c;
[data getBytes:&c length:1];
switch (c) {
case 0xFF:
return @"image/jpeg";
case 0x89:
return @"image/png";
case 0x47:
return @"image/gif";
case 0x49:
case 0x4D:
return @"image/tiff";
case 0x52:
// R as RIFF for WEBP
if ([data length] < 12) {
return nil;
}
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
return @"image/webp";
}
return nil;
}
return nil;
}
2.SDWebImageCompat
兼容類,這個類定義了很多宏還有一個伸縮圖片的方法,宏就不說了
這個方法定義成C語言式的內聯方法
核心代碼如下,傳入key和圖片,如果key中出現@2x就設定scale為2.0,出現@3x就設定scale為3.0,然后伸縮圖片
CGFloat scale = [UIScreen mainScreen].scale;
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;
}
}
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
image = scaledImage;
3.SDWebImageDecoder
這個是解碼器類,只定義了一個解碼方法,傳入圖片,返回的也是圖片
CGImageRef是一個指針類型。typedef struct CGImage *CGImageRef;
獲取傳入圖片的alpha信息,然后判斷是否符合蘋果定義的CGImageAlphaInfo,如果是就返回原圖片
CGImageRef imageRef = image.CGImage;
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
alpha == kCGImageAlphaLast ||
alpha == kCGImageAlphaPremultipliedFirst ||
alpha == kCGImageAlphaPremultipliedLast);
if (anyAlpha) { return image; }
然后獲取圖片的寬高和color space(指定顏色值如何解釋),判斷color space是否支持,不支持就轉換為支持的模式(RGB),再用圖形上下文根據獲得的信息畫出來,釋放掉創建的CG指針再返回圖片
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
// current
CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
bool unsupportedColorSpace = (imageColorSpaceModel == 0 || imageColorSpaceModel == -1 || imageColorSpaceModel == kCGColorSpaceModelCMYK || imageColorSpaceModel == kCGColorSpaceModelIndexed);
if (unsupportedColorSpace)
colorspaceRef = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(NULL, width,
height,
CGImageGetBitsPerComponent(imageRef),
0,
colorspaceRef,
kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithAlpha = [UIImage imageWithCGImage:imageRefWithAlpha scale:image.scale orientation:image.imageOrientation];
if (unsupportedColorSpace)
CGColorSpaceRelease(colorspaceRef);
CGContextRelease(context);
CGImageRelease(imageRefWithAlpha);
return imageWithAlpha;
這個算是核心部分
4.UIView+WebCacheOperation
緩存操作的UIView的分類,支持三種操作,也是整個庫中比較核心的操作。
但是首先我們來了解三種操作都要用到的存儲數據的方法。
這兩個方法用的是OC中runtime方法,原理是兩個文件關聯方法,和上層的存儲方法差不多,傳入value和key對應,取出也是根據key取出value
object傳入self即可
1.設置關聯方法
//傳入object和key和value,policy //policy即存儲方式,和聲明使用幾種屬性大致相同,有copy,retain,copy,retain_nonatomic,assign 五種) void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
2.取出方法
//傳入object和key返回value
id objc_getAssociatedObject(id object, const void *key)
這個方法是三種操作都要用到的,獲得數據
這個方法是使用前面兩個方法,根據緩存加載數據
有緩存則從緩存中取出數據,沒有則緩存數據,返回格式是字典格式
- (NSMutableDictionary *)operationDictionary {
NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
if (operations) {
return operations;
}
operations = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}
接下來是三種操作
一.加載圖片根據是否有緩存
從獲得數據方法獲得數據,傳入key,先調用第二個方法停止操作,再根據key緩存數據
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
[self sd_cancelImageLoadOperationWithKey:key];
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary setObject:operation forKey:key];
}
二.取消加載圖片如果有緩存
先獲得方法一的返回字典數據,傳入key在返回的字典中查找是否已經存在,如果存在則取消所有操作
conformsToProtocol方法如果符合這個協議(協議中聲明了取消方法),調用協議中的取消方法
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
// Cancel in progress downloader from queue
NSMutableDictionary *operationDictionary = [self operationDictionary];
id operations = [operationDictionary objectForKey:key];
if (operations) {
if ([operations isKindOfClass:[NSArray class]]) {
for (id <SDWebImageOperation> operation in operations) {
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
[(id<SDWebImageOperation>) operations cancel];
}
[operationDictionary removeObjectForKey:key];
}
}
三.移除緩存
獲得方法一的數據,傳入key如果key對應的數據在數據中則移除
- (void)sd_removeImageLoadOperationWithKey:(NSString *)key {
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary removeObjectForKey:key];
}
5.SDWebImageDownloader
下載器類,需要用到SDWebImageDownloaderOperation類,下載器操作,后面會說到
定義了一些屬性
//下載隊列的最大下載數 @property (assign, nonatomic) NSInteger maxConcurrentDownloads; //當前下載數 @property (readonly, nonatomic) NSUInteger currentDownloadCount; //下載超時的時間 @property (assign, nonatomic) NSTimeInterval downloadTimeout; //是否解壓圖片,默認是 @property (assign, nonatomic) BOOL shouldDecompressImages; //下載器順序,枚舉類型,有兩種,先進先出,還是后進先出 @property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder; #####還有一些用戶屬性 //url證書 @property (strong, nonatomic) NSURLCredential *urlCredential; //用戶名 @property (strong, nonatomic) NSString *username; //密碼 @property (strong, nonatomic) NSString *password; //頭像過濾器,block指針類型,接受url和字典headers @property (nonatomic, copy) SDWebImageDownloaderHeadersFilterBlock headersFilter;
init方法
初始化了一些屬性和寫好http請求頭
- (id)init {
if ((self = [super init])) {
_operationClass = [SDWebImageDownloaderOperation class];
_shouldDecompressImages = YES;
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_URLCallbacks = [NSMutableDictionary new];
#ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
_downloadTimeout = 15.0;
}
return self;
}
核心方法
傳入url,下載器選項(接下來會說),進度block,完成回調block
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
這個方法非常復雜,定義了http請求,定義了SDWebImageDownloaderOperation實例,即下載器操作,初始化過程非常復雜,用到了http請求,用到了前面定義的那些屬性,最后返回這個操作,這個過程建議去看源碼
6.SDWebImageDownloaderOperation
下載器的操作
直接看前面下載器需要用到的初始化方法
需要初始化了各種屬性,主要是幾個block,進度block,完成回調block,取消回調block
- (id)initWithRequest:(NSURLRequest *)request
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock
cancelled:(SDWebImageNoParamsBlock)cancelBlock {
if ((self = [super init])) {
_request = request;
_shouldDecompressImages = YES;
_shouldUseCredentialStorage = YES;
_options = options;
_progressBlock = [progressBlock copy];
_completedBlock = [completedBlock copy];
_cancelBlock = [cancelBlock copy];
_executing = NO;
_finished = NO;
_expectedSize = 0;
responseFromCached = YES;
}
return self;
}
7.SDWebImageManager
圖片管理器,負責圖片的下載,轉換,緩存等
這里先說明SDWebImageOptions
1 << X 這種是位運算符,1左移多少位,后面要用到,說明一下
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
SDWebImageRetryFailed = 1 << 0,//無效url會加入黑名單,這個標志是禁用黑名單
SDWebImageLowPriority = 1 << 1, //低優先級,會后下載
SDWebImageCacheMemoryOnly = 1 << 2, //禁用磁盤緩存
SDWebImageProgressiveDownload = 1 << 3, //顯示下載進度,下載完才顯示
SDWebImageRefreshCached = 1 << 4, //重新從遠程緩存
SDWebImageContinueInBackground = 1 << 5, //在后台繼續下載圖片
SDWebImageHandleCookies = 1 << 6, //把cookie存儲到NSHTTPCookieStorey
SDWebImageAllowInvalidSSLCertificates = 1 << 7, //允許非信任ssl證書
SDWebImageHighPriority = 1 << 8, //高優先級,插隊下載隊列
SDWebImageDelayPlaceholder = 1 << 9, //顯示的是替代圖片(初始化圖片)
SDWebImageTransformAnimatedImage = 1 << 10, //轉換圖片大小
SDWebImageAvoidAutoSetImage = 1 << 11 //避免自動設置圖片(想手動的時候設置)
};
這里包含了各種選擇
核心方法
傳入url,上面的options,進度block,完成回調block
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
實例化過程請去看源碼
說下其他方法
一個傳入key判斷圖片是否存在存儲空間的方法
使用的是NSFileManager的方法
- (BOOL)diskImageExistsWithKey:(NSString *)key {
BOOL exists = NO;
exists = [[NSFileManager defaultManager] fileExistsAtPath:[self defaultCachePathForKey:key]];
if (!exists) {
exists = [[NSFileManager defaultManager] fileExistsAtPath:[[self defaultCachePathForKey:key] stringByDeletingPathExtension]];
}
return exists;
}
還有從存儲空間或者緩存取出圖片的方法
self.memCache是AutoPurgeCache(單純繼承自NSCache)的實例
從存儲空間取圖片要先判斷內存中是否存在,然后才從存儲空間中查找
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];
}
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key {
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
return image;
}
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
return diskImage;
}
- (UIImage *)diskImageForKey:(NSString *)key {
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
if (data) {
UIImage *image = [UIImage sd_imageWithData:data];
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
return image;
}
else {
return nil;
}
}
還有幾個清除方法
- (void)clearDisk; - (void)clearMemory; - (void)cleanDisk; - (void)cleanDiskWithCompletionBlock; ...
8.SDWebImagePrefetcher
預抓取器,用來預抓取圖片
核心方法
//預抓取圖片 - (void)prefetchURLs:(NSArray *)urls progress:(SDWebImagePrefetcherProgressBlock)progressBlock completed:(SDWebImagePrefetcherCompletionBlock)completionBlock; //取消預抓取圖片 - (void)cancelPrefetching;
先來看預抓取圖片
傳入url,進度block,完成回調block
首先取消抓取,然后重新開始
- (void)prefetchURLs:(NSArray *)urls progress:(SDWebImagePrefetcherProgressBlock)progressBlock completed:(SDWebImagePrefetcherCompletionBlock)completionBlock {
[self cancelPrefetching]; // Prevent duplicate prefetch request
self.startedTime = CFAbsoluteTimeGetCurrent();
self.prefetchURLs = urls;
self.completionBlock = completionBlock;
self.progressBlock = progressBlock;
if (urls.count == 0) {
if (completionBlock) {
completionBlock(0,0);
}
} else {
NSUInteger listCount = self.prefetchURLs.count;
for (NSUInteger i = 0; i < self.maxConcurrentDownloads && self.requestedCount < listCount; i++) {
[self startPrefetchingAtIndex:i];
}
}
}
最后調用startPrefetchingAtIndex:方法,再調用self.manager的核心方法,即開始下載圖片
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
最后是各種分類,即直接再初始化的控件設置圖片,支持UIButton,UIImage,UIImageView,大同小異,我直接說UIImageView+WebCache
9.UIImageView+WebCache
很多加載方法最終都會以缺省參數方式或者直接調用這個方法,傳入一個URL,一個用來初始化的image,一個options(枚舉,下面詳細說明),一個progressBlock(返回圖片接受進度等),一個completedBlock(完成回調block)
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
首先根據url緩存圖片,這里用到的是OC的runtime中的關聯方法(見4)
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
然后判斷options(見7)是其他選擇則直接給圖片賦值placehoder圖片,這里判斷使用的是 & 與 位運算符,SDWebImageDelayPlacehoder是 1 << 9,1左移9位與options相與
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
如果url存在,則定義圖片操作,使用圖片管理器的單例來調用核心方法(下載圖片方法)
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
//過程省略
}
10.UIImage+GIF
gif的實現使用了ImageIO中的CGImageSourceRef
用獲得的gif數據得到CGImageSourceRef,然后算出時間,在這個時間內把圖片一幀一幀的放進一個數組,最后再把這個數組和時間轉成圖片,就成了gif
+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
if (!data) {
return nil;
}
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
size_t count = CGImageSourceGetCount(source);
UIImage *animatedImage;
if (count <= 1) {
animatedImage = [[UIImage alloc] initWithData:data];
}
else {
NSMutableArray *images = [NSMutableArray array];
NSTimeInterval duration = 0.0f;
for (size_t i = 0; i < count; i++) {
CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
duration += [self sd_frameDurationAtIndex:i source:source];
[images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];
CGImageRelease(image);
}
if (!duration) {
duration = (1.0f / 10.0f) * count;
}
animatedImage = [UIImage animatedImageWithImages:images duration:duration];
}
CFRelease(source);
return animatedImage;
}
總結
看完SDWebImage的源碼后感覺學到了很多東西,特別是緩存那一塊寫的特別好。ImageIO和objc/runtime很值得學習一下。也感覺到設計出這樣一個庫需要很強大的知識面,非常嚴謹的思想,真的不容易。
安♂利
