IOS框架:SDWeblmage(下)
原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- 一、简介
- 1、设计目的
- 2、特性
- 3、常见问题
- 4、使用方法
- 二、实现原理
- 1、架构图(UML 类图)
- 2、流程图(方法调用顺序图)
- 3、目录结构
- 4、核心逻辑
- 三、反思和拓展
- 四、实现细节
- 1、SDWebImageDownloader
- 2、SDWebImageDownloaderOperation
- 3、SDImageCache
- 4、SDWebImageManager
- 5、UIImageView+WebCache
- Demo
- 参考文献
四、实现细节
SDWebImage 最核心的功能
- 下载(
SDWebImageDownloader
) - 缓存(
SDImageCache
) - 将缓存和下载的功能组合起来(
SDWebImageManager
) - 封装成
UIImageView
等类的分类方法(UIImageView+WebCache
等)
1、SDWebImageDownloader
SDWebImageDownloader
继承于 NSObject
,主要承担了异步下载图片和优化图片加载的任务。
a、问题
- 如何实现异步下载,也就是多张图片同时下载?
- 如何处理同一张图片(同一个 URL)多次下载的情况?
b、源码
枚举定义
// 下载选项
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
SDWebImageDownloaderLowPriority = 1 << 0,
SDWebImageDownloaderProgressiveDownload = 1 << 1,
SDWebImageDownloaderUseNSURLCache = 1 << 2,
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
SDWebImageDownloaderContinueInBackground = 1 << 4,
SDWebImageDownloaderHandleCookies = 1 << 5,
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
SDWebImageDownloaderHighPriority = 1 << 7,
};
// 下载任务执行顺序
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
SDWebImageDownloaderFIFOExecutionOrder, // 先进先出
SDWebImageDownloaderLIFOExecutionOrder // 后进先出
};
SDWebImageDownloader.h
文件中的属性
@property (assign, nonatomic) BOOL shouldDecompressImages; // 下载完成后是否需要解压缩图片,默认为 YES
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;
@property (readonly, nonatomic) NSUInteger currentDownloadCount;
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;
@property (strong, nonatomic) NSString *username;
@property (strong, nonatomic) NSString *password;
@property (nonatomic, copy) SDWebImageDownloaderHeadersFilterBlock headersFilter;
SDWebImageDownloader.m
文件中的属性
@property (strong, nonatomic) NSOperationQueue *downloadQueue; // 图片下载任务是放在这个 NSOperationQueue 任务队列中来管理的
@property (weak, nonatomic) NSOperation *lastAddedOperation;
@property (assign, nonatomic) Class operationClass;
@property (strong, nonatomic) NSMutableDictionary *HTTPHeaders;
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;
@property (strong, nonatomic) NSMutableDictionary *URLCallbacks; // 图片下载的回调 block 都是存储在这个属性中,该属性是一个字典,key 是图片的 URL,value 是一个数组,包含每个图片的多组回调信息。
SDWebImageDownloader.h
文件中方法
+ (SDWebImageDownloader *)sharedDownloader;
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
- (NSString *)valueForHTTPHeaderField:(NSString *)field;
- (void)setOperationClass:(Class)operationClass; // 创建 operation
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
- (void)setSuspended:(BOOL)suspended;
SDWebImageDownloader.m
文件中的方法
// Lifecycle
+ (void)initialize;
+ (SDWebImageDownloader *)sharedDownloader;
- init;
- (void)dealloc;
// Setter and getter
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
- (NSString *)valueForHTTPHeaderField:(NSString *)field;
- (void)setMaxConcurrentDownloads:(NSInteger)maxConcurrentDownloads;
- (NSUInteger)currentDownloadCount;
- (NSInteger)maxConcurrentDownloads;
- (void)setOperationClass:(Class)operationClass;
// Download
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(NSURL *)url
createCallback:(SDWebImageNoParamsBlock)createCallback;
// Download queue
- (void)setSuspended:(BOOL)suspended;
c、实现思路
先看看 +initialize
方法,这个方法中主要是通过注册通知让SDNetworkActivityIndicator
监听下载事件,来显示和隐藏状态栏上的 network activity indicator
。为了让 SDNetworkActivityIndicator
文件可以不用导入项目中来(如果不要的话),这里使用了 runtime
的方式来实现动态创建类以及调用方法。
+ (void)initialize {
if (NSClassFromString(@"SDNetworkActivityIndicator")) {
id activityIndicator = [NSClassFromString(@"SDNetworkActivityIndicator") performSelector:NSSelectorFromString(@"sharedActivityIndicator")];
// Remove observer in case it was previously added.
// 先移除通知观察者 SDNetworkActivityIndicator
[[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStartNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStopNotification object:nil];
// 再添加通知观察者 SDNetworkActivityIndicator
[[NSNotificationCenter defaultCenter] addObserver:activityIndicator
selector:NSSelectorFromString(@"startActivity")
name:SDWebImageDownloadStartNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:activityIndicator
selector:NSSelectorFromString(@"stopActivity")
name:SDWebImageDownloadStopNotification object:nil];
}
}
+sharedDownloader
方法中调用了 -init
方法来创建一个单例。
+ (SDWebImageDownloader *)sharedDownloader {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
-init
方法中做了一些初始化设置和默认值设置,包括设置最大并发数(6)、下载超时时长(15s)等。
- (id)init {
if ((self = [super init])) {
_operationClass = [SDWebImageDownloaderOperation class];
_shouldDecompressImages = YES;
// 设置下载 operation 的默认执行顺序(先进先出还是先进后出)
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
// 初始化 _downloadQueue(下载队列)
_downloadQueue = [NSOperationQueue new];
// 初始化 _barrierQueue(GCD 队列)最大并发数(6)
_downloadQueue.maxConcurrentOperationCount = 6;
// 初始化 _URLCallbacks(下载回调 block 的容器)
_URLCallbacks = [NSMutableDictionary new];
// 设置 _HTTPHeaders 默认值
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
// 设置默认下载超时时长 15s
_downloadTimeout = 15.0;
}
return self;
}
这个类中最核心的方法就是 - downloadImageWithURL: options: progress: completed:
方法,这个方法中首先通过调用-addProgressCallback: andCompletedBlock: forURL: createCallback:
方法来保存每个url
对应的回调block
。
- (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 andCompletedBlock:completedBlock forURL:url createCallback:^{
......
-addProgressCallback: ...
方法先进行错误检查,判断 URL
是否为空,然后再将 URL
对应的 progressBlock
和 completedBlock
保存到 URLCallbacks
属性中去。
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
// 判断 url 是否为 nil,如果为 nil 则直接回调 completedBlock,返回失败的结果,然后 return
// 因为 url 会作为存储 callbacks 的 key
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
// MARK: 使用 dispatch_barrier_sync 函数来保证同一时间只有一个线程能对 URLCallbacks 进行操作
dispatch_barrier_sync(self.barrierQueue, ^{
// 从属性 URLCallbacks(一个字典) 中取出对应 url 的 callBacksForURL
// 这是一个数组,因为可能一个 url 不止在一个地方下载
BOOL first = NO;
if (!self.URLCallbacks[url]) {
// 如果没有取到,也就意味着这个 url 是第一次下载
// 那就初始化一个 callBacksForURL 放到属性 URLCallbacks 中
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}
// 处理同一个 URL 的多次下载请求
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
// 往数组 callBacksForURL 中添加 包装有 callbacks(progressBlock 和 completedBlock)的字典
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
// 更新 URLCallbacks 存储的对应 url 的 callBacksForURL
self.URLCallbacks[url] = callbacksForURL;
// 如果这个 url 是第一次请求下载,就回调 createCallback
if (first) {
createCallback();
}
});
}
URLCallbacks
属性是一个 NSMutableDictionary
对象,key
是图片的 URL
,value
是一个数组,包含每个图片的多组回调信息 。用 JSON
格式表示的话,就是下面这种形式:
{
"callbacksForUrl1": [
{
"kProgressCallbackKey": "progressCallback1_1",
"kCompletedCallbackKey": "completedCallback1_1"
},
{
"kProgressCallbackKey": "progressCallback1_2",
"kCompletedCallbackKey": "completedCallback1_2"
}
],
"callbacksForUrl2": [
{
"kProgressCallbackKey": "progressCallback2_1",
"kCompletedCallbackKey": "completedCallback2_1"
},
{
"kProgressCallbackKey": "progressCallback2_2",
"kCompletedCallbackKey": "completedCallback2_2"
}
]
}
这里有个细节需要注意,因为可能同时下载多张图片,所以就可能出现多个线程同时访问 URLCallbacks
属性的情况。为了保证线程安全,所以这里使用了 dispatch_barrier_sync
来分步执行添加到 barrierQueue
中的任务,这样就能保证同一时间只有一个线程能对URLCallbacks
进行操作。
如果这个URL
是第一次被下载,就要回调 createCallback
,createCallback
主要做的就是创建并开启下载任务,下面是 createCallback
的回调处理
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self)wself = self;
// 1. 把入参 url、progressBlock 和 completedBlock 传进该方法,并在第一次下载该 URL 时回调 createCallback
[self addProgressCallback:progressBlock andCompletedBlock: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
// 1.1 创建下载 request ,设置 request 的 cachePolicy、HTTPShouldHandleCookies、HTTPShouldUsePipelining
// 以及 allHTTPHeaderFields(这个属性交由外面处理,设计的比较巧妙)
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;
}
// 1.2 创建 SDWebImageDownloaderOperation(继承自 NSOperation)
operation = [[wself.operationClass alloc] initWithRequest:request options:options progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// 1.2.1 SDWebImageDownloaderOperation 的 progressBlock 回调处理
// 这个 block 有两个回调参数:接收到的数据大小和预计数据大小
// 这里用了 weak-strong dance,首先使用 strongSelf 强引用 weakSelf,目的是为了保住 self 不被释放
SDWebImageDownloader *sself = wself;
// 然后检查 self 是否已经被释放(这里为什么先“保活”后“判空”呢?因为如果先判空的话,有可能判空后 self 就被释放了)
if (!sself) return;
// 取出 url 对应的回调 block 数组(这里取的时候有些讲究,考虑了多线程问题,而且取的是 copy 的内容)
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
// 遍历数组,从每个元素(字典)中取出 progressBlock 进行回调
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
}
} completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
// 1.2.2 SDWebImageDownloaderOperation 的 completedBlock 回调处理
// 这个 block 有四个回调参数:图片 UIImage,图片数据 NSData,错误 NSError,是否结束 isFinished
// 同样,这里也用了 weak-strong dance
SDWebImageDownloader *sself = wself;
if (!sself) return;
// 接着,取出 url 对应的回调 block 数组
__block NSArray *callbacksForURL;
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
// 如果结束了(isFinished),就移除 url 对应的回调 block 数组(移除的时候也要考虑多线程问题)
if (finished) {
[sself.URLCallbacks removeObjectForKey:url];
}
});
// 遍历数组,从每个元素(字典)中取出 completedBlock 进行回调
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
} cancelled:^{
// SDWebImageDownloaderOperation 的 cancelBlock 回调处理
// 同样,这里也用了 weak-strong dance
SDWebImageDownloader *sself = wself;
if (!sself) return;
// 然后移除 url 对应的所有回调 block
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
// 1.3 设置下载完成后是否需要解压缩
operation.shouldDecompressImages = wself.shouldDecompressImages;
// 1.4 如果设置了 username 和 password,就给 operation 的下载请求设置一个 NSURLCredential
if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
// 1.5 设置 operation 的队列优先级
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// 1.6 将 operation 加入到队列 downloadQueue 中,队列(NSOperationQueue)会自动管理 operation 的执行
[wself.downloadQueue addOperation:operation];
// 1.7 如果 operation 执行顺序是先进后出,就设置 operation 依赖关系(先加入的依赖于后加入的),并记录最后一个 operation(lastAddedOperation)
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;
}
}];
// 2. 返回 createCallback 中创建的 operation(SDWebImageDownloaderOperation)
return operation;
}
createCallback
方法中调用了- [SDWebImageDownloaderOperation initWithRequest: options: progress:]
方法来创建下载任务 SDWebImageDownloaderOperation
。那么,这个 SDWebImageDownloaderOperation
类究竟是干什么的呢?下一节再看。
d、知识点
SDWebImageDownloaderOptions 枚举使用了位运算:通过“与”运算符,可以判断是否设置了某个枚举选项,因为每个枚举选择项中只有一位是1,其余位都是 0,所以只有参与运算的另一个二进制值在同样的位置上也为 1,与 运算的结果才不会为 0.
0101 (相当于 SDWebImageDownloaderLowPriority | SDWebImageDownloaderUseNSURLCache)
& 0100 (= 1 << 2,也就是 SDWebImageDownloaderUseNSURLCache)
= 0100 (> 0,也就意味着 option 参数中设置了 SDWebImageDownloaderUseNSURLCache)
2、SDWebImageDownloaderOperation
a、问题
- 如何实现下载的网络请求?
- 如何管理整个图片下载的过程?
- 图片下载完成后需要做哪些处理?
b、用途
- 每张图片的下载都会发出一个异步的
HTTP
请求,这个请求就是由SDWebImageDownloaderOperation
管理的。 - 继承
NSOperation
,遵守SDWebImageOperation
、NSURLConnectionDataDelegate
协议 -
SDWebImageOperation
协议只定义了一个方法-cancel
,用来取消operation
。
c、源码
SDWebImageDownloaderOperation.h
文件中的属性:
@property (strong, nonatomic, readonly) NSURLRequest *request; // 用来给 operation 中的 connection 使用的请求
@property (assign, nonatomic) BOOL shouldDecompressImages; // 下载完成后是否需要解压缩
@property (nonatomic, assign) BOOL shouldUseCredentialStorage;
@property (nonatomic, strong) NSURLCredential *credential;
@property (assign, nonatomic, readonly) SDWebImageDownloaderOptions options;
@property (assign, nonatomic) NSInteger expectedSize;
@property (strong, nonatomic) NSURLResponse *response;
SDWebImageDownloaderOperation.m
文件中的属性:
@property (copy, nonatomic) SDWebImageDownloaderProgressBlock progressBlock;
@property (copy, nonatomic) SDWebImageDownloaderCompletedBlock completedBlock;
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
@property (assign, nonatomic, getter = isExecuting) BOOL executing; // 覆盖了 NSOperation 的 executing
@property (assign, nonatomic, getter = isFinished) BOOL finished; // 覆盖了 NSOperation 的 finished
@property (assign, nonatomic) NSInteger expectedSize;
@property (strong, nonatomic) NSMutableData *imageData;
@property (strong, nonatomic) NSURLConnection *connection;
@property (strong, atomic) NSThread *thread;
@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
// 成员变量
size_t width, height; // 图片宽高
UIImageOrientation orientation; // 图片方向
BOOL responseFromCached;
SDWebImageDownloaderOperation.h
文件中的方法:
- (id)initWithRequest:(NSURLRequest *)request
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock
cancelled:(SDWebImageNoParamsBlock)cancelBlock;
SDWebImageDownloaderOperation.m
文件中的方法:
// 覆盖了父类的属性,需要重新实现属性合成方法
@synthesize executing = _executing;
@synthesize finished = _finished;
// Initialization
- (id)initWithRequest:(NSURLRequest *)request
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock
cancelled:(SDWebImageNoParamsBlock)cancelBlock;
// Operation
- (void)start;
- (void)cancel;
- (void)cancelInternalAndStop;
- (void)cancelInternal;
- (void)done;
- (void)reset;
// Setter and getter
- (void)setFinished:(BOOL)finished;
- (void)setExecuting:(BOOL)executing;
- (BOOL)isConcurrent;
// NSURLConnectionDataDelegate 方法
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; // 下载过程中的 response 回调
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data; // 下载过程中 data 回调
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection; // 下载完成时回调
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error; // 下载失败时回调
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; // 在 connection 存储 cached response 到缓存中之前调用
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection __unused *)connection; // URL loader 是否应该使用 credential storage
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge; // connection 发送身份认证的请求之前被调用
// Helper
+ (UIImageOrientation)orientationFromPropertyValue:(NSInteger)value;
- (UIImage *)scaledImageForKey:(NSString *)key image:(UIImage *)image;
- (BOOL)shouldContinueWhenAppEntersBackground;
d、具体实现
首先来看看指定初始化方法 -initWithRequest:options:progress:completed:cancelled:
,这个方法是保存一些传入的参数,设置一些属性的初始默认值。
// 接受参数,设置属性
- (id)initWithRequest:(NSURLRequest *)request
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock
cancelled:(SDWebImageNoParamsBlock)cancelBlock {
if ((self = [super init])) {
// 设置属性_shouldUseCredentialStorage、_executing、_finished、_expectedSize、responseFromCached 的默认值/初始值
_request = request;
_shouldDecompressImages = YES;
_shouldUseCredentialStorage = YES;
_options = options;
_progressBlock = [progressBlock copy];
_completedBlock = [completedBlock copy];
_cancelBlock = [cancelBlock copy];
_executing = NO;
_finished = NO;
_expectedSize = 0;
responseFromCached = YES; // Initially wrong until `connection:willCacheResponse:` is called or not called
}
return self;
}
当创建的 SDWebImageDownloaderOperation
对象被加入到downloader
的downloadQueue
中时,该对象的-start
方法就会被自动调用。
-start
方法中首先创建了用来下载图片数据的 NSURLConnection
,然后开启 connection
,同时发出开始图片下载的 SDWebImageDownloadStartNotification
通知,为了防止非主线程的请求被 kill
掉,这里开启runloop
保活,直到请求返回。
- (void)start {
// 给 self 加锁
@synchronized (self) {
// 如果 self 被 cancell 掉的话
if (self.isCancelled) {
// finished 属性变为 YES
self.finished = YES;
// reset 下载数据和回调 block
[self reset];
// 然后直接 return
return;
}
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
// 如果允许程序退到后台后继续下载
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
// 就标记为允许后台执行,在后台任务过期的回调 block 中
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
// 首先来一个 weak-strong dance
__strong __typeof (wself) sself = wself;
if (sself) {
// 调用 cancel 方法(这个方法里面又做了一些处理,反正就是 cancel 掉当前的 operation)
[sself cancel];
// 调用 UIApplication 的 endBackgroundTask: 方法结束任务
[app endBackgroundTask:sself.backgroundTaskId];
// 记录结束后的 taskId
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
// 标记 executing 属性为 YES
self.executing = YES;
// 创建 connection,赋值给 connection 属性
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
// 获取 currentThread,赋值给 thread 属性
self.thread = [NSThread currentThread];
}
// 启动 connection
[self.connection start];
// 因为上面初始化 connection 时可能会失败,所以这里我们需要根据不同情况做处理
if (self.connection) {// A.如果 connection 不为 nil
if (self.progressBlock) {
// 回调 progressBlock(初始的 receivedSize 为 0,expectSize 为 -1)
self.progressBlock(0, NSURLResponseUnknownLength);
}
dispatch_async(dispatch_get_main_queue(), ^{
// 发出 SDWebImageDownloadStartNotification 通知(SDWebImageDownloader 会监听到)
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
// Make sure to run the runloop in our background thread so it can process downloaded data
// Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5
// not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466)
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
}
else {
// 开启 runloop
CFRunLoopRun();
}
// runloop 结束后继续往下执行(也就是 cancel 掉或者 NSURLConnection 请求完毕代理回调后调用了 CFRunLoopStop)
if (!self.isFinished) {
[self.connection cancel];
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
}
}
else {// B.如果 connection 为 nil
if (self.completedBlock) {
// 回调 completedBlock,返回 connection 初始化失败的错误信息
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
// 下载完成后,调用 endBackgroundTask: 标记后台任务结束
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
}
NSURLConnection
请求图片数据时,服务器返回的的结果是通过 NSURLConnectionDataDelegate
的代理方法回调的,其中最主要的是以下三个方法:
- connection:didReceiveResponse: // 下载过程中的 response 回调,调用一次
- connection:didReceiveData: // 下载过程中 data 回调,调用多次
- connectionDidFinishLoading: // 下载完成时回调,调用一次
前两个方法是在下载过程中回调的,第三个方法是在下载完成时回调的。
第一个方法 - connection:didReceiveResponse:
被调用:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// A. 返回 code 不是 304 Not Modified
if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) {
// 1. 获取 expectedSize
NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
self.expectedSize = expected;
if (self.progressBlock) {
// 回调 progressBlock
self.progressBlock(0, expected);
}
// 2. 初始化 imageData 属性
self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
self.response = response;
dispatch_async(dispatch_get_main_queue(), ^{
// 3. 发送 SDWebImageDownloadReceiveResponseNotification 通知
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
});
}
// B. 针对 304 Not Modified 做处理,直接 cancel operation,并返回缓存的 image
else {
NSUInteger code = [((NSHTTPURLResponse *)response) statusCode];
//This is the case when server returns '304 Not Modified'. It means that remote image is not changed.
//In case of 304 we need just cancel the operation and return cached image from the cache.
if (code == 304) {
// 1. 取消连接
[self cancelInternal];
} else {
[self.connection cancel];
}
dispatch_async(dispatch_get_main_queue(), ^{
// 2. 发送 SDWebImageDownloadStopNotification 通知
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});
if (self.completedBlock) {
// 3. 回调 completedBlock
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES);
}
// 4. 停止 runloop
CFRunLoopStop(CFRunLoopGetCurrent());
[self done];
}
}
接着会多次调用 - connection:didReceiveData:
方法来更新进度、拼接图片数据:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// 1.拼接图片数据
[self.imageData appendData:data];
// 2.针对 SDWebImageDownloaderProgressiveDownload 做的处理
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
// Get the total bytes downloaded
const NSInteger totalSize = self.imageData.length;
// 2.1 根据更新的 imageData 创建 CGImageSourceRef 对象
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
// 2.2 首次获取到数据时,读取图片属性:width, height, orientation
// 2.3 图片还没下载完,但不是第一次拿到数据,使用现有图片数据 CGImageSourceRef 创建 CGImageRef 对象
if (width + height > 0 && totalSize < self.expectedSize) {
// Create the image
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
if (partialImageRef) {
UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
// 2.4 对图片进行缩放
UIImage *scaledImage = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
// 对图片进行解码
image = [UIImage decodedImageWithImage:scaledImage];
}
else {
image = scaledImage;
}
CGImageRelease(partialImageRef);
dispatch_main_sync_safe(^{
if (self.completedBlock) {
// 回调 completedBlock
self.completedBlock(image, nil, nil, NO);
}
});
}
}
CFRelease(imageSource);
}
// 3.回调 progressBlock
if (self.progressBlock) {
self.progressBlock(self.imageData.length, self.expectedSize);
}
}
当图片数据全部下载完成时,- connectionDidFinishLoading:
方法就会被调用:
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
// 1. 下载结束
SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
@synchronized(self) {
// 停止 runloop
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
self.connection = nil;
dispatch_async(dispatch_get_main_queue(), ^{
// 发送 SDWebImageDownloadStopNotification 通知和 SDWebImageDownloadFinishNotification 通知
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
});
}
if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) {
responseFromCached = NO;
}
// 2. 回调 completionBlock
if (completionBlock) {
// 2.1 如果是返回的结果是 URL Cache,就回调图片数据为 nil 的 completionBlock
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) {
completionBlock(nil, nil, nil, YES);
} else if (self.imageData) {// 2.2 如果有图片数据
// 2.2.1 针对不同图片格式进行数据转换 data -> image
UIImage *image = [UIImage sd_imageWithData:self.imageData];
// 2.2.2 据图片名中是否带 @2x 和 @3x 来做 scale 处理
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
// 2.2.3 如果需要解码,就进行图片解码(如果不是 GIF 图)
if (!image.images) {
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
}
// 2.2.4 判断图片尺寸是否为空
if (CGSizeEqualToSize(image.size, CGSizeZero)) {
// 如果没有图片数据,回调带有错误信息的 completionBlock
completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES);
}
else {
// 回调成功拿到图片的 completionBlock
completionBlock(image, self.imageData, nil, YES);
}
} else {
// 2.3 如果没有图片数据,回调带有错误信息的 completionBlock
completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES);
}
}
// 3. 将 completionBlock 置为 nil
self.completionBlock = nil;
// 4. 重置
[self done];
}
当图片的所有数据下载完成后,SDWebImageDownloader
传入的 completionBlock
被调用,至此,整个图片的下载过程就结束了。
从上面的解读中我们可以看到,一张图片的数据下载是由一个 NSConnection
对象来完成的,这个对象的整个生命周期(从创建到下载结束)又是由 SDWebImageDownloaderOperation
来控制的,将 operation
加入到operation queue
中就可以实现多张图片同时下载了。
简单概括成一句话就是,NSConnection
负责网络请求,NSOperation
负责多线程。
e、反思
-
NSOperation
的-start
方法、-main
方法和-cancel
方法 -
-start
方法中为什么要调用CFRunLoopRun()
或者CFRunLoopRunInMode()
函数? -
SDWebImageDownloaderOperation
中是什么时候开启异步线程的? -
NSURLConnection
的几个代理方法分别在什么时候调用?
-NSURLCache
是什么? - 下载完成后,为什么需要对图片进行解压缩操作?
-
WebP
图片的解码
3、SDImageCache
a、原因
- 以空间换时间,提升用户体验:加载同一张图片,读取缓存是肯定比远程下载的速度要快得多的。
- 减少不必要的网络请求,提升性能,节省流量:一般来讲,同一张图片的
URL
是不会经常变化的,所以没有必要重复下载。另外,现在的手机存储空间都比较大,相对于流量来,缓存占的那点空间算不了什么
b、问题
- 从读取速度和保存时间上来考虑,缓存该怎么存?
key
怎么定? - 内存缓存怎么存?
- 磁盘缓存怎么存?路径、文件名怎么定?
- 使用时怎么读取缓存?
- 什么时候需要移除缓存?怎么移除?
- 文件操作和
NSDirectoryEnumerator
- 如何判断一个图片的格式是
PNG
还是JPG
?
c、属性和方法
SDImageCache
管理着一个内存缓存和磁盘缓存(可选),同时在写入磁盘缓存时采取异步执行,所以不会阻塞主线程,影响用户体验。
SDImageCache.h
文件中的属性:
@property (assign, nonatomic) BOOL shouldDecompressImages; // 读取磁盘缓存后,是否需要对图片进行解压缩
@property (assign, nonatomic) NSUInteger maxMemoryCost; // 其实就是 NSCache 的 totalCostLimit,内存缓存总消耗的最大限制,cost 是根据内存中的图片的像素大小来计算的
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit; // 其实就是 NSCache 的 countLimit,内存缓存的最大数目
@property (assign, nonatomic) NSInteger maxCacheAge; // 磁盘缓存的最大时长,也就是说缓存存多久后需要删掉
@property (assign, nonatomic) NSUInteger maxCacheSize; // 磁盘缓存文件总体积最大限制,以 bytes 来计算
SDImageCache.m
文件中的属性:
@property (strong, nonatomic) NSCache *memCache;
@property (strong, nonatomic) NSString *diskCachePath;
@property (strong, nonatomic) NSMutableArray *customPaths; // 只读的路径,比如 bundle 中的文件路径,用来在 SDWebImage 下载、读取缓存之前预加载用的
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue;
NSFileManager *_fileManager;
枚举
typedef NS_ENUM(NSInteger, SDImageCacheType) {
SDImageCacheTypeNone, // 没有读取到图片缓存,需要从网上下载
SDImageCacheTypeDisk, // 磁盘中的缓存
SDImageCacheTypeMemory // 内存中的缓存
};
SDImageCache.h
文件中的方法:
+ (SDImageCache *)sharedImageCache;
- (id)initWithNamespace:(NSString *)ns;
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;
-(NSString *)makeDiskCachePath:(NSString*)fullNamespace;
- (void)addReadOnlyCachePath:(NSString *)path;
- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
- (void)removeImageForKey:(NSString *)key;
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
- (void)clearMemory;
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
- (void)clearDisk;
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
- (void)cleanDisk;
- (NSUInteger)getSize;
- (NSUInteger)getDiskCount;
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;
- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
- (BOOL)diskImageExistsWithKey:(NSString *)key;
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;
- (NSString *)defaultCachePathForKey:(NSString *)key;
SDImageCache.m
文件中的函数:
NSUInteger SDCacheCostForImage(UIImage *image);
BOOL ImageDataHasPNGPreffix(NSData *data);
SDImageCache.m
文件中的方法:
// Lifecycle
+ (SDImageCache *)sharedImageCache;
- (id)init;
- (id)initWithNamespace:(NSString *)ns;
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;
- (void)dealloc;
// Cache Path
- (void)addReadOnlyCachePath:(NSString *)path; // 添加只读路径,比如 bundle 中的文件路径,用来在 SDWebImage 下载、读取缓存之前预加载用的
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;
- (NSString *)defaultCachePathForKey:(NSString *)key;
- (NSString *)cachedFileNameForKey:(NSString *)key
-(NSString *)makeDiskCachePath:(NSString*)fullNamespace;
// Store Image
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk
- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
// Check if image exists
- (BOOL)diskImageExistsWithKey:(NSString *)key;
- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
// Query the image cache
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key;
- (UIImage *)diskImageForKey:(NSString *)key;
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
- (UIImage *)scaledImageForKey:(NSString *)key image:(UIImage *)image;
// Remove specified image
- (void)removeImageForKey:(NSString *)key;
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
// Setter and getter
- (void)setMaxMemoryCost:(NSUInteger)maxMemoryCost;
- (NSUInteger)maxMemoryCost;
- (NSUInteger)maxMemoryCountLimit;
- (void)setMaxMemoryCountLimit:(NSUInteger)maxCountLimit;
// Clear and clean
- (void)clearMemory;
- (void)clearDisk;
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
- (void)cleanDisk;
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
- (void)backgroundCleanDisk;
// Cache Size
- (NSUInteger)getSize;
- (NSUInteger)getDiskCount;
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;
d、具体实现
SDImageCache
的内存缓存是通过一个继承 NSCache
的 AutoPurgeCache
类来实现的。
NSCache
是一个类似于 NSMutableDictionary
存储 key-value
的容器,主要有以下特点:
-
自动删除机制:当系统内存紧张时,
NSCache
会自动删除一些缓存对象 -
线程安全:从不同线程中对同一个
NSCache
对象进行增删改查时,不需要加锁 - 不同于
NSMutableDictionary
,NSCache
存储对象时不会对key
进行copy
操作
// SDImageCache
_memCache = [[AutoPurgeCache alloc] init];
// AutoPurgeCache
@interface AutoPurgeCache : NSCache
@end
@implementation AutoPurgeCache
- (id)init
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
@end
SDImageCache
的磁盘缓存是通过异步操作 NSFileManager
存储缓存文件到沙盒来实现的。
1、初始化方法调用栈
-init
-initWithNamespace:
-makeDiskCachePath:
-initWithNamespace:diskCacheDirectory:
-init
方法中默认调用了 -initWithNamespace:
方法,-initWithNamespace:
方法又调用了 -makeDiskCachePath:
方法来初始化缓存目录路径, 同时还调用了 -initWithNamespace:diskCacheDirectory:
方法来实现初始化。
- (id)init {
return [self initWithNamespace:@"default"];
}
- (id)initWithNamespace:(NSString *)ns {
NSString *path = [self makeDiskCachePath:ns];
return [self initWithNamespace:ns diskCacheDirectory:path];
}
-initWithNamespace: diskCacheDirectory:
是一个 Designated Initializer
,这个方法中主要是初始化实例变量、属性,设置属性默认值,并根据 namespace
设置完整的缓存目录路径,除此之外,还针对 iOS 添加了通知观察者,用于内存紧张时清空内存缓存,以及程序终止运行时和程序退到后台时清扫磁盘缓存。
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
if ((self = [super init])) {
// 根据 namespace 设置完整的缓存目录路径
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
// 初始化实例变量、属性,设置属性默认值
// initialise PNG signature data
kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];
// Create IO serial queue
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
// Init default values
_maxCacheAge = kDefaultCacheMaxCacheAge;
// Init the memory cache
_memCache = [[AutoPurgeCache alloc] init];
_memCache.name = fullNamespace;
// Init the disk cache
if (directory != nil) {
_diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
} else {
NSString *path = [self makeDiskCachePath:ns];
_diskCachePath = path;
}
// Set decompression to YES
_shouldDecompressImages = YES;
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});
// 添加了通知观察者,用于内存紧张时清空内存、磁盘缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
// 以及程序终止运行时和程序退到后台时清扫磁盘缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}
return self;
}
2、写入缓存
步骤一:写入缓存的操作主要是由 - storeImage:recalculateFromImage:imageData:forKey:toDisk:
方法处理的,在存储缓存数据时,先计算图片像素大小,并存储到内存缓存中去,然后如果需要存到磁盘(沙盒)中,就开启异步线程将图片的二进制数据存储到磁盘(沙盒)中。
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (!image || !key) {
return;
}
// 1. 添加内存缓存
// 1.1 先计算图片像素大小
NSUInteger cost = SDCacheCostForImage(image);
// 1.2 并存储到内存缓存中去
[self.memCache setObject:image forKey:key cost:cost];
// 2. 如果需要存储到沙盒的话,就异步执行磁盘缓存操作
// 如果需要存到磁盘(沙盒)中
if (toDisk) {
// 就开启异步线程
dispatch_async(self.ioQueue, ^{
// 将图片的二进制数据存储到磁盘(沙盒)中
NSData *data = imageData;
// 2.1 如果需要 recalculate (重新转 data)或者传进来的 imageData 为空的话,就再转一次 data,因为存为文件的必须是二进制数据
if (image && (recalculate || !data)) {
......
}
if (data) {
// 2.2 借助 NSFileManager 将图片二进制数据存储到沙盒
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// 存储的文件名是对 key 进行 MD5 处理后生成的字符串
[_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];
}
});
}
}
步骤二:如果需要在存储之前将传进来的 image
转成 NSData
,而不是直接使用传入的 imageData
,那么就要针对 iOS 系统下,按不同的图片格式来转成对应的 NSData
对象。那么图片格式是怎么判断的呢?这里是根据是否有 alpha
通道以及图片数据的前 8 位字节来判断是不是 PNG
图片,不是 PNG
的话就按照 JPG
来处理。
// 将图片的二进制数据存储到磁盘(沙盒)中
NSData *data = imageData;
// 2.1 如果需要 recalculate (重新转 data)或者传进来的 imageData 为空的话,就再转一次 data,因为存为文件的必须是二进制数据
if (image && (recalculate || !data)) {
// 2.1.1 如果 imageData 为 nil,就根据 image 是否有 alpha 通道来判断图片是否是 PNG 格式的
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;
// 2.1.2 如果 imageData 不为 nil,就根据 imageData 的前 8 位字节来判断是不是 PNG 格式的
// 因为 PNG 图片有一个唯一签名,前 8 位字节是(十进制): 137 80 78 71 13 10 26 10
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
// 2.1.3 根据图片格式将 UIImage 转为对应的二进制数据 NSData
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
}
else {
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
}
步骤三:将图片数据存储到磁盘(沙盒)时,需要提供一个包含文件名的路径,这个文件名是一个对 key
进行 MD5
处理后生成的字符串。
// 存储的文件名是对 key 进行 MD5 处理后生成的字符串
[_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];
- (NSString *)defaultCachePathForKey:(NSString *)key {
return [self cachePathForKey:key inPath:self.diskCachePath];
}
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path {
NSString *filename = [self cachedFileNameForKey:key];
return [path stringByAppendingPathComponent:filename];
}
#define CC_MD5_DIGEST_LENGTH 16 /* digest length in bytes */
- (NSString *)cachedFileNameForKey:(NSString *)key {
const char *str = [key UTF8String];
if (str == NULL) {
str = "";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11], r[12], r[13], r[14], r[15]];
return filename;
}
3、读取缓存
步骤一:SDWebImage
在给 UIImageView
加载图片时首先需要查询缓存,查询缓存的操作主要是 -queryDiskCacheForKey:done:
方法来实现的,该方法首先会调用-imageFromMemoryCacheForKey
方法来查询内存缓存,也就是从 memCache
中去找,如果找到了对应的图片(一个 UIImage
对象),就直接回调 doneBlock
,并直接返回。 如果内存缓存中没有找到对应的图片,就开启异步队列,调用 -diskImageForKey
读取磁盘缓存,读取成功之后,再保存到内存缓存,最后再回到主队列,回调 doneBlock
。
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
// 1.先检查内存缓存,如果找到了对应的图片就回调 doneBlock,并直接返回
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
// 2.如果内存缓存中没有找到对应的图片,开启异步队列,读取硬盘缓存
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
@autoreleasepool {
// 2.1 读取磁盘缓存
UIImage *diskImage = [self diskImageForKey:key];
// 2.2 如果有磁盘缓存,就保存到内存缓存
if (diskImage) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
// 2.3 回到主队列,回调 doneBlock
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
步骤二:其中读取磁盘缓存并不是一步就完成了的,读取磁盘缓存时,会先从沙盒中去找,如果沙盒中没有,再从 customPaths
(也就是bundle
)中去找,找到之后,再对数据进行转换,后面的图片处理步骤跟图片下载成功后的图片处理步骤一样——先将 data
转成 image
,再根据文件名中的 @2x
、@3x
进行缩放处理,如果需要解压缩,最后再解压缩一下。
- (UIImage *)diskImageForKey:(NSString *)key {
// 读取磁盘缓存时,会先从沙盒中去找,如果沙盒中没有,再从 customPaths (也就是bundle)中去找
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
// 跟图片下载成功后的图片处理步骤一样
if (data) {
// 先将 data 转成 image
UIImage *image = [UIImage sd_imageWithData:data];
// 再根据文件名中的 @2x、@3x 进行缩放处理
image = [self scaledImageForKey:key image:image];
// 如果需要解压缩,最后再解压缩一下
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
return image;
}
else {
return nil;
}
}
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
// 取磁盘缓存时,会先从沙盒中去找
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath];
if (data) {
return data;
}
// 如果沙盒中没有,再从 customPaths (也就是bundle)中去找
NSArray *customPaths = [self.customPaths copy];
for (NSString *path in customPaths) {
NSString *filePath = [self cachePathForKey:key inPath:path];
NSData *imageData = [NSData dataWithContentsOfFile:filePath];
if (imageData) {
return imageData;
}
}
return nil;
}
4、清扫磁盘缓存
每新加载一张图片,就会新增一份缓存,时间一长,磁盘上的缓存只会越来越多,所以我们需要定期清除部分缓存。值得注意的是,清扫磁盘缓存(clean
)和清空磁盘缓存(clear
)是两个不同的概念,清空是删除整个缓存目录,清扫只是删除部分缓存文件。
清扫磁盘缓存有两个指标:一是缓存有效期,二是缓存体积最大限制。SDImageCache
中的缓存有效期是通过 maxCacheAge
属性来设置的,默认值是 1 周,缓存体积最大限制是通过 maxCacheSize
来设置的,默认值为 0。
SDImageCache
在初始化时添加了通知观察者,所以在应用即将终止时和退到后台时,都会调用 -cleanDiskWithCompletionBlock:
方法来异步清扫缓存。
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
.......
// 添加了通知观察者,用于内存紧张时清空内存、磁盘缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
// 以及程序终止运行时和程序退到后台时清扫磁盘缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
......
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
SDDispatchQueueRelease(_ioQueue);
}
// 清空内存缓存
- (void)clearMemory {
[self.memCache removeAllObjects];
}
// 清空磁盘缓存
- (void)clearDisk {
[self clearDiskOnCompletion:nil];
}
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion
{
dispatch_async(self.ioQueue, ^{
[_fileManager removeItemAtPath:self.diskCachePath error:nil];
[_fileManager createDirectoryAtPath:self.diskCachePath
withIntermediateDirectories:YES
attributes:nil
error:NULL];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
});
}
// 清扫只是删除部分缓存文件
- (void)cleanDisk {
[self cleanDiskWithCompletionBlock:nil];
}
- (void)backgroundCleanDisk {
......
[self cleanDiskWithCompletionBlock:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
}
清扫磁盘缓存的逻辑是,先遍历所有缓存文件,并根据文件的修改时间来删除过期的文件,同时记录剩下的文件的属性和总体积大小,如果设置了 maxCacheAge 属性的话,接下来就把剩下的文件按修改时间从小到大排序(最早的排最前面),最后再遍历这个文件数组,一个一个删,直到总体积小于 desiredCacheSize
为止,也就是 maxCacheSize
的一半。
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
// 先遍历所有缓存文件
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
// Skip directories.
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// Remove files that are older than the expiration date;
// 并根据文件的修改时间来删除过期的文件
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// Store a reference to this file and account for its total size.
// 同时记录剩下的文件的属性和总体积大小
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
// 如果设置了 maxCacheAge 属性的话
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
// Target half of our maximum cache size for this cleanup pass.
const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
// Sort the remaining cache files by their last modification time (oldest first).
// 接下来就把剩下的文件按修改时间从小到大排序(最早的排最前面)
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// Delete files until we fall below our desired cache size.
// 最后再遍历这个文件数组,一个一个删,直到总体积小于 desiredCacheSize 为止,也就是 maxCacheSize 的一半。
for (NSURL *fileURL in sortedFiles) {
if ([_fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
4、SDWebImageManager
a、问题
- 读取磁盘缓存操作和下载操作都是异步的,如何管理这两个操作(
operation
)? - 对于下载失败过的
URL
,如何处理重试?
b、属性和方法
SDWebImageManager.h
文件中的属性
@property (weak, nonatomic) iddelegate;
@property (strong, nonatomic, readonly) SDImageCache *imageCache; // 缓存器
@property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader; // 下载器
@property (nonatomic, copy) SDWebImageCacheKeyFilterBlock cacheKeyFilter; // 用来自定义缓存 key 的 block
SDWebImageManager.m
文件中的属性
@property (strong, nonatomic, readwrite) SDImageCache *imageCache;
@property (strong, nonatomic, readwrite) SDWebImageDownloader *imageDownloader;
@property (strong, nonatomic) NSMutableSet *failedURLs; // 下载失败过的 URL
@property (strong, nonatomic) NSMutableArray *runningOperations; // 正在执行中的任务
c、具体实现
SDWebImageManager
的核心任务是由 -downloadImageWithURL:options:progress:completed:
方法来实现的,这个方法中先会从SDImageCache
中读取缓存,如果有缓存,就直接返回缓存,如果没有就通过 SDWebImageDownloader
下载,下载成功后再保存到缓存中去,然后再回调 completedBlock
。其中 progressBlock
的回调是直接交给了 SDWebImageDownloader
的 progressBlock
来处理的。
SDWebImageManager
在读取缓存和下载之前会创建一个 SDWebImageCombinedOperation
对象,这个对象是用来管理缓存读取操作和下载操作的,SDWebImageCombinedOperation
对象有 3 个属性:
-
cancelled
:用来取消当前加载任务的 -
cancelBlock
:用来移除当前加载任务和取消下载任务的 -
cacheOperation
:用来取消读取缓存操作
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
// Invoking this method without a completedBlock is pointless
// 1. 对 completedBlock 和 url 进行检查
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
// 2. 创建 SDWebImageCombinedOperation 对象
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
// 3. 判断是否是曾经下载失败过的 url
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
// 4. 如果这个 url 曾经下载失败过,并且没有设置 SDWebImageRetryFailed,就直回调 completedBlock,并且直接返回
if (!url || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
dispatch_main_sync_safe(^{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
});
return operation;
}
// 5. 添加 operation 到 runningOperations 中
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
// 6. 计算缓存用的 key,读取缓存
NSString *key = [self cacheKeyForURL:url];
// 7. 处理缓存查询结果回调
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
// 7.1 判断 operation 是否已经被取消了,如果已经取消了就直接移除 operation
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}
// 7.2.A 如果缓存中没有图片或者图片每次都需要更新
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
// 7.2.A.1 如果有缓存图片,先回调 completedBlock,回传缓存的图片
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
completedBlock(image, nil, cacheType, YES, url);
});
}
// download if no image or requested to refresh anyway, and download allowed by delegate
// 7.2.A.2 开始下载图片,获得 subOperation
SDWebImageDownloaderOptions downloaderOptions = 0;
.......
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
// 7.2.A.2.1.A 操作被取消,什么都不干
if (weakOperation.isCancelled) {}
// 7.2.A.2.1.B 下载失败
else if (error) {
// 7.2.A.2.B.1 没有被取消的话,回调 completedBlock
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
}
});
.......
if (shouldBeFailedURLAlliOSVersion || shouldBeFailedURLiOS7) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}
// 7.2.A.2.1.C 下载成功
else {
// 7.2.A.2.1.C.1 将 URL 从下载失败的黑名单中移除
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
// 7.2.A.2.1.C.2 缓存图片
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}
// 7.2.A.2.1.C.3 回调 completedBlock
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
}
// 7.2.A.2.2 将 operation 从 runningOperations 中移除
if (finished) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
// 7.2.1.3 设置 SDWebImageCombinedOperation 的 cancelBlock——cancel 掉 subOperation,并移除 operation
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:weakOperation];
}
};
// 7.2.B 如果有缓存图片且不需要每次更新
else if (image) {
// 7.2.B.1 回调 completedBlock
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
});
// 7.2.B.2 流程结束,从 runningOperations 中移除 operation
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
// 7.2.C 如果没有缓存图片而且不允许下载
else {
// Image not in cache and download disallowed by delegate
// 7.2.B.1 回调 completedBlock
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
}
});
// 7.2.B.2 流程结束,从 runningOperations 中移除 operation
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
}];
return operation;
}
5、UIImageView+WebCache
a、问题
- 如何处理
UIImageView
连续多次加载图片的情况,比如在UITableView
的 ...cellForRow
... 方法中加载cell
上的图片? - 如何处理
placeholder image
的显示逻辑?
b、具体实现
实际上是将 SDWebImageManager
封装了一层
- 占位图设置
- 自动管理图片加载任务
- 图片成功获取后,自动设置图片显示
为了防止多个异步加载任务同时存在时,可能出现互相冲突和干扰,该方法中首先通过调用 -sd_cancelCurrentImageLoad
方法取消这个 UIImageView
当前的下载任务,然后设置了占位图,如果 url
不为 nil
,接着就调用 SDWebImageManager
的 -downloadImage...
方法开始加载图片,并将这个加载任务 operation
保存起来,用于后面的 cancel
操作。图片获取成功后,再重新设置 imageView
的 image
,并回调 completedBlock
。
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
// 1. 取消当前正在进行的加载任务
[self sd_cancelCurrentImageLoad];
// 2. 通过 Associated Object 将 url 作为成员变量存起来
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 3. 设置占位图
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
// 4. 根据 url 是否为 nil 做处理
if (url) {// A. 如果 url 不为 nil
__weak __typeof(self)wself = self;
// A.1 调用 SDWebImageManager 的 -downloadImage... 方法开始加载图片,并获得一个 operation
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
dispatch_main_sync_safe(^{
if (!wself) return;
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
// A.1.1.A 如果不需要自动设置 image,直接 return
completedBlock(image, error, cacheType, url);
return;
}
else if (image) {
// A.1.1.B 图片下载成功,设置 image
wself.image = image;
[wself setNeedsLayout];
} else {
// A.1.1.C 图片下载失败,设置 placeholder
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
// A.1.2 回调 completedBlock
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
// A.2 借助 UIView+WebCacheOperation 将获得的 operation 保存到成员变量中去
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else {
// B. URL 为空时,直接回调 completedBlock,返回错误信息
dispatch_main_async_safe(^{
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
if (completedBlock) {
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
值得注意的是,为了防止多个异步加载任务同时存在时,可能出现互相冲突和干扰,每个UIImageView
的图片加载任务都会保存成一个 Associated Object
,方便需要时取消任务。这个Associated Object
的操作是在 UIView+WebCacheOperation
中实现的,因为除了 UIImageView
用到图片加载功能之外,还有 UIButton
等其他类也用到了加载远程图片的功能,所以需要进行同样的处理,这样设计实现了代码的复用。
#import "UIView+WebCacheOperation.h"
#import "objc/runtime.h"
static char loadOperationKey;
@implementation UIView (WebCacheOperation)
- (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;
}
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
[self sd_cancelImageLoadOperationWithKey:key];
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary setObject:operation forKey:key];
}
- (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 <SDWebImageOperation> operation in operations) {
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
[(id<SDWebImageOperation>) operations cancel];
}
[operationDictionary removeObjectForKey:key];
}
}
- (void)sd_removeImageLoadOperationWithKey:(NSString *)key {
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary removeObjectForKey:key];
}
@end
Demo
Demo在我的Github上,欢迎下载。
SourceCodeAnalysisDemo