SDWebImage网络部分实现源码解析

SDWebImage是一个OC常用的图片下载第三方框架,其负责下载的类主要有SDWebImageDownloader和SDWebImageDownloaderOperation,下面来分析一下这两个类。本文分析的版本为4.4.3

SDWebImageDownloaderOperation

SDWebImageDownloaderOperation是一个NSOperation的子类的任务是把一张图片从服务器下载到本地。先来看一下其.h文件:

SDWebImageDownloaderOperationInterface

在其h文件的开头,定义了一个SDWebImageDownloaderOperationInterface的协议,SDWebImageDownloaderOperation遵循这个协议,如果用户想自定义一个下载任务,只需遵循这个协议,就可以被SDWebImageDownloader调度了,下面来看看协议的接口

@protocol SDWebImageDownloaderOperationInterface 
@required
//初始化方法,显然一个请求对应一个任务
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options;
//给任务添加进度和完成的回调
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
//是否应该解压图片,下面两个方法可以用一个属性去遵循,
- (BOOL)shouldDecompressImages;
- (void)setShouldDecompressImages:(BOOL)value;
//是否需要设置凭证,用于HTTPS请求,下面两个方法可以用一个属性去遵循
- (nullable NSURLCredential *)credential;
- (void)setCredential:(nullable NSURLCredential *)value;
//取消一系列的回调,token是这一些列回调的表示
- (BOOL)cancel:(nullable id)token;

@optional
//下载任务
- (nullable NSURLSessionTask *)dataTask;

@end

SDWebImageDownloaderOperation所暴露的接口基本也是这些。需要注意的事,如果遵循了这个协议,只需要在.m文件实现即可,这里为了让开发者使用时更加直观,再次将这些方法写到.h文件里。下面来看一下它的实现部分。

SDWebImageDownloaderOperation.m

基本方法

首先是它的初始化方法:

- (nonnull instancetype)init {
    return [self initWithRequest:nil inSession:nil options:0];
}

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options {
    if ((self = [super init])) {
        _request = [request copy];
        _shouldDecompressImages = YES;
        //下载的策略
        _options = options;
        //一个保存回调的数组,其成员变量是一个字典
        //这个字典的键只有两种情况,kProgressCallbackKey和kCompletedCallbackKey,分别代表进度回调和完成回调
        //值为回调的block
        _callbackBlocks = [NSMutableArray new];
        这里重写了父类的两个属性并进行手动管理,原因后面讲
        _executing = NO;
        _finished = NO;
        下载文件的大小
        _expectedSize = 0;
        //这里弱引用了传入的session
        _unownedSession = session;
        //管理回调数组的锁,是一个信号量
        _callbacksLock = dispatch_semaphore_create(1); 
        //创建队列  
        _coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

然后是给任务添加block的代码

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    LOCK(self.callbacksLock);
    [self.callbackBlocks addObject:callbacks];
    UNLOCK(self.callbacksLock);
    return callbacks;
}

其中LOCK的宏如下:

#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);

显然这是一个信号量的锁,是iOS中最高效的锁

- (nullable NSArray *)callbacksForKey:(NSString *)key {
    LOCK(self.callbacksLock);
    NSMutableArray *callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
    UNLOCK(self.callbacksLock);
    [callbacks removeObjectIdenticalTo:[NSNull null]];
    return [callbacks copy]; // strip mutability here
}

数组调用valueForKey方法会使得其中所有元素都调用valueForKey方法再生成一个数组,如果其中有元素的没有这个key,则返回的数组包含[NSNull null]],因此在生成的结果要移除掉这些空值

- (BOOL)cancel:(nullable id)token {
    BOOL shouldCancel = NO;
    LOCK(self.callbacksLock);
    [self.callbackBlocks removeObjectIdenticalTo:token];
    if (self.callbackBlocks.count == 0) {
        shouldCancel = YES;
    }
    UNLOCK(self.callbacksLock);
    if (shouldCancel) {
        [self cancel];
    }
    return shouldCancel;
}

- (void)cancel {
    @synchronized (self) {
        [self cancelInternal];
    }
}

取消某一回调,如果所有的回调都被取消,则取消该任务。

start方法

SDWebImageDownloaderOperation选择重写start()方法来执行其中的任务,代码如下:

