最近项目进度缓慢了下来,决定看看各种源码来涨点知识。就先从SDWebImage开始吧!
在项目中用的最多的方法应该是UIImageView+WebCache与UIButton+WebCache里面的sd_setImageWithURL:这个系列的方法了。这里从UIImageView+WebCache开始看起。
其中该系列所有方法都基于:
- (void)sd_setImageWithPreviousCachedImageWithURL:(NSURL*)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
参数很简单明了,url是需要加载的图片url;placeholder是在url指向的图片加载完成之前暂时使用的图片;options是个枚举,用来控制加载图片时的一些设置(具体百度即可,大把);progressBlock则可以通过返回的receivedSize与expectedSize来花式展现下载进度;completedBlock则是下载完成后的Block;
下面来看作者的具体实现:
[self sd_cancelCurrentImageLoad];
按照意思来看是取消当前所有的图片加载,进入方法内部看:
- (void)sd_cancelImageLoadOperationWithKey:(NSString*)key {
// Cancel in progress downloader from queue
NSMutableDictionary*operationDictionary = [selfoperationDictionary];
idoperations = [operationDictionaryobjectForKey:key];
if(operations) {
if([operationsisKindOfClass:[NSArrayclass]]) {
for(id operationinoperations) {
if(operation) {
[operationcancel];
}
}
}elseif([operationsconformsToProtocol:@protocol(SDWebImageOperation)]){
[(id) operationscancel];
}
[operationDictionaryremoveObjectForKey:key];
}
}
首先取出一个operationDictionary,在取出这个字典的过程中,作者在分类中使用了objc_setAssociatedObject与objc_getAssociatedObject,来给分类添加属性。
接着通过传过来的key(@"UIImageViewImageLoad")来获取ImageView的加载队列。
最后,花式cancle掉这个队列中的任务。
[operation cancel];
[operation DictionaryremoveObjectForKey:key];
回到主方法,第二行又通过objc_setAssociatedObject方法将url关联到分类中,接着通过位与运算判断下option是否为SDWebImageDelayPlaceholder,来设置默认的占位图片。其中dispatch_main_async_safe这个宏很好用,避免了在主线程中造成死锁的情况。
然后是判断一下是否需要转动菊花:
if([selfshowActivityIndicatorView]) {
[selfaddActivityIndicator];
}
接下来是这个方法的核心部分,调用了SDWebImageManager中的
- (id)downloadImageWithURL:(NSURL*)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
由此方法完成图片的下载。
我们可以跳到SDWebImageManager.h中查看一下作者对于该方法的描述:
翻译过来的意思大概是:如果URL指定的图片不在缓存中就下载该图片,否则就返回缓存的版本。
回到这个方法的实现,前几行是判断URL的类型是否正确。
BOOLisFailedUrl =NO;
@synchronized(self.failedURLs) {
isFailedUrl = [self.failedURLscontainsObject:url];
}
这几句来获取传入的URL是否为之前下载失败过的URL,用一个BOOL值来记录下来。
如果URL不为空或者未设置options为SDWebImageRetryFailed项、且URL在黑名单之中,就会直接返回掉。
@synchronized(self.runningOperations) {
[self.runningOperationsaddObject:operation];
}
这段代码是先给运行中的下载队列加锁,避免多个线程同时对数组进行操作,将一个SDWebImageCombinedOperation对象加入到下载队列中。
NSString*key = [selfcacheKeyForURL:url];
operation.cacheOperation= [self.imageCache queryDiskCacheForKey:key done:^(UIImage*image,SDImageCacheTypecacheType) {
if(operation.isCancelled) {
@synchronized(self.runningOperations) {
[self.runningOperationsremoveObject:operation];
}
return;
}
将图片的URL当做key值,再调用 queryDiskCacheForKey:done:来获取缓存中的图片。
我们来看看这个方法的内部实现:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
//这里封装了NSChace的objectForKey方法,直接从内存缓存中获取图片对象
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
//如果内存缓存中获取到则直接返回
NSOperation *operation = [NSOperation new];
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);
//在主线程中回调Block
});
}
});
return operation;
}
缓存这里获取完成之后,来看下面的代码,有点长,我们分解开来一部分一部分阅读,先看这个判断:
if ((!image || options & SDWebImageRefreshCached)
//图片未从缓存中获取,或者是 option设置需要刷新缓存
&& (![self.delegate respondsToSelector:
@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]))
/*这部分条件中,如果imageManager:shouldDownloadImageForURL:方法未实现、或是实现了并且返回YES。可以从方法的名字中来理解,代理方法返回的BOOL为是否应该下载URL对应的图片。
*/
总而言之就是判断各种条件之下,图片是否应该被下载,让我们进入方法内部。
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
// If image was found in the cache bug SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
//翻译一下,如果图片在缓存中被找到但是options设置了SDWebImageRefreshCached(刷新缓存),通知这个缓存图片,并且试图从新下载这个图片,让服务端有机会刷新这个缓存。
completedBlock(image, nil, cacheType, YES, url);
});
}
SDWebImageDownloaderOptions downloaderOptions = 0;
//初始化downloaderOptions
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
//如果options为低优先级,则设置downloaderOptions 为SDWebImageDownloaderLowPriority,下面的同理。
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (image && options & SDWebImageRefreshCached) {
// force progressive off if image already cached but forced refreshing
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
//翻译一哈,两行代码的意思是,如果图片存在缓存并且需要刷新缓存,则强制取消掉SDWebImageDownloaderProgressiveDownload模式(渐进下载),
然后忽略从缓存中读取的图片。
}
接下来使用SDWebImageDownloader来执行一个下载任务
id <SDWebImageOperation> subOperation =
[self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:
^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished)
来看下载completedBlock中的第一部分处理
if (weakOperation.isCancelled) {
/*这里什么都没做操作,源代码中提及了#699号更新,于是我去看了下,大概意思是说:
当weakOperation取消的时候不要试图去调用completion block,dispatch_main_sync_safe()也无法保证这个block被终止的时候没有其他的代码在运行,所以其他代码运行时可能会被截断。
比如说,如果取消weakOperation后再调用completion block,那么在随后的一个TableViewCell中加载Image的completion block将会和这个completion block产生竞争关系。说的通俗一点就是,先调用的completion block里面的数据可能会被第二个completion block的数据覆盖掉。
*/
}
else if (error) {
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
}
//有错误信息,完成回调。
});
if (error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
//将发送错误的URL添加到黑名单里面
}
}
}
else {
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
//options设置为SDWebImageRefreshCached选项,在缓存中又找到了image且没有下载成功
// Image refresh hit the NSURLCache cache, do not call the completion block
//图片刷新时遇到了具有缓存的情况,不调用 completion block
}
else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage))
&& [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
//图片下载成功且图片设置为SDWebImageTransformAnimatedImage并且实现了imageManager:transformDownloadedImage:withURL:方法
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
//调用delegate方法完成图片的变形
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:data forKey:key toDisk:cacheOnDisk];
//将变形后的图片缓存起来
}
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
//在主线程中回调completedBlock
}
});
});
}
else {
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
//如果没设置图片变形,并且下载完成,则直接缓存图片
}
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
//在主线程中完成回调
}
});
}
}
if (finished) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
//从下载队列移除
}
}
}];
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:weakOperation];
}
};
//设置operation取消之后的一些操作
else if (image) {
else if (image) {
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
//在缓存中找到图片并且设置了不能下载的选项,完成回调
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
else {
//在缓存中没有找到图片,并且设置不能下载的选项
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
}
//完成回调
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
}];
return operation;
这么一大段的方法按照功能排序来看,分解为首先创建下载operation,再读取系统的内存缓存与磁盘缓存,接着判断是否需要下载来进行下载操作,最后对下载的图片进行处理。