优雅的SDWebImage

简述

Asynchronous image downloader with cache support as a UIImageView category.
上面的是关于SDWebImage的官方说明, 可以翻译为: 一个支持缓存的, 采用UIImageView分类形式的图片下载器.
共包含以下功能:

  1. 以UIImageView的分类, 来支持网络图片的加载和缓存管理
  2. 一个异步的图片加载器
  3. 一个异步的内存+磁盘图片缓存
  4. 支持GIF
  5. 支持WebP
  6. 后台图片解压缩处理
  7. 确保同一个URL的图片不被多次下载
  8. 确保虚假的URL不会被反复加载
  9. 确保下载及缓存时, 主线程不被阻塞
  10. 使用GCD和ARC
  11. 支持ARM64

源码分析

以UIImageView图片加载为例, 最常用的方法就是如下方法(类目 UIImageView + WebCache 里):
 - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder

内部调用以下方法:

 - (void)sd_setImageWithURL:(NSURL *)url
                  placeholderImage:(UIImage *)placeholder
                  options:(SDWebImageOptions)options
                  progress:(SDWebImageDownloaderProgressBlock)progressBlock
                  completed:(SDWebImageCompletionBlock)completedBlock

对这个方法进行如下结构解析:

取消当前图片加载

为防止同一个UIImageView对象同时加载多个image, 需首先取消当前UIImageView对象的图片加载, 如下:

[self sd_cancelCurrentImageLoad];

下载图片操作的存储和取消单独放在一个UIView的类目里, 这样的设计让代码更简洁, 耦合性更低. UIView+WebCacheOperation, 这个类目主要有三个方法,

  1. 添加下载操作
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
    [self sd_cancelImageLoadOperationWithKey:key];
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    [operationDictionary setObject:operation forKey:key];
}
  1. 取消下载操作
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
    // Cancel in progress downloader from queue
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    id operations = [operationDictionary objectForKey:key];
    if (operations) {
        if ([operations isKindOfClass:[NSArray class]]) {
            for (id  operation in operations) {
                if (operation) {
                    [operation cancel];
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id) operations cancel];
        }
        [operationDictionary removeObjectForKey:key];
    }
}
  1. 移除下载操作
- (void)sd_removeImageLoadOperationWithKey:(NSString *)key {
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    [operationDictionary removeObjectForKey:key];
}

上述代码普遍使用了关联对象,主要会用到以下两个方法:

  1. void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
    用来给对象添加成员关联对象
  2. id objc_getAssociatedObject(id object, void *key)
    通过key值获取对应的关联对象

一般来说, 关联对象在OC中主要解决如下这样的问题:
我们需要在类目中保存一些状态,可以使用成员变量的形式来实现,但是,这些状态又是相对独立的, 只在这个类目中会被用到。所以,我们想把这个成员变量隐藏在这个类目中,对于其他的类目,这个成员变量都不可见。
如果类目允许扩展成员变量的话,这个就很好解决, 直接在类目的实现文件里声明一个成员变量即可。这样既对外部隐藏这个成员变量,又能在这个类目中使用它。很完美的解决方案。但是不幸的是,OC不允许在类目中扩展成员变量。所以不得不在类的声明中添加需要的成员变量,而且还需要把它暴露出来,以使你的分类能够使用它。这样,随着类目越来越多,你不得不在类中声明越来越多原本需要对外隐藏的成员变量。
以上代码所用的关联对象就很好的解决了这个问题,不需要在声明中创建对外可见的成员变量operationDictionary,只用key: loadOperationKey 关联了字典operationDictionary,就可以用如下的方式来使用此对象。

NSMutableDictionary *operationDictionary = [self operationDictionary]; 

[operationDictionary setObject:operation forKey:key];

具体可参考雷纯峰的博客:http://blog.leichunfeng.com/blog/2015/06/26/objective-c-associated-objects-implementation-principle/

取消图片加载操作的内部逻辑是:
利用key: UIImageViewImageLoad 去操作字典中查找, 如果存在下载图片的操作则取消.
代理 SDWebImageOperation 只有一个 -cancel 方法, 目的是为服从此代理的所有 Operation 类都提供一个统一的方法名, 而不需要在 SDWebImageCombinedOperation 和 SDWebImageDownloaderOperation 的头文件中再单独声明取消方法. - sd_cancelImageLoadOperationWithKey 方法中的 operation属于 SDWebImageCombinedOperation 类, 它的取消方法如下:

- (void)cancel {
    self.cancelled = YES;
    if (self.cacheOperation) {
        [self.cacheOperation cancel];
        self.cacheOperation = nil;
    }
    if (self.cancelBlock) {
        self.cancelBlock();
        
        // TODO: this is a temporary fix to #809.
        // Until we can figure the exact cause of the crash, going with the ivar instead of the setter
//        self.cancelBlock = nil;
        _cancelBlock = nil;
    }
}