//0.start方法在队列在该任务放入队列就会执行
- (void)start {
    @synchronized (self) {
        //1.如果任务已经取消,结束该任务
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }
        //2.根据配置项向app申请后台运行时间,使得app退到后台时仍能执行任务
        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:^{
                __strong __typeof (wself) sself = wself;

                if (sself) {
                    [sself cancel];

                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
        //3.配置好session,如果传入session为nil,则让自己成为session的代理
        NSURLSession *session = self.unownedSession;
        if (!session) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            
            session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                    delegate:self
                                               delegateQueue:nil];
            self.ownedSession = session;
        }
        //4.根据选项读取url缓存中的数据
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
            NSURLCache *URLCache = session.configuration.URLCache;
            if (!URLCache) {
                URLCache = [NSURLCache sharedURLCache];
            }
            NSCachedURLResponse *cachedResponse;
            @synchronized (URLCache) {
                cachedResponse = [URLCache cachedResponseForRequest:self.request];
            }
            if (cachedResponse) {
                self.cachedData = cachedResponse.data;
            }
        }
        
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }
    //5.开启下载任务,并通知外界
    if (self.dataTask) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
        if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
            if (self.options & SDWebImageDownloaderHighPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityHigh;
            } else if (self.options & SDWebImageDownloaderLowPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityLow;
            }
        }
#pragma clang diagnostic pop
        [self.dataTask resume];
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
        });
    } else {
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
        [self done];
        return;
    }
    6.取消后台任务,这个地方没看懂
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
        UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
        [app endBackgroundTask:self.backgroundTaskId];
        self.backgroundTaskId = UIBackgroundTaskInvalid;
    }

}

0.这个地方使用重写start方法而非main方法,其两者的区别在于当main方法执行完后任务的finishd属性就被置为YES,队列就会根据这个属性将该移除,而使用start方法则不会,这里使用start方法的原因是该类还可能需要处理请求的回调(当传入的session为nil时),这个回调可能会在这片代码执行之后才会被调用,也就是说,这个任务在请求回调后才算结束,因此需要手动管理finishd,除了这个属性外,executing也是手动管理的。

1.外界调用cancel并不会真正地取消正在执行的任务,只会把它的isCancelled属性置为YES

2.如果配置了后台下载,则要向应用申请时间,一般能申请180秒,申请时间到后需要调用endBackgroundTask方法,否则会crash

6.这个地方没明白,在提交记录上看到有这么一段话:

SDWebImageDownloaderOperation: call endBackgroundTask after operation is finished so that beginBackgroundTaskWithExpirationHandler and endBackgroundTask are balanced

意思是在任务结束的时候调用endBackgroundTask,但根据0的分析,start结束后并不代表任务结束,因此我并不认为这段代码放在这个地方是合理的

内部还有几个方法,用于管理任务的下载流程

//取消任务
- (void)cancelInternal {
    if (self.isFinished) return;
    [super cancel];

    if (self.dataTask) {
        [self.dataTask cancel];
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
        });

        // As we cancelled the task, 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];
}
结束任务
- (void)done {
    self.finished = YES;
    self.executing = NO;
    [self reset];
}
//重置任务
- (void)reset {
    LOCK(self.callbacksLock);
    [self.callbackBlocks removeAllObjects];
    UNLOCK(self.callbacksLock);
    self.dataTask = nil;
    
    if (self.ownedSession) {
        [self.ownedSession invalidateAndCancel];
        self.ownedSession = nil;
    }
}
//重写finished、executing的set方法,使他们向父类一样可被kvo监听
- (void)setFinished:(BOOL)finished {
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

- (void)setExecuting:(BOOL)executing {
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

上面这些方法没什么好说的,值得一提的是,重写的是start方法而不是main方法,因此需要手动管理cancel、finish、excecuting等变量

网络协议相关部分

从上面的start方法可知,如果在初始化时传入的session为nil,则SDWebImageDownloaderOperation会创建一个session并让自己作为改session的代理,而如果使用了SDWebImageDownloader来下载图片,网络的请求的回调也会调到这里来。

//收到响应头的回调
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;
    NSInteger expected = (NSInteger)response.expectedContentLength;
    expected = expected > 0 ? expected : 0;
    self.expectedSize = expected;
    self.response = response;
    NSInteger statusCode = [response respondsToSelector:@selector(statusCode)] ? ((NSHTTPURLResponse *)response).statusCode : 200;
    BOOL valid = statusCode < 400;
    //如果返回304(本地重定向),但本地又没有缓存,也是这个请求无效的
    if (statusCode == 304 && !self.cachedData) {
        valid = NO;
    }
    if (valid) {
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, expected, self.request.URL);
        }
    } else {
        //标记为取消,如果直接取消,可能会延长session的生命周期
        disposition = NSURLSessionResponseCancel;
    }
    __weak typeof(self) weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:weakSelf];
    });
    //调用这个block表示是否允许这次下载
    if (completionHandler) {
        completionHandler(disposition);
    }
}

