之前就看过SDWebImage
的源码,当时只是为了看而看,看了个热闹,这次是以学习为目的来看的。
下面包括代码解析和设计思考两个部分,很遗憾的说在看代码的时候感觉自己就是在还债,希望这篇文章也能给你提供不同的思路。
SDWebImageDownloader
init
- (nonnull instancetype)init {
return [self initWithConfig:SDWebImageDownloaderConfig.defaultDownloaderConfig];
}
- (instancetype)initWithConfig:(SDWebImageDownloaderConfig *)config {
self = [super init];
if (self) {
...
}
return self;
}
在初始化SDWebImageDownloader
时如果没有传入config
,则会使用默认的defaultDownloaderConfig
,config
中记录着最大并发数,超时时长等配置,SDWebImageDownloader
中有很多非必传的入参,如果一个一个传进来会增加SDWebImageDownloader
的复杂度,这里使用config统一收拢,并且提供默认设置defaultDownloaderConfig
。还有一个小细节defaultDownloaderConfig
是一个单例,就算SDWebImageDownloader
被频繁创建,defaultDownloaderConfig
也只会创建一次,减少资源的消耗。
_config = [config copy];
[_config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxConcurrentDownloads)) options:0 context:SDWebImageDownloaderContext];
将config
进行copy
,防止调用方通过修改config
造成前后状态不一致,如果之后要修改config
中的参数得需要通过下面方式:
SDWebImageDownloader *downloader = [[SDWebImageDownloader alloc] initWithConfig:config];
downloader.config.maxConcurrentDownloads = 4;
// 创建下载队列
_downloadQueue = [NSOperationQueue new];
// 设置最大并发数
_downloadQueue.maxConcurrentOperationCount = _config.maxConcurrentDownloads;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
// 创建记录Operation的字典,方便通过key-value的方式取Operation
_URLOperations = [NSMutableDictionary new];
NSMutableDictionary<NSString *, NSString *> *headerDictionary = [NSMutableDictionary dictionary];
// 创建_HTTPHeadersLock的锁
SD_LOCK_INIT(_HTTPHeadersLock);
// 创建_operationsLock的锁
SD_LOCK_INIT(_operationsLock);
// 创建默认配置的session
NSURLSessionConfiguration *sessionConfiguration = _config.sessionConfiguration;
if (!sessionConfiguration) {
sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
}
_session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
初始化_HTTPHeadersLock
和_operationsLock
锁,其中SD_LOCK_INIT(lock)
在iOS10之前使用的是自旋锁,之后使用的是os_unfair_lock
。自旋锁是一个忙等锁,线程反复检查资源是否可用,不会挂起,避免上下文切换的开销,但是会存在优先级反转的问题,具体可看不再安全的 OSSpinLock,iOS10之后苹果推出了os_unfair_lock来
代替自旋锁。
downloadImageWithURL
// 加锁
SD_LOCK(_operationsLock);
id downloadOperationCancelToken;
// 通过url获取operation
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
if (!operation || operation.isFinished || operation.isCancelled) {
operation = [self createDownloaderOperationWithUrl:url options:options context:context];
if (!operation) {
// 解锁
SD_UNLOCK(_operationsLock);
// 创建operation失败则向上回调
...
}
@weakify(self);
operation.completionBlock = ^{
@strongify(self);
if (!self) {
return;
}
// 完成之后删除operation
SD_LOCK(self->_operationsLock);
[self.URLOperations removeObjectForKey:url];
SD_UNLOCK(self->_operationsLock);
};
self.URLOperations[url] = operation;
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
[self.downloadQueue addOperation:operation];
}
// 解锁
SD_UNLOCK(_operationsLock);
在进行opertaion
的相关操作时加锁处理了,因为系统会在一个新线程中去处理Operation
的任务,这里用到的Operation
的isCancel
和isFinished
属性也会在新的线程中进行改变,所以需要加锁进行处理。
还有一个细节时,如果需要监听Operation
的完成时机,则需要在addOperation
前配置completionBlock
等,因为在addOperation
之后系统就会自动将NSOperationQueue
中的 NSOperation
取出来进行操作,如果不按现在的顺序的话可能存在在监听之前就已经完成的情况。
// 根据配置设置operation的优先级
if (!operation.isExecuting) {
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
} else {
operation.queuePriority = NSOperationQueuePriorityNormal;
}
}
createDownloaderOperationWithUrl:options:context
创建Request
并且配置相应的属性。
NSTimeInterval timeoutInterval = self.config.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
mutableRequest.HTTPShouldHandleCookies = SD_OPTIONS_CONTAINS(options, SDWebImageDownloaderHandleCookies);
mutableRequest.HTTPShouldUsePipelining = YES;
SD_LOCK(_HTTPHeadersLock);
mutableRequest.allHTTPHeaderFields = self.HTTPHeaders;
SD_UNLOCK(_HTTPHeadersLock);
正常情况下,在我们发一起一个请求结束后,下一个请求才会发起,为了减少等待耗时,在HTTP1.1中Pipelining
被引入,其可以使我们不必等前一个一个请求结束便可以发起下一个请求,其大致如下图。
继续向下看
// 如果context未传来requestModifier则使用本身的requestModifier
id<SDWebImageDownloaderRequestModifier> requestModifier;
if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) {
requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier];
} else {
requestModifier = self.requestModifier;
}
NSURLRequest *request;
// 如果requestModifier则调用 modifiedRequestWithRequest
if (requestModifier) {
NSURLRequest *modifiedRequest = [requestModifier modifiedRequestWithRequest:[mutableRequest copy]];
if (!modifiedRequest) {
return nil;
} else {
request = [modifiedRequest copy];
}
} else {
request = [mutableRequest copy];
}
requestModifier
是一个非常棒的设计,他给了外界进行二次修改request
的机会。
requestModifier = [SDWebImageDownloaderRequestModifier requestModifierWithBlock:^NSURLRequest * _Nullable(NSURLRequest * _Nonnull request) {
if ([request.URL.absoluteString isEqualToString:kTestPNGURL]) {
NSMutableURLRequest *mutableRequest = [request mutableCopy];
[mutableRequest setValue:@"Bar" forHTTPHeaderField:@"Foo"];
NSURLComponents *components = [NSURLComponents componentsWithURL:mutableRequest.URL resolvingAgainstBaseURL:NO];
components.query = @"text=Hello+World";
mutableRequest.URL = components.URL;
return mutableRequest;
}
}];
downloader.requestModifier = requestModifier;
这里借用SDWebImage中的测试用例简单说明一下执行的顺序。
下面的ResponseModifier和DownloaderDecryptor也是同理,将处理时机抛给调用方,使他们可以做个性化的处理。
Class operationClass = self.config.operationClass;
if (operationClass &&
[operationClass isSubclassOfClass:[NSOperation class]] &&
[operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
// Custom operation class
} else {
operationClass = [SDWebImageDownloaderOperation class];
}
NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
这个operationClass
的设计也是极其巧妙,通过operationClass
外界可以切换成自己的下载能力,只需要继承NSOperation
并且遵循SDWebImageDownloaderOperation
协议便可。如果想自己去替换SDWebImageDownloaderOperation
可以借鉴SDWebImageTestDownloadOperation
。
那么SDWebImage
是怎么实现的呢?
这几个类的关系图如下:
首先SD将SDWebImageDownloader
要是使用Operation
的方法抽象为<SDWebImageDownloaderOperation>
,SDWebImageDownloader
不直接使用实现类SDWebImageDownloaderOperation
,在需要使用Operation
的地方通过使用
NSOperation <SDWebImageDownloaderOperation>
来完成。比如其中一个操作:
NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:dataTask];
if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)]) {
[dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}
只需要在创建Operation
时指定使用那个实现类就可。
NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
剩下的NSURLSessionTaskDelegate
和NSURLSessionDataDelegate
中的方法都是将收到的代理回调分发给operation
,这部分的解析转移到SDWebImageDownloaderOperation中
。
SDWebImageDownloaderOperation
initWithRequest
// 如果传入的是NSMutableURLRequest,外界可以修改配置参数,造成前后状态不一致此时使用copy,会copy出一个新Request,这样就可以不受外界干扰
_request = [request copy];
_options = options;
// 与上同理,获取一个新的contexr,不受外界操作变化的影响
_context = [context copy];
_callbackBlocks = [NSMutableArray new];
_responseModifier = context[SDWebImageContextDownloadResponseModifier];
_decryptor = context[SDWebImageContextDownloadDecryptor];
// 保存外界传来的session
_unownedSession = session;
// 创建一个串行的编码队列
_coderQueue = [NSOperationQueue new];
_coderQueue.maxConcurrentOperationCount = 1;
_backgroundTaskId = UIBackgroundTaskInvalid;
在获取入参的时候善用copy
会减少很多莫名的问题。
// 如果已经取消了则将finish置为YES,向上回调并且进行重置操作
if (self.isCancelled) {
if (!self.isFinished) self.finished = YES;
// Operation cancelled by user before sending the request
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user before sending the request"}]];
[self reset];
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)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
// 取消任务
[wself cancel];
}];
}
// 如果unownedSession为空
NSURLSession *session = self.unownedSession;
if (!session) {
// 创建ownedSession
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
self.ownedSession = session;
}
什么时候unownedSession
为空呢?这里会放在后面讲。
if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
// 根据request获取缓存的response
NSURLCache *URLCache = session.configuration.URLCache;
if (!URLCache) {
URLCache = [NSURLCache sharedURLCache];
}
NSCachedURLResponse *cachedResponse;
// cachedResponseForRequest是非线程安全的
@synchronized (URLCache) {
cachedResponse = [URLCache cachedResponseForRequest:self.request];
}
// 记录缓存数据
if (cachedResponse) {
self.cachedData = cachedResponse.data;
}
}
// SDWebImageDownloader 中代码
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
系统默认设置好了内存以及磁盘空间来缓存我们的网络请求,默认情况下的缓存策略是NSURLRequestUseProtocolCachePolicy
,这个代表的是走HTTP
协议的缓存策略。但为什么要在设置IgnoreCache
中cache
数据呢?我们结合使用cache
的地方来看。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
...
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image is not modified and ignored"}];
// call completion block with not modified error
[self callCompletionBlocksWithError:self.responseError];
[self done];
}
...
}
在设置忽略缓存的情况下,如果请求下来的数据和缓存数据一样则报错。
HTTP
协议缓存策略的简略逻辑如下。
// 根据配置来设置dataTask和解码队列的优先级
if (self.options & SDWebImageDownloaderHighPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityHigh;
self.coderQueue.qualityOfService = NSQualityOfServiceUserInteractive;
} else if (self.options & SDWebImageDownloaderLowPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityLow;
self.coderQueue.qualityOfService = NSQualityOfServiceBackground;
} else {
self.dataTask.priority = NSURLSessionTaskPriorityDefault;
self.coderQueue.qualityOfService = NSQualityOfServiceDefault;
}
// dataTask发起请求
[self.dataTask resume];
// 进行进度为0的回调,同一个url的图片不会开启新的下载,只是添加一个新的监听
// 触发所有监听回调
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
__block typeof(self) strongSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
// 发送通知
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:strongSelf];
});
每个NSURLSession
对象会有自己的socket
复用池,由同一个NSURLSession
管理的task
,并发请求是有限制的,而priority
会来控制这些task
的优先级。
qualityOfService
时用于指定应用于添加到队列的操作对象的服务级别,服务级别影响操作对象访问系统资源(如CPU时间、网络资源、磁盘资源等)的优先级,服务水平越高的操作,其优先级就越高。mainQueue
是服务级别在最高的 NSQualityOfServiceUserInteractive
,不要滥用SDWebImageDownloaderHighPriority
,因为会和主队列竞争资源。
Notification
在哪个线程中post
,就在哪个线程中被转发,而不一定是在注册观察者的那个线程中,此时切主线程是为了防止外界做UI操作的时候出现问题
URLSession:dataTask:didReceiveResponse:completionHandler:
回调时机:刚开始收到服务器发来Resopnse
的header
数据时。
// 如果调用方需要修改response,则向上抛出修改时机
if (self.responseModifier && response) {
response = [self.responseModifier modifiedResponseWithResponse:response];
// 防止调用方修改的时候返回nil
if (!response) {
valid = NO;
self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadResponse userInfo:@{NSLocalizedDescriptionKey : @"Download marked as failed because response is nil"}];
}
}
// 记录数据
NSInteger expected = (NSInteger)response.expectedContentLength;
expected = expected > 0 ? expected : 0;
self.expectedSize = expected;
self.response = response;
// 防止通过responseModifier返回来的response不是NSHTTPURLResponse
NSInteger statusCode = [response respondsToSelector:@selector(statusCode)] ? ((NSHTTPURLResponse *)response).statusCode : 200;
// 首先判断请求是否失败
BOOL statusCodeValid = statusCode >= 200 && statusCode < 400;
if (!statusCodeValid) {
valid = NO;
// 组装失败信息
self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadStatusCode userInfo:@{NSLocalizedDescriptionKey : @"Download marked as failed because response status code is not in 200-400", SDWebImageErrorDownloadStatusCodeKey : @(statusCode)}];
}
URLSession:dataTask:didReceiveData:
// 只有设置了支持分布加载并且不是加密的图片才能分步加载
BOOL supportProgressive = (self.options & SDWebImageDownloaderProgressiveLoad) && !self.decryptor;
if (supportProgressive && !finished) {
// 复制拼接后的数据
NSData *imageData = [self.imageData copy];
// 如果解码队列中有任务正在执行则跳过
if (self.coderQueue.operationCount == 0) {
@weakify(self);
[self.coderQueue addOperationWithBlock:^{
@strongify(self);
if (!self) {
return;
}
// 将图片进行解码
UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, NO, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
if (image) {
// 将图片回调给上层
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
}
}];
}
}
本篇只针对下载模块的解析,之后会单独来写图片解码的部分。
URLSession:task:didCompleteWithError:
当数据传输完成后会触发。
@synchronized(self) {
self.dataTask = nil;
__block typeof(self) strongSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:strongSelf];
if (!error) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:strongSelf];
}
});
}
在使用self.dataTask
时都会使用同步块@synchronized
进行保护,虽然没有显示的出现新的线程,在创建NSURLSession
时如果没有指定其代理回调的队列,系统会自动指定一个串行队列,这样的话代理回调的线程可能和Operation任务执行的线程不是同一个线程,所以需要@synchronized
对self.dataTask
进行保护。
谈谈设计
Config
我们在封装独立的功能模块,当需要调用方传来的参数时会提供接收参数的构造方法,但是参数很多的时候会变得极其复杂。这个时候就可以使用Config
的方式,我们可以把一些必传的参数使用构造方法进行传递,其它非必传的参数放在Config
中,而且如果需要设置默认值时就可以通过提供defaultConfig
的方式去实现,Android
中是通过建造者模式来解决的。
依赖注入
在SDWebImage
中很多地方用到了依赖注入的思想,比如上文将的operationClass
等的实现,这种方式在Android中比较常见,在iOS中很少有人这样去做,具体的讲解可以回到上文requestModifier
和SDWebImageDownloaderOperation
的部分。
SDWebImage
设计确实让我学到比较多的东西,SDWebImageDownloader
将所有下载回调都分发给了SDWebImageDownloaderOperation
去具体的处理,这样的好处是SDWebImageDownloader可以很方便的使用其它的Customer Operation,并且SDWebImageDownloaderOperation
也可以单独被使用,为了保证其能单独使用还添加了ownedSession等相应处理。
为什么要使用NSOperation?
这是我一开始在看代码时就想到的问题,NSURLSession的代理回调都是在一个串行队列中,本身其就在新的子线程中了,而且还专门创建一个queue来处理图片的编码,按道理来讲并没有其它的耗时任务需要专门开辟一个字线程来做了。 在看完代码之后,我猜测可能是为了复用NSOperation的队列管理,因为其中有各种优先级的设置,NSOperation的确是一个很稳定的选择。反思自己在做项目的时候,为了自己兴趣,重复造了很多轮子。
NS_OPTIONS 和 NS_ENUM
我的理解是NS_ENUM是用来单选的,NS_OPTIONS是用来多选的。
这些是我在看SDWebImage下载部分的思考,希望能给看到此篇的同学提供一些不一样思路。