美文网首页程序员技术栈iOS面试总结
SDWebImage 360°无死角分析之下载

SDWebImage 360°无死角分析之下载

作者: 王技术 | 来源:发表于2019-05-16 11:52 被阅读2次

    打算用几篇文章整理一下 SDWebImage 的源码
    源码有点小多, 决定把每个模块分开来整理
    这其中包括 : 调度模块、下载模块、缓存模块、解码模块和一些代码整理
    调度模块看这里
    缓存模块看这里
    下载模块看这里
    解码模块看这里
    整理模块看这里


    本文是下载模块

    下载模块由 SDWebImageDownloader 管理
    SDWebImageDownloader 是以单例存在
    作用是创建 NSURLSession, 处理所有下载任务的回调并分发给对应的 SDWebImageDownloaderOperation
    也对图片下载管理进行一些全局的配置, 比如:
    1).设置最大并发数,下载时间默认15秒,是否压缩图片和下载顺序等。
    2).设置operation的请求头信息,负责生成每个图片的下载单位 SDWebImageDownloaderOperation 以及取消operation 等。
    3).设置下载的策略SDWebImageDownloaderOptions。

    直接看核心方法 :
    在调度一文 中的 SDWebImageManager 的 Load 方法中
    SDImageCache 在缓存中没有找到图片
    那么就会来到 SDWebImageDownloader 的核心方法
    去下载图片

    - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                       options:(SDWebImageDownloaderOptions)options
                                                      progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                     completed:(nullableSDWebImageDownloaderCompletedBlock)completedBlock
    
    

    四个参数很简单
    分别是 : 下载链接 下载策略 进度回调 完成回调
    代码注解如下

    - (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);
        // 先尝试在 URLOperations 中取下载任务, 有一种情况是操作可能被标记为已完成,但未从“self.URLOperations”中删除。
        /** 获取的是一个 SDWebImageDownloaderOperation  对象 */ 
        NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
        if (!operation || operation.isFinished) {
            //  crucial moment ! 关键时刻 ! 创建图片下载任务!
            operation = [self createDownloaderOperationWithUrl:url options:options];
            __weak typeof(self) wself = self;
            operation.completionBlock = ^{
                __strong typeof(wself) sself = wself;
                if (!sself) {
                    return;
                }
                LOCK(sself.operationsLock);
                [sself.URLOperations removeObjectForKey:url];
                UNLOCK(sself.operationsLock);
            };
            [self.URLOperations setObject:operation forKey:url];
            // 将 SDWebImageDownloaderOperation 加入队列开始下载任务
            [self.downloadQueue addOperation:operation];
        }
        UNLOCK(self.operationsLock);
    
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        
        // 生成下载 token 用于取消
        SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
        token.downloadOperation = operation;
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    
        return token;
    }
    

    首先根据下载 URL 去取下载任务
    如果下载任务不存在或者已完成, 就创建新的下载任务
    把下载任务加入任务队列后执行下载任务
    SDWebImageDownloadToken 用于取消任务

    创建 SDWebImageDownloaderOperation 下载任务的关键时刻是在这个方法中实现的 :
    其中包括一些网络请求的配置

    /** 创建下载任务 */
    - (NSOperation<SDWebImageDownloaderOperationInterface> *)createDownloaderOperationWithUrl:(nullable NSURL *)url
                                                                                      options:(SDWebImageDownloaderOptions)options {
        NSTimeInterval timeoutInterval = self.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }
    
        // 配置
        NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
        //创建request 设置请求缓存策略 下载时间
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                    cachePolicy:cachePolicy
                                                                timeoutInterval:timeoutInterval];
        
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        //设置请求头
        if (self.headersFilter) {
            request.allHTTPHeaderFields = self.headersFilter(url, [self allHTTPHeaderFields]);
        } else {
            request.allHTTPHeaderFields = [self allHTTPHeaderFields];
        }
        // 重头戏 在这里创建 SDWebImageDownloaderOperation 对象
        NSOperation<SDWebImageDownloaderOperationInterface> *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) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            [self.lastAddedOperation addDependency:operation];
            self.lastAddedOperation = operation;
        }
    
        return operation;
    }
    
    

    创建下载任务 SDWebImageDownloaderOperation 的时候, 传入了 self.session 也就是 NSURLSession
    SDWebImageDownloaderOperation 拿到这个 NSURLSession 创建 NSURLSessionTask 进行图片请求
    self.session 的创建是在 SDWebImageDownloader 单例初始化的时候创建的 :

    - (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;
            // 初始化 NSOperationQueue
            _downloadQueue = [NSOperationQueue new];
            // 最大并发出
            _downloadQueue.maxConcurrentOperationCount = 6;
            // 队列名称
            _downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
            // 下载url作为key value是具体的下载 operation 用字典来存储,方便cancel等操作
            _URLOperations = [NSMutableDictionary new];
            // HTTP请求头
            SDHTTPHeadersMutableDictionary *headerDictionary = [SDHTTPHeadersMutableDictionary dictionary];
            NSString *userAgent = nil;
    #if SD_UIKIT
            // User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
            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]];
    #elif SD_WATCH
            // User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
            userAgent = [NSString stringWithFormat:@"%@/%@ (%@; watchOS %@; 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], [[WKInterfaceDevice currentDevice] model], [[WKInterfaceDevice currentDevice] systemVersion], [[WKInterfaceDevice currentDevice] screenScale]];
    #elif SD_MAC
            userAgent = [NSString stringWithFormat:@"%@/%@ (Mac OS X %@)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[NSProcessInfo processInfo] operatingSystemVersionString]];
    #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
            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;
            // 创建 NSURLSession
            [self createNewSessionWithConfiguration:sessionConfiguration];
        }
        return self;
    }
    
    - (void)createNewSessionWithConfiguration:(NSURLSessionConfiguration *)sessionConfiguration {
        [self cancelAllDownloads];
        if (self.session) {
            [self.session invalidateAndCancel];
        }
        sessionConfiguration.timeoutIntervalForRequest = self.downloadTimeout;
        self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
                                                     delegate:self
                                                delegateQueue:nil];
    }
    

    也就是说 NSURLSession 的代理的是 SDWebImageDownloader
    所以所有图片下载任务的代理回调都由 SDWebImageDownloader 来处理
    SDWebImageDownloader 再把每个下载任务的回调分发给对应的 SDWebImageDownloaderOperation
    代码如下 :

    #pragma mark NSURLSessionDataDelegate
    /** 接收到服务器数据, 决定要不要进行 */
    - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response
                                            completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
        // 确定运行此任务的操作,并将委托方法传递给它, 确认回调的是哪个任务
        NSOperation<SDWebImageDownloaderOperationInterface> *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);
            }
        }
    }
    
    #pragma mark NSURLSessionTaskDelegate
    /** task已经完成. */
    - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
       // task 为唯一标识, 去查找对应的 operation
        NSOperation<SDWebImageDownloaderOperationInterface> *dataOperation = [self operationWithTask:task];
        if ([dataOperation respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
            [dataOperation URLSession:session task:task didCompleteWithError:error];
        }
    }
    

    SDWebImageDownloaderOperation

    SDWebImageDownloaderOperation 继承自 NSOperation 是具体的图片下载单位
    重写了 NSOperationstart 方法
    使用 SDWebImageDownloader 的 NSURLSession 创建 NSURLSessionTask 发起图片下载
    支持下载取消和后台下载
    在下载中及时汇报下载进度
    在下载成功后,对图片进行解码,缩放和压缩等操作

    SDWebImageDownloaderOperation 的初始化:
    - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                                  inSession:(nullable NSURLSession *)session
                                    options:(SDWebImageDownloaderOptions)options {
        if ((self = [super init])) {
            //  NSURLRequest
            _request = [request copy];
            //  图片是否可以被压缩属性
            _shouldDecompressImages = YES;
            //  下载策略
            _options = options;
            //  将进度 progressBlock 和下载结束 completedBlock 封装成字典 SDCallbacksDictionary
            _callbackBlocks = [NSMutableArray new];
            //  是否正在执行
            _executing = NO;
            //  是否完成
            _finished = NO;
            //  期望data的大小size
            _expectedSize = 0;
            //  SDWebImageDownloader 传入的 NSURLSession
            _unownedSession = session;
            //  信号量
            _callbacksLock = dispatch_semaphore_create(1);
            //  解码队列
            _coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);
        }
        return self;
    }
    
    start 方法

    SDWebImageDownloaderOperation 重写了 NSOperation 的 start 方法
    当任务添加到 NSOperationQueue 后会执行该方法,启动下载任务

    - (void)start {
        //添加同步锁,防止多线程数据竞争
        @synchronized (self) {
            if (self.isCancelled) {
                self.finished = YES;
                [self reset];
                return;
            }
    
    #if SD_UIKIT
            //如调用者配置了在后台可以继续下载图片,那么在这里继续下载
            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;
                    }
                }];
            }
    #endif
            NSURLSession *session = self.unownedSession;
            if (!session) {
                // 判断unownedSession是否为了nil,如果是nil则重新创建一个ownedSession
                /** SDWebImageDownloaderOperation 在 初始化 的时候已经传入了 session, 使用 sd 自带单例的时候不会遇到为空的情况 */
                NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
                sessionConfig.timeoutIntervalForRequest = 150;
                
                /**
                 *  Create the session for this task
                 *  We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
                 *  method calls and completion handler calls.
                 */
                session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                        delegate:self
                                                   delegateQueue:nil];
                self.ownedSession = session;
            }
            
            // NSURLCache
            if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
                // Grab the cached data for later check
                NSURLCache *URLCache = session.configuration.URLCache;
                if (!URLCache) {
                    URLCache = [NSURLCache sharedURLCache];
                }
                NSCachedURLResponse *cachedResponse;
                // NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483
                @synchronized (URLCache) {
                    cachedResponse = [URLCache cachedResponseForRequest:self.request];
                }
                if (cachedResponse) {
                    self.cachedData = cachedResponse.data;
                }
            }
            
            // 创建 task
            self.dataTask = [session dataTaskWithRequest:self.request];
            self.executing = YES;
        }
    
        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 {
            // dataTask 创建失败, 回调错误
            [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
            [self done];
            return;
        }
    
    #if SD_UIKIT
        // 后台下载
        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;
        }
    #endif
    }
    
    处理图片下载的数据回调

    start 方法执行后, 下载任务开启
    但是下载回调由 SDWebImageDownloader 接收, 然后分发给对应的 SDWebImageDownloaderOperation
    SDWebImageDownloaderOperation 接收到数据的处理 :
    省略了其他代理回调方法, 只看下载完成时的回调方法

    #pragma mark NSURLSessionTaskDelegate
    /** 下载完成或下载失败时的回调方法 */
    - (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];
                }
            });
        }
        
        // make sure to call `[self done]` to mark operation as finished
        if (error) {
            [self callCompletionBlocksWithError:error];
            [self done];
        } else {
            /** 图片下载结束的处理 */
            if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
                /**
                 *   如果指定使用`NSURLCache`,那么您在此处获得的响应就是所需要的。
                 */
                __block NSData *imageData = [self.imageData copy];
                if (imageData) {
                     / **
                     如果您指定仅通过`SDWebImageDownloaderIgnoreCachedResponse`使用缓存数据
                     然后我们应该检查缓存的数据是否等于图像数据
                     */
                    if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
                        // call completion block with nil
                        [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
                        [self done];
                    } else {
                        // decode the image in coder queue
                        dispatch_async(self.coderQueue, ^{
                            @autoreleasepool {
                                // 这一步是 data 转 UIImage => UIImage *image = [[UIImage alloc] initWithData:data];
                                UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
                                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                                // 处理一下三倍图和二倍图, 以图片名称中是否有 @2x 和 @3x 为标准
                                image = [self scaledImageForKey:key image:image];
                                
                                // 图片是否需要解码
                                BOOL shouldDecode = YES;
                                // 不强制解码动画GIF和WebP
                                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];
            }
        }
    }
    

    图片下载结束并解码后回调
    下载任务就完成了

    相关文章

      网友评论

        本文标题:SDWebImage 360°无死角分析之下载

        本文链接:https://www.haomeiwen.com/subject/kbhdoqtx.html