这个回调的处理逻辑比较简单,也是没什么好说的

//收到响应数据的回调
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    if (!self.imageData) {
        self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
    }
    [self.imageData appendData:data];
    //SDWebImageDownloaderProgressiveDownload这个选项表示是否阶段性展示图片
    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
        __block NSData *imageData = [self.imageData copy];
        const NSInteger totalSize = imageData.length;
        BOOL finished = (totalSize >= self.expectedSize);
        //初始化序列化器
        if (!self.progressiveCoder) {
            // We need to create a new instance for progressive decoding to avoid conflicts
            for (idcoder in [SDWebImageCodersManager sharedInstance].coders) {
                if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
                    [((id)coder) canIncrementallyDecodeFromData:imageData]) {
                    self.progressiveCoder = [[[coder class] alloc] init];
                    break;
                }
            }
        }
  
        // 在另外的线程对图片解码
        dispatch_async(self.coderQueue, ^{
            @autoreleasepool {
                UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
                if (image) {
                    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                    image = [self scaledImageForKey:key image:image];
                    if (self.shouldDecompressImages) {
                        image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
                    }
                    // 在completionBlock里面回调,这个时候由于还没下载完成,finished为NO
                    [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
                }
            }
        });
    }
    //进度回调
    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
        progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
    }
}

如果把中间的条件代码去掉,这个回调还是很容易看得懂的。

//是否缓存响应
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
    
    NSCachedURLResponse *cachedResponse = proposedResponse;
    //如果没有缓存响应策略
    if (!(self.options & SDWebImageDownloaderUseNSURLCache)) {
        //防止缓存
        cachedResponse = nil;
    }
    if (completionHandler) {
        //传入的数据即为缓存的数据
        completionHandler(cachedResponse);
    }
}

这个策略默认是不打开的,也就是不缓存,如果把回调的参数设置为nil,就不会缓存响应,因为真正缓存的数据就是回调中的参数。

