内容简介:关于SDWebImage的文章网上已经非常多了,今天写SD相关的一方面算是对优秀的开源框架代码学习,另一方面总结一下框架内优秀的思想,知识的积累本身也是在于总结。本篇博客着重分析一下这几个类的部分实现:上面是对
关于SDWebImage的文章网上已经非常多了,今天写SD相关的一方面算是对优秀的开源框架代码学习,另一方面总结一下框架内优秀的思想,知识的积累本身也是在于总结。本篇博客着重分析一下这几个类的部分实现:
SDWebImageManager SDImageCache SDWebImageDownloader 总结
一、SDWebImageManager
SDWebImageManager
是 SDWebImage
的核心类,管理着 SDWebImageDownloader
和 SDImageCache
, SDWebImageDownloader
为图片下载器对象,里面主要管理着 SDWebImageDownloaderOperation
进行对图片的下载, SDImageCache
主要是处理图片缓存相关,先分析一下 SDWebImageManage
看看它都做了什么:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock { //封装下载操作对象 __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; __weak SDWebImageCombinedOperation *weakOperation = operation; BOOL isFailedUrl = NO; //防止多线程访问出错,加互斥锁对self.failedURLs进行保护 @synchronized (self.failedURLs) { isFailedUrl = [self.failedURLs containsObject:url]; } if (url.absoluteString.length == 0 || (!(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]; } NSString *key = [self cacheKeyForURL:url]; //根据key查找缓存对象 operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) { //是不是被取消了 if (operation.isCancelled) { @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } return; } ... //省略了缓存策略相关的代码,到这里是真正的调用了imageDownloader下载图片,imageDownloader的内部实现一会说。 id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, 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) { dispatch_main_sync_safe(^{ if (strongOperation && !strongOperation.isCancelled) { completedBlock(nil, error, SDImageCacheTypeNone, finished, url); } }); if ( error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut && error.code != NSURLErrorInternationalRoamingOff && error.code != NSURLErrorDataNotAllowed && error.code != NSURLErrorCannotFindHost && error.code != NSURLErrorCannotConnectToHost) { //下载失败则添加图片url到failedURLs集合 @synchronized (self.failedURLs) { [self.failedURLs addObject:url]; } } } else { //虽然下载失败,但是如果设置了可以重新下载失败的url则remove该url if ((options & SDWebImageRetryFailed)) { @synchronized (self.failedURLs) { [self.failedURLs removeObject:url]; } } //是否需要缓存在磁盘 BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly); if (options & SDWebImageRefreshCached && image && !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:)]) { ... } else { //下载完成且有image则缓存图片 if (downloadedImage && finished) { [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; } dispatch_main_sync_safe(^{ if (strongOperation && !strongOperation.isCancelled) { completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url); } }); } } if (finished) { @synchronized (self.runningOperations) { if (strongOperation) { [self.runningOperations removeObject:strongOperation]; } } } }]; operation.cancelBlock = ^{ [subOperation cancel]; @synchronized (self.runningOperations) { __strong __typeof(weakOperation) strongOperation = weakOperation; if (strongOperation) { [self.runningOperations removeObject:strongOperation]; } } }; } else if (image) { // 有图片且线程没有被取消,则返回有图片的completedBlock dispatch_main_sync_safe(^{ __strong __typeof(weakOperation) strongOperation = weakOperation; if (strongOperation && !strongOperation.isCancelled) { completedBlock(image, nil, cacheType, YES, url); } }); @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } } else { //没有在缓存中并且代理方法也不允许下载则回调失败 dispatch_main_sync_safe(^{ __strong __typeof(weakOperation) strongOperation = weakOperation; if (strongOperation && !weakOperation.isCancelled) { completedBlock(nil, nil, SDImageCacheTypeNone, YES, url); } }); @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } } }]; return operation; } 复制代码
二、SDImageCache
上面是对 SDWebImageManager
源码做了简要分析,我想以这里为入口着重分析一下:
首先SD先调用 queryDiskCacheForKey:done:
去内存中查看是否有我们要的图片,那么这里面做了什么呢:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock { ... //内存中查找,SD内存缓存使用NSCache实现。 UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { doneBlock(image, SDImageCacheTypeMemory); return nil; } NSOperation *operation = [NSOperation new]; //开辟子线程,将block中的任务放入到ioQueue中执行,目的是为了防止io操作阻塞主线程,可以看到ioQueue实际上_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);,SD使用GCD异步串行来实现io操作, 既保证了UI不被阻塞,有能保证block中的代码串行执行,防止多线程访问造成数据出错。 //执行磁盘io操作 dispatch_async(self.ioQueue, ^{ if (operation.isCancelled) { return; } @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; } 复制代码
1. self.ioQueue
实际上为 _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
, SD
使用 GCD
异步串行来实现 io
操作,既保证了 UI
不被阻塞,又能保证 block
中的代码串行执行,实际上包括后面的磁盘写入操作都是放在这个 ioQueue
中执行的,主要目的就是防止多线程访问造成数据竞争导致数据出错。
2. @autoreleasepool
, SD
使用自动释放池对内存进行了优化, diskImage
对象实际上如果图片比较大确实会占用很大内存开销,而且 [self diskImageForKey:key]
返回的 image
对象实际为 autorelease
自动释放,这样也导致了此对象只能在下一次事件循环中再外层的 autoreleasepool
中释放,让这段时间内存增长,影响性能。
三、SDWebImageDownloader
如果本地没有这张图片那么就会进入到 imageDownloader
, imageDownloader
为下载器对象,处理下载图片的逻辑,那么 imageDownloader
中实现了什么我们还是看代码:
- (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:^{ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval]; ... //在这里创建operation对象 operation = [[wself.operationClass alloc] initWithRequest:request inSession:self.session 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; } 复制代码
这里面我着重讲一下 addProgressCallback:completedBlock:forURL:createCallback:
的实现,看看它里面都做了什么事情:
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback { ... //省略了一部分源码不影响阅读 //栅栏块加GCD锁 dispatch_barrier_sync(self.barrierQueue, ^{ BOOL first = NO; //URLCallbacks 实际上存储的是所有图片下载的回调的可变字典 url为我们请求图片的地址,以url为key,value为可变数组。那么可变数组中存储的是什么呢,往下看。 if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; } //取出当前url对应的可变数组 NSMutableArray *callbacksForURL = self.URLCallbacks[url]; //创建可变字典callbacks,callbacks实际上存储的是本次下载的进度和完成回调block。 NSMutableDictionary *callbacks = [NSMutableDictionary new]; if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; //将此可变字典添加至刚才我们创建的可变数组callbacksForURL中。 [callbacksForURL addObject:callbacks]; self.URLCallbacks[url] = callbacksForURL; //如果是第一次下载,也就是URLCallbacks存储所有下载回调的字典中没有当前的,那么认为是第一次下载,执行createCallback()去下载图片,否则什么也不做。 if (first) { createCallback(); } }); } 复制代码
1. dispatch_barrier_sync
栅栏块,顾名思义,是做了个拦截,它会将队列中在它之前的任务执行完毕才会执行它后面的任务,可以理解为一个分界线,而 barrierQueue
实际上为 _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
,也就是说在一个并发队列上会将 queue
中 barrier
前面添加的任务 block
全部执行后,再执行 barrier
任务的 block
,再执行 barrier
后面添加的任务 block
,这样一来相当于对 block
中的内容加了层锁,保证线程安全。
2. URLCallbacks
是个可变字典,存储着所有调用了下载的 block
回调,如果是第一次下载那么就执行下载,如果不是第一次那么就将其回调保存在 URLCallbacks
里面,什么也不做。我以下载进度为例,探究一下 URLCallbacks
要干什么。在它的回调里面是这样实现的:
//弱引用 SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; //这里是我们上面说的栅栏块block,做线程保护 dispatch_sync(sself.barrierQueue, ^{ //此url所对应的所有下载回调value,数组类型,存储的是ui部分对此url所有下载的回调。 callbacksForURL = [sself.URLCallbacks[url] copy]; }); //遍历这些回调 for (NSDictionary *callbacks in callbacksForURL) { //回到主线程,为每一个回调block返回当前图片的下载进度 dispatch_async(dispatch_get_main_queue(), ^{ SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; if (callback) callback(receivedSize, expectedSize); }); 复制代码
四、总结:
1. GCD
的使用,多线程加锁防止资源竞争以及 barrier
栅栏块的使用。
2.如果 for
循环中或者获取的资源内存开销较大可以尝试使用 @autoreleasepool
进行内存优化。
3.对于不同地方下载同一资源的情况,可以尝试使用 SD
的 block
回调存储以及回调时机的策略,保证资源只有一个在下载,而不同调用的地方都能得到回调,进度回调或者完成回调及失败回调等等。
以上所述就是小编给大家介绍的《SDWebImage源码解读《一》》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Phoenix解读 | Phoenix源码解读之索引
- Phoenix解读 | Phoenix源码解读之SQL
- Redux 源码解读 —— 从源码开始学 Redux
- AQS源码详细解读
- MJExtension源码解读
- axios 核心源码解读
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。