SDWebImage源码解读《一》

栏目: IOS · 发布时间: 7年前

内容简介:关于SDWebImage的文章网上已经非常多了,今天写SD相关的一方面算是对优秀的开源框架代码学习,另一方面总结一下框架内优秀的思想,知识的积累本身也是在于总结。本篇博客着重分析一下这几个类的部分实现:上面是对

关于SDWebImage的文章网上已经非常多了,今天写SD相关的一方面算是对优秀的开源框架代码学习,另一方面总结一下框架内优秀的思想,知识的积累本身也是在于总结。本篇博客着重分析一下这几个类的部分实现:

SDWebImageManager
SDImageCache
SDWebImageDownloader
总结

一、SDWebImageManager

SDWebImageManagerSDWebImage 的核心类,管理着 SDWebImageDownloaderSDImageCache , 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. @autoreleasepoolSD 使用自动释放池对内存进行了优化, diskImage 对象实际上如果图片比较大确实会占用很大内存开销,而且 [self diskImageForKey:key] 返回的 image 对象实际为 autorelease 自动释放,这样也导致了此对象只能在下一次事件循环中再外层的 autoreleasepool 中释放,让这段时间内存增长,影响性能。

三、SDWebImageDownloader

如果本地没有这张图片那么就会进入到 imageDownloaderimageDownloader 为下载器对象,处理下载图片的逻辑,那么 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); ,也就是说在一个并发队列上会将 queuebarrier 前面添加的任务 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.对于不同地方下载同一资源的情况,可以尝试使用 SDblock 回调存储以及回调时机的策略,保证资源只有一个在下载,而不同调用的地方都能得到回调,进度回调或者完成回调及失败回调等等。


以上所述就是小编给大家介绍的《SDWebImage源码解读《一》》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Programming Ruby

Programming Ruby

Dave Thomas、Chad Fowler、Andy Hunt / Pragmatic Bookshelf / 2004-10-8 / USD 44.95

Ruby is an increasingly popular, fully object-oriented dynamic programming language, hailed by many practitioners as the finest and most useful language available today. When Ruby first burst onto the......一起来看看 《Programming Ruby》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具