//响应完成的回调
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    //在主线程发送响应的通知
    @synchronized(self) {
        self.dataTask = nil;
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
            if (!error) {
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:weakSelf];
            }
        });
    }
    //不管是哪一个条件分支,最终都要调用 [self done]表示任务结束
    if (error) {
        
        [self callCompletionBlocksWithError:error];
        [self done];
    } else {
        if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
           
            __block NSData *imageData = [self.imageData copy];
            if (imageData) {
                    //这个选项不会将图片image回调出去
                if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
                    [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
                    [self done];
                } else {
                    //解压图片
                    dispatch_async(self.coderQueue, ^{
                        @autoreleasepool {
                            UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
                            NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                            image = [self scaledImageForKey:key image:image];
                            
                            BOOL shouldDecode = YES;
                            // Do not force decoding animated GIFs and WebPs
                            if (image.images) {
                                shouldDecode = NO;
                            } else {
#ifdef SD_WEBP
                                SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
                                if (imageFormat == SDImageFormatWebP) {
                                    shouldDecode = NO;
                                }
#endif
                            }
                            //按照需要解码图片
                            if (shouldDecode) {
                                if (self.shouldDecompressImages) {
                                    BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
                                    image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
                                }
                            }
                            CGSize imageSize = image.size;
                            if (imageSize.width == 0 || imageSize.height == 0) {
                                [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
                            } else {
                                [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
                            }
                            [self done];
                        }
                    });
                }
            } else {
                [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
                [self done];
            }
        } else {
            [self done];
        }
    }
}

这块代码主要复杂在对完成数据的处理,包括数据转图片、图片解码等等,这一块就涉及到另外的知识点了,这里不再赘述

//需要证书验证的时候调用
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
    
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;
    //判断是否需要认证服务器返回的证书
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        } else {
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            disposition = NSURLSessionAuthChallengeUseCredential;
        }
    } else {
            //如果之前有过失败,则取消认证
        if (challenge.previousFailureCount == 0) {
            if (self.credential) {
                credential = self.credential;
                disposition = NSURLSessionAuthChallengeUseCredential;
            } else {
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
        }
    }
    
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

这里如果请求时https的,则需要用到这个方法来处理

SDWebImageDownloader

SDWebImageDownloaderOperation主要是负责下载单个图片的类,而在开发中,图片的下载任务往往是成批出现的,因此需要一个管理这些下载任务的类,SDWebImageDownloader就是这样的类。

SDWebImageDownloader.h

下面来看一下SDWebImageDownloader.h文件。

SDWebImageDownloaderOptions

SDWebImageDownloaderOptions是下载的选项,如下

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    //下载低优先级
    SDWebImageDownloaderLowPriority = 1 << 0,
    //在下载过程中展示部分下载的图片
    SDWebImageDownloaderProgressiveDownload = 1 << 1,
    //使用NSURLCache缓存响应
    SDWebImageDownloaderUseNSURLCache = 1 << 2,
    //忽略缓存的响应
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
    //后台下载
    SDWebImageDownloaderContinueInBackground = 1 << 4,
    //使用cookie
    SDWebImageDownloaderHandleCookies = 1 << 5,
    //允许无效证书验证
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
    //下载高优先级
    SDWebImageDownloaderHighPriority = 1 << 7,
    //裁剪图片
    SDWebImageDownloaderScaleDownLargeImages = 1 << 8,
};


这里设计了几种不同的选项,开发者可以根据自己的需求添加合适的选项。如果想多个选项一起使用,使用|即可。

SDWebImageDownloaderExecutionOrder

这是一个下载任务的调用顺序

typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
    //先进先出
    SDWebImageDownloaderFIFOExecutionOrder,
    //后进先出
    SDWebImageDownloaderLIFOExecutionOrder
};

SDWebImageDownloadToken

SDWebImageDownloadToken是每一个下载的唯一身份标识,SDWebImageOperation是一个协议,它还有cancel这个方法

@interface SDWebImageDownloadToken : NSObject 
//url
@property (nonatomic, strong, nullable) NSURL *url;
//取消的回调
@property (nonatomic, strong, nullable) id downloadOperationCancelToken;
@end

SDWebImagerDownloader

