推荐阅读
SDWebImage4.0源码探究(一)面试题
https://www.jianshu.com/p/b8517dc833c7SDWebImage详解
https://www.jianshu.com/p/9f4af32b25efiOS进阶-剖析SDWebImage
https://www.jianshu.com/p/7c93b7089d24iOS SDWebimage 源码阅读
https://www.jianshu.com/p/18b635ca0456
SDWebImage 架构
SDWebImage内部原理
流程图
SDWebImage 详解
SDWebImage特性
- 提供了UIImageView,UIButton,MKAnnotationView的分类,用来显示网络图片,以及缓存管理
- 采用异步方式来下载图片
- 采用异步方式使用memory+disk来缓存网络图片,自动管理缓存
- 支持GIF和webP格式
- 使用GCD和ARC,不会阻塞主线程
- 同一个URL不会重复下载
- 自动识别无效URL,不会反复重试
SDWebImage用法
为UIImageView加载图片
1.sd_setImageWithURL:
//直接加载网络图片
[iv1 sd_setImageWithURL:[NSURL URLWithString:imageArray[0]]];
2.sd_setImageWithURL: placeholderImage:
//先展示默认图片,当网络图片加载完成后替换
[iv2 sd_setImageWithURL:[NSURL URLWithString:imageArray[1]] placeholderImage:[UIImage imageNamed:@"defaultImage"]];
3.sd_setImageWithURL: placeholderImage: options:
//先展示默认图片,并设置网络图片处理方式
[iv3 sd_setImageWithURL:[NSURL URLWithString:imageArray[2]] placeholderImage:[UIImage imageNamed:@"defaultImage"] options:SDWebImageCacheMemoryOnly];
4.sd_setImageWithURL: placeholderImage: options: completed:
//展示默认图片,设置图片处理方式,以及执行图片加载完成后的block内部代码
[iv4 sd_setImageWithURL:[NSURL URLWithString:imageArray[3]] placeholderImage:[UIImage imageNamed:@"defaultImage"] options:SDWebImageRefreshCached completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
NSLog(@"imageSize--height:%f,width:%f",image.size.height,image.size.width);
}];
其他加载方式不一一介绍,可在SDWebImage源码中查看
SDWebImage内部原理
核心逻辑
1.给 UIImageView 设置图片,然后 SDWebImage 调用这个最终的图片加载方法。
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
[self sd_cancelCurrentImageLoad];//取消先前的下载任务
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);//动态添加属性
//设置默认图片
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
if (url) {
// check if activityView is enabled or not
if ([self showActivityIndicatorView]) {
[self addActivityIndicator];
}
__weak __typeof(self)wself = self;//防止循环引用
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
[wself removeActivityIndicator];
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);
}
});
}];
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else {
dispatch_main_async_safe(^{
[self removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
以上方法主要进行以下几件事:
- 取消当前正在进行的加载任务operation
- 设置placeholder
- 如果URL不为nil,就通过SDWebImageManager单例开启图片加载任务operation,SDWebImageManager的图片加载方法中会返回一个SDWebImageOperation对象,这个对象包含一个cacheOperation和一个cancelBlock。
2.进入 SDWebImageManager类进行图片加载
在上面代码中的downloadImageWithURL:options:progress:completed:方法中会先拿图片的key(默认是图片的url)去SDImageCache单例中通过queryDiskCacheForKey: done:方法读取内存缓存,如果有,就返回给SDWebImageManager;如果内存缓存没有,就开启异步线程,拿经过MD5处理的key去读取磁盘缓存,如果找到则同步到内存缓存,然后返回给SDWebImageManager。
如果内存缓存和磁盘缓存中都没有,SDWebImageManager就会调用SDWebImageDownloader单例的downloadImageWithURL:options:progress:completed:方法去下载。
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}
//判断key是否为空
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// 首先检查内存缓存
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
//通过key检查磁盘缓存
@autoreleasepool {
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);
});
}
});
return operation;
}
3. SDWebImageDownloader进行图片下载
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self)wself = self;
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (wself.headersFilter) {
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = wself.HTTPHeaders;
}
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
dispatch_async(dispatch_get_main_queue(), ^{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
});
}
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
if (finished) {
[sself.URLCallbacks removeObjectForKey:url];
}
});
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
cancelled:^{
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
operation.shouldDecompressImages = wself.shouldDecompressImages;
if (wself.urlCredential) {
operation.credential = wself.urlCredential;
} else if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];
return operation;
}
在downloadImageWithURL:options:progress:completed:方法中会将传入的progressBlock和completedBlock保存起来,并在第一次下载该URL的图片时,创建一个NSMutableURLRequest对象和一个SDWebImageDownloaderOperation对象,并将该对象添加到SDWebImageDownloader的downloadQueue来启动异步下载任务。
SDWebImageDownloaderOperation中封装了一个NSURLConnection的网络请求,并通过runloop来保持NSURLConnection在start后、收到响应前不被干掉,下载图片时,监听NSURLConnection回调的connection:didReceiveData:方法中会负责progress相关的处理和回调,connectionDidFinishLoading:方法中会负责将data转为image,以及图片解码操作,并最终回调completedBlock。
4.图片下载完成后的回调
SDWebImageDownloaderOperation 中的图片下载请求完成后,会回调给 SDWebImageDownloader,然后 SDWebImageDownloader 再回调给 SDWebImageManager,SDWebImageManager 中再将图片分别缓存到内存和磁盘上(可选),并回调给 UIImageView,UIImageView 中再回到主线程设置 image 属性。至此,图片的下载和缓存操作就圆满结束了。
参考:完整项目资料下载
https://github.com/Haochenchen/OCDemo