上面的方法包含两个取消操作

  1. [self.cacheOperation cancel], 这个是取消查找存储(缓存)图片操作
  2. self.cancelBlock(), 这个block 为如下代码:
operation.cancelBlock = ^{
                [subOperation cancel];
                
                @synchronized (self.runningOperations) {
                    __strong __typeof(weakOperation) strongOperation = weakOperation;
                    if (strongOperation) {
                        [self.runningOperations removeObject:strongOperation];
                    }
                }
            };

其中 subOperation 属于SDWebImageDownloaderOperation类, 为下载图片的操作类, 它将执行最终的取消下载方法, 如下:

- (void)cancel {
    @synchronized (self) {
        if (self.thread) {
            [self performSelector:@selector(cancelInternalAndStop) onThread:self.thread withObject:nil waitUntilDone:NO];
        }
        else {
            [self cancelInternal];
        }
    }
}

如上所示, 如果operations对象在单独的线程里, 需要单独处理线程, 取消下载后, 要停止当前线程的runloop, CFRunLoopStop(CFRunLoopGetCurrent()), 具体如下代码:

- (void)cancelInternalAndStop {
    if (self.isFinished) return;
    [self cancelInternal];
    CFRunLoopStop(CFRunLoopGetCurrent());
}

一系列的操作, 最终会通过苹果提供的NSURLConnection类的方法 cancel 来完成本次取消下载任务, 如下代码所示:

- (void)cancelInternal {
    if (self.isFinished) return;
    [super cancel];
    if (self.cancelBlock) self.cancelBlock();

    if (self.connection) {
        // 取消下载的操作最终会来到这里
        [self.connection cancel];
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
        });

        // As we cancelled the connection, its callback won't be called and thus won't
        // maintain the isFinished and isExecuting flags.
        if (self.isExecuting) self.executing = NO;
        if (!self.isFinished) self.finished = YES;
    }

    [self reset];
}

以上就是 [self sd_cancelCurrentImageLoad] 这个方法的逻辑步骤.

②. 然后通过runtime把图片的url地址和UIImageView对象设置依赖关系, 代码如下:

objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

接下来紧跟着两个判断语句:
③. 第一个是判断如果只有placeHolder, 那么就显示它.
④. 第二判断语句内部是真正的下载.下面来详细分析:
如果图片url不为空, 进入处理下载图片的逻辑:
1> 首先判断是否需要显示图片加载Indicator, 代码如下:

if ([self showActivityIndicatorView]) {
         [self addActivityIndicator];
}

这里也用到了关联对象的方法.
第一步:
使用key: TAG_ACTIVITY_SHOW 保存 传入的布尔值(转化为NSNumber类型保存), 调用共有方法:

- (void)setShowActivityIndicatorView:(BOOL)show{
    objc_setAssociatedObject(self, &TAG_ACTIVITY_SHOW, [NSNumber numberWithBool:show], OBJC_ASSOCIATION_RETAIN);
}

第二步:
通过key: TAG_ACTIVITY_SHOW 获取传入的是否显示图片加载的布尔值.

- (BOOL)showActivityIndicatorView{
    return [objc_getAssociatedObject(self, &TAG_ACTIVITY_SHOW) boolValue];
}

2> 通过单例SDWebImageManager,创建一个下载操作operation,这个操作operation不是真正的 NSOperation 类,而是一个服从代理 SDWebImageOperation 的继承 NSObject的类 SDWebImageCombinedOperation,这个类的作用是用来组合查找存储图片操作 cacheOperation 和 下载图片操作 subOperation(属于类SDWebImageDownloaderOperation) ,当需要取消这两个操作时,如上分析,只需要调用 operation (类SDWebImageCombinedOperation) 的 -cancel 方法。
通过 id operation 接收 SDWebImageManager 的下载操作,为了能够确保单个 UIImageView 同一时刻只有单个对应的图片被下载,需要保存这个 operation。这个时候你会想到什么类,没错,真是苹果提供的字典类。
把operation放进字典, 对应的key 为“UIImageViewImageLoad”。在下载图片前要在字典里通过key查找是否有正在进行的下载操作,如果有就取消。
图片下载就在这个operation的block里执行,并通过它向外输出下载图片最终的信息。在这里要插入一个runtime的关联对象的概念,在把 operation对象 放字典里这个过程中, 作者使用了runtime的关联对象的方法, 代码如下:

    - (NSMutableDictionary *)operationDictionary {
    NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
    if (operations) {
        return operations;
    }
    operations = [NSMutableDictionary dictionary];
    objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return operations;
}

SDWebImageManager的下载操作

  • 如下方法