@interface SDWebImageDownloader : NSObject
//是否压缩图片,默认是YES
@property (assign, nonatomic) BOOL shouldDecompressImages;
//最大并发数,默认是6
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;
//并发数
@property (readonly, nonatomic) NSUInteger currentDownloadCount;
//下载超时时间,默认是15秒
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
//URLSession,默认是全局的session
@property (readonly, nonatomic, nonnull) NSURLSessionConfiguration *sessionConfiguration;
//下载策略,默认是先进后出
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;
//获得单例,这个单例并不是严格的
+ (nonnull instancetype)sharedDownloader;
//凭证配置
@property (strong, nonatomic, nullable) NSURLCredential *urlCredential;
//用户名,配置证书用
@property (strong, nonatomic, nullable) NSString *username;
//密码,配置证书用
@property (strong, nonatomic, nullable) NSString *password;
//请求头过滤的block
@property (nonatomic, copy, nullable) SDWebImageDownloaderHeadersFilterBlock headersFilter;
//初始化方法
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration NS_DESIGNATED_INITIALIZER;
//设置请求头
- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field;
//拿到请求头的配置
- (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field;
//设置下载任务的类,默认是SDWebImageDownloaderOperation,传入的类必须是NSOperation的子类且遵循
//SDWebImageDownloaderOperationInterface协议才生效
- (void)setOperationClass:(nullable Class)operationClass;
//下载任务的主要方法
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
//取消任务
- (void)cancel:(nullable SDWebImageDownloadToken *)token;
//挂起任务
- (void)setSuspended:(BOOL)suspended;
取消所有任务
- (void)cancelAllDownloads;
//配置新的session
- (void)createNewSessionWithConfiguration:(nonnull NSURLSessionConfiguration *)sessionConfiguration;
//取消session,并取消下载
- (void)invalidateSessionAndCancel:(BOOL)cancelPendingOperations;
@end

SDWebImagerDownloader对外暴露的接口有点多,大致可以分为初始化,配置,下载,任务管理这几个部分,其主要的下载接口只有一个。

下面我们来看一下其.m文件的实现

SDWebImageDownloadToken

@interface SDWebImageDownloadToken ()

@property (nonatomic, weak, nullable) NSOperation *downloadOperation;

@end

@implementation SDWebImageDownloadToken

- (void)cancel {
    if (self.downloadOperation) {
        SDWebImageDownloadToken *cancelToken = self.downloadOperationCancelToken;
        if (cancelToken) {
            [self.downloadOperation cancel:cancelToken];
        }
    }
}
@end

首先是SDWebImageDownloadToken的cancel的实现,这是SDWebImageOperation的一个方法,在这里取消其下载的任务

初始化部分

//返回一个全局的实例
+ (nonnull instancetype)sharedDownloader {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}

- (nonnull instancetype)init {
    return [self initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
}
//在这里进行完整的初始化
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
    if ((self = [super init])) {
        _operationClass = [SDWebImageDownloaderOperation class];//配置默认的下载任务的类
        _shouldDecompressImages = YES;
        _executionOrder = SDWebImageDownloaderFIFOExecutionOrder;//配置下载策略
        _downloadQueue = [NSOperationQueue new];//创建下载队列
        _downloadQueue.maxConcurrentOperationCount = 6;
        _downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
        _URLOperations = [NSMutableDictionary new];//管理下载任务
        SDHTTPHeadersMutableDictionary *headerDictionary = [SDHTTPHeadersMutableDictionary dictionary];
        NSString *userAgent = nil;
#if SD_UIKIT
        //配置请求头的默认userAgent
         userAgent = [NSString stringWithFormat:@"%@/%@ (%@; iOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], [[UIScreen mainScreen] scale]];
#endif
        if (userAgent) {
            if (![userAgent canBeConvertedToEncoding:NSASCIIStringEncoding]) {
                NSMutableString *mutableUserAgent = [userAgent mutableCopy];
                if (CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)) {
                    userAgent = mutableUserAgent;
                }
            }
            headerDictionary[@"User-Agent"] = userAgent;
        }
#ifdef SD_WEBP
        //配置响应头的Accept
        headerDictionary[@"Accept"] = @"image/webp,image/*;q=0.8";
#else
        headerDictionary[@"Accept"] = @"image/*;q=0.8";
#endif
        //缓存响应头
        _HTTPHeaders = headerDictionary;
        //两把锁
        _operationsLock = dispatch_semaphore_create(1);
        _headersLock = dispatch_semaphore_create(1);
        //下载超时时间
        _downloadTimeout = 15.0;
         //配置session
        [self createNewSessionWithConfiguration:sessionConfiguration];
    }
    return self;
}
- (void)createNewSessionWithConfiguration:(NSURLSessionConfiguration *)sessionConfiguration {
    [self cancelAllDownloads];
    //先取消原来的session
    if (self.session) {
        [self.session invalidateAndCancel];
    }
    sessionConfiguration.timeoutIntervalForRequest = self.downloadTimeout;
    //这里并没有使用sharedsession得到全局的session
    self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
                                                 delegate:self
                                            delegateQueue:nil];
}

可以看出,这并不是一个完整的单例,如果使用sharedDownloader,则总会返回同一个Downloader,使用init或initWithSessionConfiguration创建则返回不同的Downloader。

createNewSessionWithConfiguration这个方法是一个对外暴露的接口,因此在配置前,需要先把原来的session取消掉,再配置新的session。

此外,既然session创建出来,那就得在Downloader释放后将其取消掉:

- (void)dealloc {
    [self.session invalidateAndCancel];
    self.session = nil;

    [self.downloadQueue cancelAllOperations];
}

此外,Downloader还i提供nvalidateSessionAndCancel是个外部可调用的借口。也就是说,取消的时机可由开发者控制

