SDWebImage 下载篇

1,578 阅读10分钟

之前就看过SDWebImage的源码,当时只是为了看而看,看了个热闹,这次是以学习为目的来看的。 下面包括代码解析和设计思考两个部分,很遗憾的说在看代码的时候感觉自己就是在还债,希望这篇文章也能给你提供不同的思路。

SDWebImageDownloader

init

- (nonnull instancetype)init {
    return [self initWithConfig:SDWebImageDownloaderConfig.defaultDownloaderConfig];
}

- (instancetype)initWithConfig:(SDWebImageDownloaderConfig *)config {
    self = [super init];
    if (self) {
        ...
    }
    return self;
}

在初始化SDWebImageDownloader时如果没有传入config,则会使用默认的defaultDownloaderConfigconfig中记录着最大并发数,超时时长等配置,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的任务,这里用到的OperationisCancelisFinished属性也会在新的线程中进行改变,所以需要加锁进行处理。 还有一个细节时,如果需要监听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被引入,其可以使我们不必等前一个一个请求结束便可以发起下一个请求,其大致如下图。

image.png

继续向下看

// 如果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中的测试用例简单说明一下执行的顺序。

image.png 下面的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是怎么实现的呢?

这几个类的关系图如下:

image.png 首先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];

剩下的NSURLSessionTaskDelegateNSURLSessionDataDelegate中的方法都是将收到的代理回调分发给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协议的缓存策略。但为什么要在设置IgnoreCachecache数据呢?我们结合使用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协议缓存策略的简略逻辑如下。 image.png

// 根据配置来设置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:

回调时机:刚开始收到服务器发来Resopnseheader数据时。

// 如果调用方需要修改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任务执行的线程不是同一个线程,所以需要@synchronizedself.dataTask进行保护。

谈谈设计

Config

我们在封装独立的功能模块,当需要调用方传来的参数时会提供接收参数的构造方法,但是参数很多的时候会变得极其复杂。这个时候就可以使用Config的方式,我们可以把一些必传的参数使用构造方法进行传递,其它非必传的参数放在Config中,而且如果需要设置默认值时就可以通过提供defaultConfig的方式去实现,Android中是通过建造者模式来解决的。

依赖注入

SDWebImage中很多地方用到了依赖注入的思想,比如上文将的operationClass等的实现,这种方式在Android中比较常见,在iOS中很少有人这样去做,具体的讲解可以回到上文requestModifierSDWebImageDownloaderOperation的部分。

SDWebImage设计确实让我学到比较多的东西,SDWebImageDownloader将所有下载回调都分发给了SDWebImageDownloaderOperation去具体的处理,这样的好处是SDWebImageDownloader可以很方便的使用其它的Customer Operation,并且SDWebImageDownloaderOperation也可以单独被使用,为了保证其能单独使用还添加了ownedSession等相应处理。

为什么要使用NSOperation?

这是我一开始在看代码时就想到的问题,NSURLSession的代理回调都是在一个串行队列中,本身其就在新的子线程中了,而且还专门创建一个queue来处理图片的编码,按道理来讲并没有其它的耗时任务需要专门开辟一个字线程来做了。 在看完代码之后,我猜测可能是为了复用NSOperation的队列管理,因为其中有各种优先级的设置,NSOperation的确是一个很稳定的选择。反思自己在做项目的时候,为了自己兴趣,重复造了很多轮子。

NS_OPTIONS 和 NS_ENUM

我的理解是NS_ENUM是用来单选的,NS_OPTIONS是用来多选的。

这些是我在看SDWebImage下载部分的思考,希望能给看到此篇的同学提供一些不一样思路。