分为内存缓存(利用SDImageCache类的NSCache属性),磁盘缓存(利用NSFileManager),和操作缓存(利用runtime关联的字典属性)。下载之前先查询缓存,没有就下载并在下载后保存图片到缓存。
(1). 查询图片缓存 的内部API
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
调用位置 -- SDWebImageManager.m
- (id )loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
(2). 保存图片到缓存 的内部API
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
调用位置 -- SDWebImageManager.m
- (id )loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
实现原理 --- SDImageCache.m
其中,数据转换部分 原理为:
- (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;
}
其中,保存到沙盒部分 原理为:
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key { if (!imageData || !key) { return;
}
[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];
}
}
在NSURLSession下载完成后的代理方法中,具体文件是SDWebImageDownloaderOperation.m。
内部API
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
调用位置1 -- SDWebImageDownloaderOperation.m代理1
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { @synchronized(self) { self.dataTask = nil; dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; if (!error) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
}
});
} if (error) {
[self callCompletionBlocksWithError:error];
} else { if ([self callbacksForKey:kCompletedCallbackKey].count > 0) { /**
* See #1608 and #1623 - apparently, there is a race condition on `NSURLCache` that causes a crash
* Limited the calls to `cachedResponseForRequest:` only for cases where we should ignore the cached response
* and images for which responseFromCached is YES (only the ones that cannot be cached).
* Note: responseFromCached is set to NO inside `willCacheResponse:`. This method doesn't get called for large images or images behind authentication
*/ if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) { // hack [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
} else if (self.imageData) { UIImage *image = [UIImage sd_imageWithData:self.imageData]; NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image]; // Do not force decoding animated GIFs if (!image.images) { if (self.shouldDecompressImages) { if (self.options & SDWebImageDownloaderScaleDownLargeImages) { #if SD_UIKIT || SD_WATCH image = [UIImage decodedAndScaledDownImageWithImage:image];
[self.imageData setData:UIImagePNGRepresentation(image)]; #endif } else {
image = [UIImage decodedImageWithImage:image];
}
}
} if (CGSizeEqualToSize(image.size, CGSizeZero)) {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
} else {
[self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES];
}
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
}
}
}
[self done];
}
调用位置2 -- SDWebImageDownloaderOperation.m代理1
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { //省略...
实现原理 -- SDWebImageDecoder.m
+ (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);
size_t bytesPerRow = kBytesPerPixel * width; // kCGImageAlphaNone is not supported in CGBitmapContextCreate. // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast // to create bitmap graphics contexts without alpha info. 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 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;
}
}
有时候会把将要执行的内容放到主线程里面执行,但如果已经是主线程里面的代码调用dispatch_async的时候偶尔会出现crash,所以就需要判断是否已经在主线程里面了。
老版本的SDWebImage这样封装了一个宏:
//主线程同步队列 #define dispatch_main_sync_safe(block)/ if ([NSThread isMainThread]) {/
block();/
} else {/ dispatch_sync(dispatch_get_main_queue(), block);/
} //主线程异步队列 #define dispatch_main_async_safe(block)/ if ([NSThread isMainThread]) {/
block();/
} else {/ dispatch_async(dispatch_get_main_queue(), block);/
}
//用法 dispatch_main_async_safe(^{ //需要执行的代码片段; });
新版本的SDWebImage是这样封装的宏:
#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
其中的strcmp是字符串比较函数:
int strcmp(char *str1, char *str2)
当str1>str2时,返回一个正数;
当str1当str1=str2时,返回0。
最后应该注意的是:两个字符串比较时,是按asiic码大小逐个比较的,当发现某一个大或者小时,就停止比较、返回一个值。否则比较到最后一个字母。
注意的问题是,宏里面的block是无法打断点调试的。你如果步进查看,可以发现会跳到汇编的文件里面步进。
SDWebImageManager提供的关键API是loadImageWithURL开头的,负责加载的,加载load这个词跟下载download不同,比它更广,加载负责管理下载之前的操作:
管理下载操作的开始和取消
下载之前查询图片的内存缓存和磁盘缓存
下载之后保存图片到内存缓存和磁盘缓存
返回一个操作对象给上级对象UIImageView+WebCache作为操作缓存数组属性中去
SDWebImageDownloader提供的关键API是downloadImageWithURL开头的,可见它仅仅管理下载的操作,没有缓存的管理功能。