- (void)invalidateSessionAndCancel:(BOOL)cancelPendingOperations {
    //如果是全局的,则无法取消掉
    if (self == [SDWebImageDownloader sharedDownloader]) {
        return;
    }
    if (cancelPendingOperations) {
        //直接取消session,包括正在执行的任务
        [self.session invalidateAndCancel];
    } else {
        //取消session,但正在执行的任务会执行完成,之后的任务会被取消掉
        [self.session finishTasksAndInvalidate];
    }
}

配置部分

//配置请求头
- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field {
    LOCK(self.headersLock);
    if (value) {
        self.HTTPHeaders[field] = value;
    } else {
        [self.HTTPHeaders removeObjectForKey:field];
    }
    UNLOCK(self.headersLock);
}
//根据field获取相应的请求头信息
- (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field {
    if (!field) {
        return nil;
    }
    return [[self allHTTPHeaderFields] objectForKey:field];
}
//获取所有的请求头信息
- (nonnull SDHTTPHeadersDictionary *)allHTTPHeaderFields {
    LOCK(self.headersLock);
    SDHTTPHeadersDictionary *allHTTPHeaderFields = [self.HTTPHeaders copy];
    UNLOCK(self.headersLock);
    return allHTTPHeaderFields;
}
//设置队列的最大并发数
- (void)setMaxConcurrentDownloads:(NSInteger)maxConcurrentDownloads {
    _downloadQueue.maxConcurrentOperationCount = maxConcurrentDownloads;
}
//获取现在队列的并发数
- (NSUInteger)currentDownloadCount {
    return _downloadQueue.operationCount;
}
//获取队列的最大并发数
- (NSInteger)maxConcurrentDownloads {
    return _downloadQueue.maxConcurrentOperationCount;
}
//配置session的配置
- (NSURLSessionConfiguration *)sessionConfiguration {
    return self.session.configuration;
}
//设置下载任务的类
- (void)setOperationClass:(nullable Class)operationClass {
    if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperationInterface)]) {
        _operationClass = operationClass;
    } else {
        _operationClass = [SDWebImageDownloaderOperation class];
    }
}

这一部分都是暴露给外界的接口,实现也比较简单。

下载部分

首先是下载的核心方法

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    //如果url为空,则返回
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }
    
    LOCK(self.operationsLock);
    //按照url取出对应的任务(如果有)
    NSOperation *operation = [self.URLOperations objectForKey:url];

    if (!operation || operation.isFinished) {
        //如果任务已完成或者任务已经结束,则创建一个任务
        operation = [self createDownloaderOperationWithUrl:url options:options];
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            LOCK(sself.operationsLock);
            //任务结束后通知Downloader将自己移除
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        //加入缓存,并开始改任务
        [self.URLOperations setObject:operation forKey:url];
        [self.downloadQueue addOperation:operation];
    }
    UNLOCK(self.operationsLock);
    //下面的代码是为任务创建一个标识,
    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

这个方法给外界返回一个Token,外界可通过这个Token将任务取消掉
opeation创建的代码如下:

- (NSOperation *)createDownloaderOperationWithUrl:(nullable NSURL *)url                                                                       options:(SDWebImageDownloaderOptions)options {
    //设置超时时间
    NSTimeInterval timeoutInterval = self.downloadTimeout;
    if (timeoutInterval == 0.0) {
        timeoutInterval = 15.0;
    }
    //根据选项决定请求是否缓存
    NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                cachePolicy:cachePolicy
                                                            timeoutInterval:timeoutInterval];
    //根据选项决定是否使用cookies
    request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
    request.HTTPShouldUsePipelining = YES;
    //设置请求头,如果有过滤的函数,则调用
    if (self.headersFilter) {
        request.allHTTPHeaderFields = self.headersFilter(url, [self allHTTPHeaderFields]);
    }
    else {
        request.allHTTPHeaderFields = [self allHTTPHeaderFields];
    }
    //创建下载任务,这个任务是由operationClass创建的,而默认是SDWebImageDownloaderOperation
    NSOperation *operation = [[self.operationClass alloc] initWithRequest:request inSession:self.session options:options];
    operation.shouldDecompressImages = self.shouldDecompressImages;
    //配置凭证
    if (self.urlCredential) {
        operation.credential = self.urlCredential;
    } else if (self.username && self.password) {
        operation.credential = [NSURLCredential credentialWithUser:self.username password:self.password persistence:NSURLCredentialPersistenceForSession];
    }
    //配置高/低优先级
    if (options & SDWebImageDownloaderHighPriority) {
        operation.queuePriority = NSOperationQueuePriorityHigh;
    } else if (options & SDWebImageDownloaderLowPriority) {
        operation.queuePriority = NSOperationQueuePriorityLow;
    }
    //如果设置成后进先出,则添加任务的依赖关系以实现
    if (self.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
        [self.lastAddedOperation addDependency:operation];
        self.lastAddedOperation = operation;
    }

    return operation;
}

