前言
SDWebImage是一个强大的第三方图片异步加载库,从事iOS开发的人或多或少用过它。今天我点开源码一看,里面一些实现细节确实令人玩味,同时也让我了解到缓存机制是如何实现。
简析
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
作者利用category为一些UIKit类拓展出相应的缓存功能,其中UIImageView和UIButton使用频率比较高,因为日常开发中,我们需要这两个控件加载图片。上面那个方法是UIImageView+WebCache最常用的方法,传一个url,UIImageView就会将网络图片加载出来,这个方法看起来简单,可是背后的实现没那么容易,不禁感慨大神之牛,巧妙的设计和背后复杂的实现竟然浓缩成这样的一个接口函数。
点进去看源码
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
这个是原始接口
点进去这个原始接口
我可以看到这个函数体
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock
这个函数体就是就是sd_setImage的实现逻辑,利用block回调,有加载过程的回调和加载完成的回调
[self sd_cancelCurrentImageLoad];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
首先,它先取消下载队列里正在进行的任务,将图片的url和UIImageView关联起来作为UIImageView的一部分;如果有placeholder,就先将它显示出来;
接着就是网络加载图片实现逻辑的主要部分
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (!wself) return;
dispatch_main_sync_safe(^{
if (!wself) return;
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
completedBlock(image, error, cacheType, url);
return;
}
else if (image) {
wself.image = image;
[wself setNeedsLayout];
} else {
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
利用一个全局的SDWebImageManager来进行下载缓存操作的管理和调度。SDWebImageManager由两部分构成,由SDImageCache缓存器和SDWebImageDownloader下载器构成。同步上面加载过程的回调,在completed里除了同步加载完成的回调外,还将block回调的image加载在控件上面。
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
这个函数是为了判断当前是否有重复的网络图片加载操作,如果有,就取消,key作为判断加载操作的标识。
点进去看downloadImageWithURL源码
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
首先,先new一个新的operation,operation是遵守SDWebImageOperation协议的一个对象,协议有个cancle方法,利用cancle可以取消当前操作
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
if (!url || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
dispatch_main_sync_safe(^{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
});
return operation;
}
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
接着我们可以看到一个实现的亮点,它用一个failedURLs集合来记录失败或者无效的url,如果传进来的url是failedURLs这个集合里的或者是为空,则完成操作并回调,否则将operation添加到运行的operation数组中。
NSString *key = [self cacheKeyForURL:url];
}
它将url缓存起来,给他一个key值
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {}];
SDWebImageManager的缓存器根据缓存的url的key值来寻找是否有对应的缓存
点进去看queryDiskCacheForKey源码
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
如果相关的缓存记录则doneBlock回调,标识为SDImageCacheTypeNone
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
先在内存里寻找,SDImage用的内存缓存是用NSCache实现的,如果有就是doneBlock回调,标志为SDImageCacheTypeMemory
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
如果没有,就从磁盘缓存里找,如果找到了,就将他缓存到内存中,doneBlock回调,标志为SDImageCacheTypeDisk
返回上层函数查看done:^(UIImage *image, SDImageCacheType cacheType) {}这个block的主体
我们只看如果没有图片缓存该如何做
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {};
SDWebImageManager的下载器开启下载operation来下载图片
点进去看downloadImageWithURL源码
下载的实现就不展开讲了,它利用的苹果提供的NSURLConnection来实现。
wself.lastAddedOperation = operation;
同时记录最后一个下载操作,最后block回调。
图片下载完成后,利用SDWebImageManager的缓冲器将图片缓存到内存和磁盘中,并将operation从运行的operation数组移除,如果失败则记录他是一个失败的url,成功则block回调。