- (id )downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock

内部逻辑如下

  • 判断URL格式如果是字符串类型,转换成URL类型
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }
  • 防止传入NSNull导致程序崩溃
if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
  • 确保虚假的URL不会被反复下载
BOOL isFailedUrl = NO;
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }
  • 这几种情况下执行是下载错误, 通过block携带错误信息并返回, 不执行之后的下载动作
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];
    }
  • 根据是否有过滤条件返回对应的key
NSString *key = [self cacheKeyForURL:url];
- (NSString *)cacheKeyForURL:(NSURL *)url {
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    }
    else {
        return [url absoluteString];
    }
}
  • 从缓存或磁盘中查找图片
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) {
        return nil;
    }

    if (!key) {
        // 如果key为nil, 则返回传空递信息回去
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // First check the in-memory cache...
    // 1. 首先查找缓存中是否有图片, 如果有则返回这个图片和图片存储类型
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }

    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }

        @autoreleasepool {
            // 2. 从磁盘中查找图片, 并且判断是否需要保存, 然后返回
            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;
}

上面代码使用@autoreleasepool {},目的是在doneBlock执行完毕后,立即销毁临时变量 diskImage,优化内存管理。

执行上述代码的 doneBlock ,

  • 如果操作被取消了,移除操作,并返回
if (operation.isCancelled) {
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }

            return;
        }
  • 如果从缓存或者文件中查找到图片并返回,执行如下逻辑
else if (image) {
            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 {
            // Image not in cache and download disallowed by delegate
            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];
            }
        }

接下来分析下载逻辑

  • 下载前的 判断语句
if ((!image || options & SDWebImageRefreshCached) 
&& 
(![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] 
|| [self.delegate imageManager:self shouldDownloadImageForURL:url]))

上述判断语句,一个和查找图片有关,一个和是否允许下载的代理有关,以下几种情况会进入下载逻辑:

num 可下载
1 无代理方法,且未找到图片
2 无代理方法,找到图片,但需更新存储
3 有代理方法,允许下载图片,且未找到图片
4 有代理方法,找到图片,但需更新存储
  • 找到图片,到需要更新存储,先走如下逻辑,然后再下载更新图片
if (image && options & SDWebImageRefreshCached) {
                dispatch_main_sync_safe(^{
                    // If image was found in the cache but 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.
                    completedBlock(image, nil, cacheType, YES, url);
                });
            }

options & SDWebImageRefreshCached 相当于 options
== SDWebImageRefreshCached

downloadImagewithURL:options:progress:completed:方法里面来处理的, 该方法调用了addProgressCallback:andCompletedBlock:forURL:createCallback方法来将请求的信息存入管理器中,同时在回调block中创建新的操作, 配置之后将其放入downloadQueue操作队列中, 最后方法返回新创作的操作. 其具体实现如下:


 - (id )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;
        }

        // 1. 创建请求对象, 并根据options参数设置其属性
        // 为了避免潜在的重复缓存(NSURLCache + SDImageCache), 如果没有明确告知需要缓存, 则禁用图片请求的缓存操作
        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;
        }
        
        // 2. 创建SDWebImageDownloaderOperation操作对象,并进行配置
        // 配置信息包括是否需要认证,优先级
        operation = [[wself.operationClass alloc] initWithRequest:request
                                                          options:options
                                                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                                             // 3. 从管理器的callbacksForURL中找出该URL所有的进度处理回调并调用
                                                             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) {
                                                             // 4. 从管理器的callbacksForURL中找出该URL所有的完成处理回调并调用
                                                             // 如果finished为YES, 则将该url对应的回调信息从URLCallbacks中删除
                                                            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:^{
                                                            // 5. 取消操作讲该url对应的回调信息从URLCallBacks中删除
                                                            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;
        }

        // 6. 将操作加入到操作队列downloadQueue中
        // 如果是LIFO顺序, 则将新的操作作为原队列中最后一个操作的依赖,然后将新操作设置为最后一个操作
        [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;
}

在上面的代码中创建完请求对象, 并且配置好请求信息之后, 真正的下载操作是在SDWebImageDownLoadeerOperation类中完成的, 本类的初始化方法传入了三个block, progress, completed, 和cancelled, 分别用来处理进度 下载完成以及取消操作.
本类使用了foundation框架提供的NURLConnection类而不是7.0之后推出的NSURLSession类来执行请求的. 本类是NSOperation的子类, 执行请求是通过触发本类的start方法.

最近通过阅读这个框架的源码, 感受最深的是设计之巧妙, 我用伪代码用自己的方式试着实现一遍图片下载, 然后再和这个框架的设计方法进行比较, 发现真是有天壤之别.

你可能感兴趣的:(优雅的SDWebImage)