在最后可以看出其后进先出的实现方式,这里我觉得是有些问题的:

  • 从下载的代码可以看出,任务一旦创建马上就添加到队列里面,一旦开始执行了,就算存在新的任务,并为原来的任务添加依赖,那还是会继续执行的,新来的任务该等还是得等,并不能实现后进先出的效果
  • 这个下载队列默认有6条并发线程,但是一旦这种依赖关系建立起来,在下载任务繁多的时候,任务就只能一个接一个地执行了,相当于一条串行队列

任务管理部分

//按照token取消下载
- (void)cancel:(nullable SDWebImageDownloadToken *)token {
    NSURL *url = token.url;
    if (!url) {
        return;
    }
    LOCK(self.operationsLock);
    NSOperation *operation = [self.URLOperations objectForKey:url];
    if (operation) {
        BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
        if (canceled) {
            [self.URLOperations removeObjectForKey:url];
        }
    }
    UNLOCK(self.operationsLock);
}
//队列挂起
- (void)setSuspended:(BOOL)suspended {
    self.downloadQueue.suspended = suspended;
}
//取消所有任务
- (void)cancelAllDownloads {
    [self.downloadQueue cancelAllOperations];
}

这部分也是比较简单,不在赘述

URLSession协议部分

SDWebImageDownloader也遵循了URLSession的代理协议,但是,在协议的回调中,交给对应的operation去执行

//通过task获取对应的operation
- (NSOperation *)operationWithTask:(NSURLSessionTask *)task {
    NSOperation *returnOperation = nil;
    for (NSOperation *operation in self.downloadQueue.operations) {
        if ([operation respondsToSelector:@selector(dataTask)]) {
            if (operation.dataTask.taskIdentifier == task.taskIdentifier) {
                returnOperation = operation;
                break;
            }
        }
    }
    return returnOperation;
}

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    NSOperation *dataOperation = [self operationWithTask:dataTask];
    if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)]) {
        [dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
    } else {
        if (completionHandler) {
            completionHandler(NSURLSessionResponseAllow);
        }
    }
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    NSOperation *dataOperation = [self operationWithTask:dataTask];
    if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]) {
        [dataOperation URLSession:session dataTask:dataTask didReceiveData:data];
    }
}

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
    NSOperation *dataOperation = [self operationWithTask:dataTask];
    if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:willCacheResponse:completionHandler:)]) {
        [dataOperation URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
    } else {
        if (completionHandler) {
            completionHandler(proposedResponse);
        }
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {

    NSOperation *dataOperation = [self operationWithTask:task];
    if ([dataOperation respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
        [dataOperation URLSession:session task:task didCompleteWithError:error];
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {

    NSOperation *dataOperation = [self operationWithTask:task];
    if ([dataOperation respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)]) {
        [dataOperation URLSession:session task:task willPerformHTTPRedirection:response newRequest:request completionHandler:completionHandler];
    } else {
        if (completionHandler) {
            completionHandler(request);
        }
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {

    NSOperation *dataOperation = [self operationWithTask:task];
    if ([dataOperation respondsToSelector:@selector(URLSession:task:didReceiveChallenge:completionHandler:)]) {
        [dataOperation URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler];
    } else {
        if (completionHandler) {
            completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
        }
    }
}

可以看出,上面的实现都一样的,基本都是先看看有没有对应的operation,没有就走默认的回调方法。

心得

原本以为只是下载部分的代码会简单一些,没想到居然花了2周来看,在这期间补了好多知识点,虽然还有一些我没有看懂的地方,但这种下载图片的思想和多线程的使用还是值得我们去学习的。有空再来补一下它的其它部分吧。

你可能感兴趣的:(SDWebImage网络部分实现源码解析)