SDWebImage主线梳理(二)

篇幅过长,无奈只能分割成两篇。接续:SDWebImage主线梳理(一)


1-5-7-4-5-5 主线方法-网络请求

[SDWebImageDownloader requestImageWithURL:options:context:progress:completed:]

枚举值多选附带一个类似功能的枚举值

注意:方法的入参 completedBlock 就是1-5-7-4-5 实现的 LoaderCompletedBlock

- (id<SDWebImageOperation>)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock {
    UIImage *cachedImage = context[SDWebImageContextLoaderCachedImage];
    
    SDWebImageDownloaderOptions downloaderOptions = 0;
    if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
    if (options & SDWebImageProgressiveLoad) downloaderOptions |= SDWebImageDownloaderProgressiveLoad;
    if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
    if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
    if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
    if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
    if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
    if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
    if (options & SDWebImageAvoidDecodeImage) downloaderOptions |= SDWebImageDownloaderAvoidDecodeImage;
    if (options & SDWebImageDecodeFirstFrameOnly) downloaderOptions |= SDWebImageDownloaderDecodeFirstFrameOnly;
    if (options & SDWebImagePreloadAllFrames) downloaderOptions |= SDWebImageDownloaderPreloadAllFrames;
    if (options & SDWebImageMatchAnimatedImageClass) downloaderOptions |= SDWebImageDownloaderMatchAnimatedImageClass;
    
    if (cachedImage && options & SDWebImageRefreshCached) {
        // force progressive off if image already cached but forced refreshing
        downloaderOptions &= ~SDWebImageDownloaderProgressiveLoad;
        // ignore image read from NSURLCache if image if cached but force refreshing
        downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
    }
    // 主线,调用下载方法
    return [self downloadImageWithURL:url options:downloaderOptions context:context progress:progressBlock completed:completedBlock];
}



主线 - 下载方法的前传
-[SDWebImageDownloader downloadImageWithURL:options:context:progress:completed:]

注意:方法的入参 completedBlock 仍然是1-5-7-4-5 实现的 LoaderCompletedBlock,但是block类型变了,由原来的 SDImageLoaderCompletedBlock 类型,typedef 改成了 SDWebImageDownloaderCompletedBlock 类型,实际上还是同一种。typedef SDImageLoaderCompletedBlock SDWebImageDownloaderCompletedBlock;

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                   context:(nullable SDWebImageContext *)context
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    if (url == nil) {
        if (completedBlock) {
            NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
            completedBlock(nil, nil, error, YES);
        }
        return nil;
    }
    
    SD_LOCK(self.operationsLock);
    id downloadOperationCancelToken;
    // 默认 URLOperations 内是没有 operation 的,operation 为空
    NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
    // There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.
    if (!operation || operation.isFinished || operation.isCancelled) {
        // 创建一个 operation,详见 1-5-7-4-5-5-1
        operation = [self createDownloaderOperationWithUrl:url options:options context:context];
        // 创建 operation 出错,调用 completedBlock 回调错误信息
        if (!operation) {
            SD_UNLOCK(self.operationsLock);
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Downloader operation is nil"}];
                completedBlock(nil, nil, error, YES);
            }
            return nil;
        }
        // self 强引用 URLOperations,URLOperations 强引用 operation,因此 operation.completionBlock  内要使用弱引用
        @weakify(self);
        operation.completionBlock = ^{
            @strongify(self);
            if (!self) {
                return;
            }
            // 当 operation 完成动作后,要将其从 URLOperations 中移除
            SD_LOCK(self.operationsLock);
            [self.URLOperations removeObjectForKey:url];
            SD_UNLOCK(self.operationsLock);
        };
        // 创建完 operation 后如果不出意外则保存在 URLOperations(可变字典) 中
        self.URLOperations[url] = operation;
        // Add operation to operation queue only after all configuration done according to Apple's doc.
        // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
        // 一定是在设置好所需的配置之后,再把 operation 添加到操作队列中
        // addOperation: 操作不会同步的执行 operation.completionBlock,因此不会引起循环引用
        // 主线:添加到队列中operation就会开始执行,开始下载;详见 1-5-7-4-5-5-3
        [self.downloadQueue addOperation:operation]; 
        // 将 progressBlock 和 completedBlock 保存起来,等待下载(NSURLSession)的代理方法来调用,详见 1-5-7-4-5-5-2
        downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    } else {
        // When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue)
        // So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes.
        // 当重用 download operation 来附加更多 callbacks 的时候,由于 callbacks 的 getter 可能在其它的队列的原因可能会有线程安全问题 
        // 因此在这里给 operation 加锁,并在 SDWebImageDownloaderOperation 类中使用 @synchonzied (self),以确保这两个类的线程安全
        @synchronized (operation) {
            // 仍然参考 1-5-7-4-5-5-2
            downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        }
        // 如果 operation 未在执行,可以设置一下优先级
        if (!operation.isExecuting) {
            if (options & SDWebImageDownloaderHighPriority) {
                operation.queuePriority = NSOperationQueuePriorityHigh;
            } else if (options & SDWebImageDownloaderLowPriority) {
                operation.queuePriority = NSOperationQueuePriorityLow;
            } else {
                operation.queuePriority = NSOperationQueuePriorityNormal;
            }
        }
    }
    SD_UNLOCK(self.operationsLock);
    // 创建一个 SDWebImageDownloadToken 实例,传入 operation;详见 1-5-7-4-5-5-4
    SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
    token.url = url;
    token.request = operation.request;
    // token.downloadOperationCancelToken 是弱引用,SDWebImageDownloaderOperation 的 callbackBlocks 属性才是持有它的人
    // 可以参考 1-5-7-4-5-5-2
    token.downloadOperationCancelToken = downloadOperationCancelToken;
    
    return token;
}

1-5-7-4-5-5-1

-[SDWebImageDownloader createDownloaderOperationWithUrl:options:context:]
创建一个 downloader operation,SDWebImageDownloaderOperation 继承自 NSOperation 并遵循 SDWebImageDownloaderOperation 协议。

- (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url
                                                                                  options:(SDWebImageDownloaderOptions)options
                                                                                  context:(nullable SDWebImageContext *)context {
    NSTimeInterval timeoutInterval = self.config.downloadTimeout;
    // 如果开发者没有设置超时时间,则设置默认超时时间为15秒
    if (timeoutInterval == 0.0) {
        timeoutInterval = 15.0;
    }
    
    // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
    // 为了防止潜在的重复缓存(NSURLCache + SDImageCache),我们禁用了图像请求的缓存
    NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
     // 使用 url、cachePolicy、timeoutInterval 创建一个 NSMutableURLRequest 实例
    NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
    mutableRequest.HTTPShouldHandleCookies = SD_OPTIONS_CONTAINS(options, SDWebImageDownloaderHandleCookies);
    mutableRequest.HTTPShouldUsePipelining = YES;
    SD_LOCK(self.HTTPHeadersLock);
    // 设置请求头,默认只有 User-Agent 和 Accept 两个字段
    mutableRequest.allHTTPHeaderFields = self.HTTPHeaders;
    SD_UNLOCK(self.HTTPHeadersLock);
    
    // Context Option
    // 创建一个上下文,就是一个可变字典
    SDWebImageMutableContext *mutableContext;
    if (context) {
        mutableContext = [context mutableCopy];
    } else {
        mutableContext = [NSMutableDictionary dictionary];
    }
    
    // Request Modifier
    // self.requestModifier 默认为空,不展开解析
    id<SDWebImageDownloaderRequestModifier> requestModifier;
    if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) {
        requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier];
    } else {
        requestModifier = self.requestModifier;
    }
    
    NSURLRequest *request;
    if (requestModifier) {
        NSURLRequest *modifiedRequest = [requestModifier modifiedRequestWithRequest:[mutableRequest copy]];
        // If modified request is nil, early return
        if (!modifiedRequest) {
            return nil;
        } else {
            request = [modifiedRequest copy];
        }
    } else {
        // 拷贝一份 mutableRequest 赋值给 request
        request = [mutableRequest copy];
    }
    // Response Modifier
    // self.responseModifier 默认为空,不展开解析
    id<SDWebImageDownloaderResponseModifier> responseModifier;
    if ([context valueForKey:SDWebImageContextDownloadResponseModifier]) {
        responseModifier = [context valueForKey:SDWebImageContextDownloadResponseModifier];
    } else {
        responseModifier = self.responseModifier;
    }
    if (responseModifier) {
        mutableContext[SDWebImageContextDownloadResponseModifier] = responseModifier;
    }
    // Decryptor
    // self.decryptor 默认为空,不展开解析
    id<SDWebImageDownloaderDecryptor> decryptor;
    if ([context valueForKey:SDWebImageContextDownloadDecryptor]) {
        decryptor = [context valueForKey:SDWebImageContextDownloadDecryptor];
    } else {
        decryptor = self.decryptor;
    }
    if (decryptor) {
        mutableContext[SDWebImageContextDownloadDecryptor] = decryptor;
    }
    // 拷贝一份 mutableRequest 赋值给 request
    context = [mutableContext copy];
    
    // Operation Class
    //  self.config.operationClass 默认为空
    Class operationClass = self.config.operationClass;
    if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
        // Custom operation class
    } else {
        // 默认情况下使用 SDWebImageDownloaderOperation 这个类
        operationClass = [SDWebImageDownloaderOperation class];
    } 
    // 创建一个 SDWebImageDownloaderOperation 实例,详见1-5-7-4-5-5-1-1
    NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
    // credential 是协议 SDWebImageDownloaderOperation 的属性,一般没人实现其 setter(setCredential:) 也就不走这里
    if ([operation respondsToSelector:@selector(setCredential:)]) {
         // self.config.urlCredential 默认为空
        if (self.config.urlCredential) {
            operation.credential = self.config.urlCredential;
        } else if (self.config.username && self.config.password) { // 如你所料,username和password默认也是空
            operation.credential = [NSURLCredential credentialWithUser:self.config.username password:self.config.password persistence:NSURLCredentialPersistenceForSession];
        }
    }
    // minimumProgressInterval 是协议 SDWebImageDownloaderOperation 的属性,一般没人实现也就不走这里
    if ([operation respondsToSelector:@selector(setMinimumProgressInterval:)]) {
        operation.minimumProgressInterval = MIN(MAX(self.config.minimumProgressInterval, 0), 1);
    }
    // 为 operation 设置优先级
    if (options & SDWebImageDownloaderHighPriority) {
        operation.queuePriority = NSOperationQueuePriorityHigh;
    } else if (options & SDWebImageDownloaderLowPriority) {
        operation.queuePriority = NSOperationQueuePriorityLow;
    }
    // 下载操作的执行顺序,默认是先进先出,可以改为先进后出,但是仅仅靠这个选项不足以解决问题,还需要自己多处理一些。详见 SDWebImage 官方的单元测试案例
    if (self.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
        // Emulate LIFO execution order by systematically, each previous adding operation can dependency the new operation
        // This can gurantee the new operation to be execulated firstly, even if when some operations finished, meanwhile you appending new operations
        // Just make last added operation dependents new operation can not solve this problem. See test case #test15DownloaderLIFOExecutionOrder
        for (NSOperation *pendingOperation in self.downloadQueue.operations) {
            [pendingOperation addDependency:operation];
        }
    }
    
    return operation;
}

下载任务先进后出(扩展)

-[SDWebImageDownloaderTests test15DownloaderLIFOExecutionOrder]

Demo工程在SDWebImage官方GitHub下载即可

SDWebImageDownloaderTests.m

-[SDWebImageDownloaderTests test15DownloaderLIFOExecutionOrder]

1-5-7-4-5-5-1-1

-[SDWebImageDownloaderOperation initWithRequest:inSession:options:context:]
普普通通的一次初始化,保存了点参数

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options
                                context:(nullable SDWebImageContext *)context {
    if ((self = [super init])) {
        _request = [request copy];
        _options = options;
        _context = [context copy];
        _callbackBlocks = [NSMutableArray new];
        _responseModifier = context[SDWebImageContextDownloadResponseModifier];
        _decryptor = context[SDWebImageContextDownloadDecryptor];
        _executing = NO;
        _finished = NO;
        _expectedSize = 0;
        _unownedSession = session;
        _coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);
#if SD_UIKIT
        _backgroundTaskId = UIBackgroundTaskInvalid;
#endif
    }
    return self;
}

1-5-7-4-5-5-2

-[SDWebImageDownloaderOperation addHandlersForProgress:completed:] 请看仔细了

主要的两个事

  1. 此方法的入参,是前面的 ProgressBlockLoaderCompletedBlock。主线流程走到这里一直没有给前面实现的block回调,因为要等待网络请求的代理回调来处理。此方法彻底将这两个block封存起来待命。

  2. callbacks 被 self.callbackBlocks 持有,并且 callbacks 返回给 主线 - 下载方法的前传token.downloadOperationCancelToken(弱引用)

- (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];
    @synchronized (self) {
        [self.callbackBlocks addObject:callbacks];
    }
    return callbacks;
}

1-5-7-4-5-5-3 主线

当主线方法走到-[SDWebImageDownloader downloadImageWithURL:options:context:progress:completed:](前传) 的时候,发现流程好像断了。一直沿着调用链追踪到这里,忽然间就没有下文了(大神请忽略)。归根结底是我平时几乎不用 NSOperation 和 NSOperationQueue 的原因,不了解这一对神奇组合的妙处。这对神奇组合的用法我就不详细说了,网上多的很。

开启后续主线流程的关键在于[self.downloadQueue addOperation:operation];

看一下苹果官方文档的解释:
-[NSOperation addOperation:]

一旦添加,指定的操作将保留在队列中,直至完成执行。换句话说,添加即执行(注意:并不一定是同步)

operation 被添加到队列后,就开始执行 SDWebImageDownloaderOperation 的 start 方法。这是因为 SDWebImageDownloaderOperation 继承自 NSOperation,并且 SDWebImageDownloaderOperation 重写了 NSOperation 的 start 方法(为什么会执行 start 的具体原因请自行Google)。

巴拉巴拉说了这么多,其实就在说,下一个主线流程中的方法是:

-[SDWebImageDownloaderOperation start] 主线-下载方法的真身

  1. 这里呼应前面的下载方法的前传中提到的 callbacks 的 getter 线程安全问题,不仅仅是 start 方法,SDWebImageDownloaderOperation 所有方法只要涉及到 callbacks 的 getter 就会加上同步锁(@synchronized)。
  2. 主线正向流程到这里就结束了。至此,下载任务已经开始,接下来就是等待 NSURLSession 的代理回调了,请看第二部分重要Block的时间线-主线逆向流程
- (void)start {
    @synchronized (self) {
        // 如果操作被取消,走 if 分支
        if (self.isCancelled) {
            self.finished = YES;
            // Operation cancelled by user before sending the request
            // 回调错误信息,此方法涉及 callbacks 的 getter,详见 1-5-7-4-5-5-3-1
            [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil]];
            [self reset];
            return;
        }

#if SD_UIKIT
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        // shouldContinueWhenAppEntersBackground 默认为 NO,详见 1-5-7-4-5-5-3-2
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak typeof(self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                [wself cancel];
            }];
        }
#endif
        // unownedSession(weak) 和 ownedSession(strong) 详见 1-5-7-4-5-5-3-3
        NSURLSession *session = self.unownedSession;
        if (!session) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            
            /**
             *  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.
             我们传入nil当做代理的队列,为的是让session自动创建一个串行队列来执行所有代理方法和回调。
             */
            session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                    delegate:self
                                               delegateQueue:nil];
            self.ownedSession = session;
        }
        // SDWebImageDownloaderIgnoreCachedResponse:如果图片是从NSURLCache读取的,则回调nil回去;默认不选这个选项。
        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;
            }
        }
        // session 创建 dataTask
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }

    if (self.dataTask) {
        // 设置优先级,一般也没人设置这玩意
        if (self.options & SDWebImageDownloaderHighPriority) {
            self.dataTask.priority = NSURLSessionTaskPriorityHigh;
        } else if (self.options & SDWebImageDownloaderLowPriority) {
            self.dataTask.priority = NSURLSessionTaskPriorityLow;
        }
        // dataTask 任务开始
        [self.dataTask resume];
        // 第一次回调 ProgressBlock,这个block可有点久远了,就是前面 1-5 实现的 ProgressBlock
        // 不过改名了而已,1-5实现的 ProgressBlock 是 SDImageLoaderProgressBlock 类型,这里的 SDImageLoaderProgressBlock 是 typedef SDImageLoaderProgressBlock 而来的
        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];
        });
    } else {
        // 回调错误信息
        [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
        [self done];
    }
}

1-5-7-4-5-5-4

-[SDWebImageDownloadToken initWithDownloadOperation:]
又是一个普普通通的初始化,保存参数,并注册一个通知。

- (instancetype)initWithDownloadOperation:(NSOperation<SDWebImageDownloaderOperation> *)downloadOperation {
    self = [super init];
    if (self) {
        _downloadOperation = downloadOperation;
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadReceiveResponse:) name:SDWebImageDownloadReceiveResponseNotification object:downloadOperation];
    }
    return self;
}

这个通知是在监听NSURLSession的代理回调,当代理收到响应时,保存一份到 SDWebImageDownloadToken。

- (void)downloadReceiveResponse:(NSNotification *)notification {
    NSOperation<SDWebImageDownloaderOperation> *downloadOperation = notification.object;
    if (downloadOperation && downloadOperation == self.downloadOperation) {
        self.response = downloadOperation.response;
    }
}


1-5-8

-[UIView(WebCache) sd_setImage:imageData:basedOnClassOrViaCustomSetImageBlock:transition:cacheType:imageURL:]

这个方法就是单纯的在给控件(UIButton或UIImageView)设置图片,不论设置主图还是占位图都会调用这个方法。

#if SD_UIKIT || SD_MAC
- (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock transition:(SDWebImageTransition *)transition cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL {
    // 用 UIButton 和 UIImageView 的共同父类 UIView 指针来接收
    UIView *view = self;
    // SDSetImageBlock 是一个有参无返回值的block,参数为 UIImage、NSData、SDImageCacheType、NSURL
    SDSetImageBlock finalSetImageBlock;
    // setImageBlock 默认为空
    if (setImageBlock) {
        finalSetImageBlock = setImageBlock;
    } else if ([view isKindOfClass:[UIImageView class]]) { // 如果控件是 UIImageView 走此分支
        UIImageView *imageView = (UIImageView *)view;
        // finalSetImageBlock 保存一段代码(仅仅是给 UIImageView 设置图片),等待调用
        finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
            imageView.image = setImage;
        };
    }
#if SD_UIKIT
    // 如果控件是 UIButton 走此分支
    else if ([view isKindOfClass:[UIButton class]]) {
        UIButton *button = (UIButton *)view;
        // finalSetImageBlock 保存一段代码(仅仅是给 UIButton 设置前景图片),等待调用
        finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
            [button setImage:setImage forState:UIControlStateNormal];
        };
    }
#endif
// 这个宏的分支不用看
#if SD_MAC
    else if ([view isKindOfClass:[NSButton class]]) {
        NSButton *button = (NSButton *)view;
        finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
            button.image = setImage;
        };
    }
#endif
    // 如果图片有形变,处理形变。默认没有形变,因此不展开解析。
    if (transition) {
#if SD_UIKIT
        [UIView transitionWithView:view duration:0 options:0 animations:^{
            // 0 duration to let UIKit render placeholder and prepares block
            if (transition.prepares) {
                transition.prepares(view, image, imageData, cacheType, imageURL);
            }
        } completion:^(BOOL finished) {
            [UIView transitionWithView:view duration:transition.duration options:transition.animationOptions animations:^{
                if (finalSetImageBlock && !transition.avoidAutoSetImage) {
                    finalSetImageBlock(image, imageData, cacheType, imageURL);
                }
                if (transition.animations) {
                    transition.animations(view, image);
                }
            } completion:transition.completion];
        }];
#elif SD_MAC
        [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull prepareContext) {
            // 0 duration to let AppKit render placeholder and prepares block
            prepareContext.duration = 0;
            if (transition.prepares) {
                transition.prepares(view, image, imageData, cacheType, imageURL);
            }
        } completionHandler:^{
            [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) {
                context.duration = transition.duration;
                context.timingFunction = transition.timingFunction;
                context.allowsImplicitAnimation = SD_OPTIONS_CONTAINS(transition.animationOptions, SDWebImageAnimationOptionAllowsImplicitAnimation);
                if (finalSetImageBlock && !transition.avoidAutoSetImage) {
                    finalSetImageBlock(image, imageData, cacheType, imageURL);
                }
                if (transition.animations) {
                    transition.animations(view, image);
                }
            } completionHandler:^{
                if (transition.completion) {
                    transition.completion(YES);
                }
            }];
        }];
#endif
    } else { // 默认会走这个分支
        // 调用前面刚刚实现的 finalSetImageBlock 去给控件设置图片
        if (finalSetImageBlock) {
            finalSetImageBlock(image, imageData, cacheType, imageURL);
        }
    }
}
#endif


1-5-9

-[UIView(WebCache) sd_setImageLoadOperation:forKey:]

主要作用是保存 operation 到 sd_operationDictionary。

- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key {
    if (key) {
        [self sd_cancelImageLoadOperationWithKey:key]; // 调用 sd_cancelImageLoadOperationWithKey:,前面1-5-2已介绍过
        if (operation) {
            SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
            @synchronized (self) {
                // 保存 operation 到 sd_operationDictionary
                [operationDictionary setObject:operation forKey:key];
            }
        }
    }
}



第二部分-重要Block的时间线(主线逆向流程)

所谓主线逆向流程(还是我自己起的名)指的是从网络请求响应并回调开始,一直到把图片(UIImage)送到 UIImageView 为止。

SD的流程之所以显得有些复杂,个人觉得有大部分原因就是它的block。第一部分提到过的几个重点block,如果掌握了它们是在哪里实现,又是在哪里调用,那么就大致知道了SD的业务流程是怎么走的了。

第二部分并不是单纯的解析主线逆向流程,而是从头开始捋一捋重点的block,看看这些block从哪里来,要到哪里去。捎带着掺点主线逆向流程的解析,这样就差不多可以看到SD主线的全貌了。

先回顾一下第一部分中我们约定过的那些block的简称:

实现的位置 Block的类型 约定的简称
1-4 SDInternalCompletionBlock InternalBlock1
1-5 SDInternalCompletionBlock InternalBlock2
1-5 SDImageLoaderProgressBlock ProgressBlock
1-5-7-4-3-1 SDImageCacheQueryCompletionBlock doneBlock
1-5-7-4-5 SDWebImageDownloaderCompletedBlock LoaderCompletedBlock

再约定几个词的含义:

  1. 被动等待xx:意思是这部分代码是编号xx的block的实现中的一部分,要等编号xx的block被调用,才会走
  2. 条件性触发:总之不是主线流程,比如返回Error或者选了某些不常用的选项的时候要走的分支
  3. 单次触发:在整个网络请求过程中,仅触发一次

注:罗马数字序号表示方法调用的顺序。一个罗马数字对应一个方法,而下面用阿拉伯数字标注的则是这个方法的各个关键点

------------------------------------------------------Block时间线:第一弹------------------------------------------------------

  • -[UIImageView(WebCache) sd_setImageWithURL:placeholderImage:options:context:progress:completed:]
    1. 实现了 sd_internalSetImageWithURL: 的completedBlock (InternalBlock1)

  • -[UIView(WebCache) sd_internalSetImageWithURL:placeholderImage:options:context:setImageBlock:progress:completed:]
    1. 实现了 combinedProgressBlock (ProgressBlock) 并传递给了[manager loadImageWithURL:...] 方法
    2. 实现了 [manager loadImageWithURL:...] 的completedBlock (InternalBlock2)。
    3. 被动等待3, InternalBlock2 中有两次条件性触发 InternalBlock1

  • -[SDWebImageManager loadImageWithURL:options:context:progress:completed:]
    1. 条件性(url长度为零等)触发自己的completedBlock (InternalBlock2),会联动触发 InternalBlock1。
    2. ProgressBlock 和 InternalBlock2 不调用,传递给 -[SDWebImageManager callCacheProcessForOperation:...] 方法。

  • -[SDWebImageManager callCacheProcessForOperation:url:options:context:progress:completed:]
    1. 实现了[self.imageCache queryImageForKey:...]的 doneBlock。
    2. 被动等待7,completionBlock 中条件性触发 [manager loadImageWithURL:...] 的completedBlock (InternalBlock2)
    3. 被动等待7,completionBlock 实现代码会触发 [self callDownloadProcessForOperation:...]
    4. 被动等待7,ProgressBlock 和 InternalBlock2 仍然不调用,接下来传递给 [self callDownloadProcessForOperation:...]

  • -[SDImageCache(SDImageCache) queryImageForKey:options:context:completion:]

  • -[SDImageCache queryCacheOperationForKey:options:context:done:]
    1. 条件性(key为空)触发 doneBlock 回调。
    2. 条件性(image和options)触发 doneBlock 回调。
    3. ❗️❗️❗️触发 doneBlock。条件性 (shouldQueryDiskSync) 同步或异步触发局域block,局域block中再次判断DiskSync来选择同步或异步触发doneBlock,但是不管走哪个分支,都会触发doneBlock。

OK,在这里小结一下。走到 Ⅵ 触发了 doneBlock,开始往回走:13 -> 7 -> 9,9 触发了 Ⅶ

------------------------------------------------------Block时间线:第二弹------------------------------------------------------

  • -[SDWebImageManager callDownloadProcessForOperation:url:options:context:cachedImage:cachedData:cacheType:progress:completed:]
    1. 条件性触发 InternalBlock2
    2. 实现了的 LoaderCompletedBlock。 继续将 ProgressBlock 传递给[self.imageLoader requestImageWithURL:...]
    3. 被动等待15,LoaderCompletedBlock 三次条件性触发 InternalBlock2
    4. 被动等待15,LoaderCompletedBlock 触发 [SDWebImageManager callStoreCacheProcessForOperation:...]。 InternalBlock2仍然不调用,继续传递给 [SDWebImageManager callStoreCacheProcessForOperation:...]

  • -[SDWebImageDownloader requestImageWithURL:options:context:progress:completed:]

  • -[SDWebImageDownloader downloadImageWithURL:options:context:progress:completed:]
    1. 条件性(url为空;另一个是operation创建失败)触发自己的 completedBlock (LoaderCompletedBlock)
    2. [self.downloadQueue addOperation:operation] 添加即执行(异步); 下载任务开始执行
    3. 触发 [operation addHandlersForProgress:completed:], 将 ProgressBlock 和 LoaderCompletedBlock 传递进去

  • -[SDWebImageDownloaderOperation addHandlersForProgress:completed:]
    1. 尼玛!ProgressBlock 和 LoaderCompletedBlock 被封存到一个可变字典中,待命
    2. 字典被保存到 SDWebImageDownloaderOperation 的 callbackBlocks(可变数组) 属性中

XI

  • -[SDWebImageDownloaderOperation start]
    1. ❗️单次触发 ProgressBlock,携带参数(0, -1, 真实URL),执行一次 2(即II-2) 的block实现。

小结:在Ⅹ(罗马数字10) 的方法中,ProgressBlock 和 LoaderCompletedBlock 彻底被封存待命了,只能等网络请求发起后,NSURLSession 的代理回调来触发。start 方法开启了网络请求,接下来就是 NSURLSession 回调部分。

------------------------------------------------------Block时间线:第三弹------------------------------------------------------

XII - NSURLSession 回调

为了不打断连贯性,从 NSURLSession 回调开始的每个方法的实现源码在后面进行解析。

  • -[SDWebImageDownloaderOperation URLSession:task:didReceiveChallenge:completionHandler:]
    证书信任挑战

XIII

  • -[SDWebImageDownloaderOperation URLSession:dataTask:didReceiveResponse:completionHandler:]
    服务端第一次响应
    1. ❗️单次触发 ProgressBlock,携带参数(0, 图片预期大小, 真实URL),执行一次 2 的block实现

XIV

  • -[SDWebImageDownloaderOperation URLSession:dataTask:didReceiveData:]
    接收数据
    1. ❗️❗️❗️触发 ProgressBlock ,携带参数(实际接收大小, 图片预期大小, 真实URL)

XV

  • -[SDWebImageDownloaderOperation URLSession:dataTask:willCacheResponse:completionHandler:]
    下载已经结束,在正式回调结束前可以处理缓存响应

XVI

  • -[SDWebImageDownloaderOperation URLSession:task:didCompleteWithError:]
    1. 涉及的解码部分请参考另外一篇文章
    2. 触发 callCompletionBlocksWithImage:imageData:error:finished:

XVII

  • -[SDWebImageDownloaderOperation callCompletionBlocksWithImage:imageData:error:finished:]
    1. ❗️❗️❗️异步主队列触发 LoaderCompletedBlock

小结:28 触发 LoaderCompletedBlock,接下来的触发顺序是 28 -> 15 -> 17,17 触发保存缓存的方法callStoreCacheProcessForOperation

------------------------------------------------------Block时间线:第四弹------------------------------------------------------

XVIII

  • -[SDWebImageManager callStoreCacheProcessForOperation(url,options,context,downloadedImage,downloadedData,finished,progress,completed)]
    1. 保存图片,请参考另外一篇文章
    2. ❗️❗️❗️触发 InternalBlock2,执行 3 的block实现,给 imageView设置图片。全剧终~

------------------------------------------------------Block时间线:第五弹------------------------------------------------------

XII 解析

-[SDWebImageDownloaderOperation URLSession:task:didReceiveChallenge:completionHandler:]
证书信任挑战,调用block回传Disposition给系统。一般情况不需要处理,但是如果你们服务器是自签名SSL证书,那就需要处理。具体怎么弄请自行google&baidu。

- (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);
    }
}

XIII 解析

-[SDWebImageDownloaderOperation URLSession:dataTask:didReceiveResponse:completionHandler:]
服务器收到请求率先返回响应,在这里可以预先获取状态码和数据的预期大小。

  1. 默认不需要 responseModifier,忽略。
  2. response.expectedContentLength 可从 response 获取预期大小。
  3. response.statusCode 可从 response 获取状态码。
  4. SD认为状态码在 [200,400) 区间为有效,但是这个区间的304是个特例。
    • “304 Not Modified”,当服务器返回304状态码并且 URLCache 命中,SD将会视为 200 处理。
    • 这不是标准行为,而是SD这样处理。
  5. 异步拉回主队列通知全村老少(例如:1-5-7-4-5-5-4)已经收到响应。
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;
    // Check response modifier, if return nil, will marked as cancelled.
    BOOL valid = YES;
    if (self.responseModifier && response) {
        response = [self.responseModifier modifiedResponseWithResponse:response];
        if (!response) {
            valid = NO;
            self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadResponse userInfo:nil];
        }
    }
    
    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;
    // Status code should between [200,400)
    BOOL statusCodeValid = statusCode >= 200 && statusCode < 400;
    if (!statusCodeValid) {
        valid = NO;
        self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadStatusCode userInfo:@{SDWebImageErrorDownloadStatusCodeKey : @(statusCode)}];
    }
    //'304 Not Modified' is an exceptional one
    //URLSession current behavior will return 200 status code when the server respond 304 and URLCache hit. But this is not a standard behavior and we just add a check
    if (statusCode == 304 && !self.cachedData) {
        valid = NO;
        self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:nil];
    }
    
    if (valid) {
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, expected, self.request.URL);
        }
    } else {
        // Status code invalid and marked as cancelled. Do not call `[self.dataTask cancel]` which may mass up URLSession life cycle
        disposition = NSURLSessionResponseCancel;
    }
    __block typeof(self) strongSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:strongSelf];
    });
    
    if (completionHandler) {
        completionHandler(disposition);
    }
}

XIV 解析

-[SDWebImageDownloaderOperation URLSession:dataTask:didReceiveData:]

  1. 一般情况,此代理方法会被系统多次调用;
  2. 按照预期大小初始化一个 NSMutableData,每次返回一点数据就拼接一点,直到完整;
  3. 每接收到一次数据,就按照真实进度调用 progressBlock 回传一次,直到进度100%;
  4. minimumProgressInterval:按照百分比间隔限制回调的频次;例如每增加10%回调一次。
  5. supportProgressive 有关渐进式解码的部分不展开解析。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    if (!self.imageData) {
        self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
    }
    [self.imageData appendData:data];
    
    self.receivedSize = self.imageData.length;
    if (self.expectedSize == 0) {
        // Unknown expectedSize, immediately call progressBlock and return
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
        }
        return;
    }
    
    // Get the finish status
    BOOL finished = (self.receivedSize >= self.expectedSize);
    // Get the current progress
    double currentProgress = (double)self.receivedSize / (double)self.expectedSize;
    double previousProgress = self.previousProgress;
    double progressInterval = currentProgress - previousProgress;
    // Check if we need callback progress
    if (!finished && (progressInterval < self.minimumProgressInterval)) {
        return;
    }
    self.previousProgress = currentProgress;
    
    // Using data decryptor will disable the progressive decoding, since there are no support for progressive decrypt
    BOOL supportProgressive = (self.options & SDWebImageDownloaderProgressiveLoad) && !self.decryptor;
    if (supportProgressive) {
        // Get the image data
        NSData *imageData = [self.imageData copy];
        
        // progressive decode the image in coder queue
        dispatch_async(self.coderQueue, ^{
            @autoreleasepool {
                UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, finished, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
                if (image) {
                    // We do not keep the progressive decoding image even when `finished`=YES. Because they are for view rendering but not take full function from downloader options. And some coders implementation may not keep consistent between progressive decoding and normal decoding.
                    
                    [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
                }
            }
        });
    }
    
    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
        progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
    }
}

XV 解析

-[SDWebImageDownloaderOperation URLSession:dataTask:willCacheResponse:completionHandler:]

  1. 下载已结束的标志,准备缓存响应体
  2. 如果没有选择 SDWebImageDownloaderUseNSURLCache,清空cachedResponse;默认就是不选
    注:completionHandler() 回调 cachedResponse,你猜是发送给谁的? --系统
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
    NSCachedURLResponse *cachedResponse = proposedResponse;

    if (!(self.options & SDWebImageDownloaderUseNSURLCache)) {
        // Prevents caching of responses
        cachedResponse = nil;
    }
    if (completionHandler) {
        completionHandler(cachedResponse);
    }
}

XVI 解析

-[SDWebImageDownloaderOperation URLSession:task:didCompleteWithError:]

  1. [self.decryptor decryptedDataWithData:imageData response:self.response]
    • 首先,decryptor 这玩意是开发者自定的东西,并不是默认的
    • 其次,decryptedDataWithData:response: 并不是真在解码,而是在调用开发者实现的(传入的)block。相当于是允许开发者自定义图片解码规则。
  2. 如果你指定了 SDWebImageDownloaderIgnoreCachedResponse,不但不使用URLCache,如果本地cachedData和URLCache一样的话也要被抹杀,回传nil
  3. dispatch_async + self.coderQueue :异步+串行队列用来保证线程安全
  4. @autoreleasepool{} 添加自动释放池,加快局部变量释放,减少内存占用时间
  5. SDImageLoaderDecodeImageData() 真正的解码函数,详见另外一篇文章
  6. 宽为0或者高为0都视为出错,返回错误信息
  7. 最后调用 callCompletionBlocksWithImage:imageData:error:finished: 回传解码后的 UIImage 和原始的 imageData
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    // If we already cancel the operation or anything mark the operation finished, don't callback twice
    if (self.isFinished) return;
    
    @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];
            }
        });
    }
    
    // make sure to call `[self done]` to mark operation as finished
    if (error) {
        // custom error instead of URLSession error
        if (self.responseError) {
            error = self.responseError;
        }
        [self callCompletionBlocksWithError:error];
        [self done];
    } else {
        if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
            NSData *imageData = [self.imageData copy];
            self.imageData = nil;
            // data decryptor
            if (imageData && self.decryptor) {
                imageData = [self.decryptor decryptedDataWithData:imageData response:self.response];
            }
            if (imageData) {
                /**  if you specified to only use cached data via `SDWebImageDownloaderIgnoreCachedResponse`,
                 *  then we should check if the cached data is equal to image data
                 */
                if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
                    self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:nil];
                    // call completion block with not modified error
                    [self callCompletionBlocksWithError:self.responseError];
                    [self done];
                } else {
                    // decode the image in coder queue
                    dispatch_async(self.coderQueue, ^{
                        @autoreleasepool {
                            UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
                            CGSize imageSize = image.size;
                            if (imageSize.width == 0 || imageSize.height == 0) {
                                [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData 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:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
                [self done];
            }
        } else {
            [self done];
        }
    }
}

XVII 解析

-[SDWebImageDownloaderOperation callCompletionBlocksWithImage:imageData:error:finished:]

  1. 从 self.callbackBlocks 中取出 LoaderCompletedBlock,读取时须保证线程安全
  2. 同步或异步在主队列依次调用各个 LoaderCompletedBlock 回传数据
- (void)callCompletionBlocksWithImage:(nullable UIImage *)image
                            imageData:(nullable NSData *)imageData
                                error:(nullable NSError *)error
                             finished:(BOOL)finished {
    NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
    dispatch_main_async_safe(^{
        for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
            completedBlock(image, imageData, error, finished);
        }
    });
}

- (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
    NSMutableArray<id> *callbacks;
    @synchronized (self) {
        callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
    }
    // We need to remove [NSNull null] because there might not always be a progress block for each callback
    [callbacks removeObjectIdenticalTo:[NSNull null]];
    return [callbacks copy]; // strip mutability here
}



第三部分-读源码遇到的问题

  • Q: SDImageCacheOptions:SDImageCacheDecodeFirstFrameOnly,注释说SD默认会解码动图。然而函数SDImageCacheDefine:SDImageCacheDecodeImageData()中却明确写到 SDAnimatedImage do not decode,animated image do not decode。为什么一会说解码动图一会说不解码动图?
  • A: 根据其它选项SDImageCachePreloadAllFrames猜测,默认情况下动图是在渲染期间进行解码,为的是减少内存使用率。不是不解码,是不手动解码,让系统来解码。

  • Q: SDExternalCompletionBlock 在哪里实现的?
  • A:  调用位置:-[UIImageView(WebCache) sd_setImageWithURL:],在sd_internalSetImageWithURL 的回调中
      实现位置:没人实现,或者开发者实现

  • Q: SDWebImageDownloaderNSURLSession以及SDWebImageDownloaderOperation的联系
  • A: manager持有downloader的单例;downloader创建并持有session和operation,downloader把自己的session传递给operation
    1. -[UIView sd_internalSetImageWithURL...] 调用 +[SDWebImageManager sharedManager]
    2. +[SDWebImageManager sharedManager] 调用 -[self new]-[SDWebImageManager init]
    3. -[SDWebImageManager init] 调用 +[SDWebImageDownloader sharedDownloader] 创建 Downloader,并保存到 imageLoader 属性中

    4. -[SDWebImageDownloader sharedDownloader] 调用 -[self new]-[SDWebImageDownloader init]
    5. -[SDWebImageDownloader init] 调用 -[SDWebImageDownloader initWithConfig:]
    6. -[SDWebImageDownloader initWithConfig:]创建NSURLSession,self (SDWebImageDownloader)设置为 session 的delegate,并保存到属性 session 中

    7. -[SDWebImageDownloader downloadImageWithURL:...] 调用 -[SDWebImageDownloader createDownloaderOperationWithUrl:...]
    8. -[createDownloader...] 调用 -[SDWebImageDownloaderOperation initWithRequest:inSession:options:context:] 把 self.session 传递给 operation,创建operation并保存到 SDWebImageDownloader 的属性 URLOperations 中

  • Q: downloader 和 operation 都实现了 NSURLSession 的 NSURLSessionDataDelegate 和 NSURLSessionTaskDelegate 这两组代理方法,两个类具体是怎么分工合作的?
  • A: 事实上 downloader 才是 session 真正的 delegate,也就是说 session 的代理回调会通知 downloader 而不会通知 operation。operation 也实现了代理方法,是 downloader 在代理回调方法中主动触发 operation 的方法,可以理解为 downloader 将 session 发来的代理事件转发给了 operation。

  • Q: 在 downloader 中为什么可以直接调用 operation 的私有方法?
  • A: 按常理讲,operation 虽然实现了 NSURLSession 的代理方法,但是这些方法是私有方法,downloader 持有的 operation 应该不能在外部直接调用才对。但是呢,因为 operation 遵守的协议 <SDWebImageDownloaderOperation>是继承了 NSURLSession 那两组协议的,正是利用了协议的特点,所以才可以直接调用。

  • Q: 为什么要实现两次 NSURLSession 的代理方法?
  • A: 个人理解,Operation 有俩属性分别是 unownedSession(weak) 和 ownedSession(strong)。unownedSession 是弱引用别人注入的 session,Operation 不负责代理事件,需要 downloader 管理并转发过来。ownedSession 是没有人注入 session 时,Operation 自己创建一个 session 且强引用,并自己管理代理事件。

  • Q: 关于 SDWebImageCombinedOperation *operation 我想说的
  • A: 详解如下
    1. CombinedOperation 在-[SDWebImageManager loadImageWithURL] 方法中被初始化,manager 的 runningOperations(NSMutableSet) 属性持有 CombinedOperation
    2. CombinedOperation 的 manager 属性弱引用 manager
    3. CombinedOperation 是一个合并的 Operation 而非具体 Operation,这点有点类似 runloop 模式中的 NSRunLoopCommonModes 的玩法。CombinedOperation 通过属性强引用 id<SDWebImageOperation> cacheOperationid<SDWebImageOperation> loaderOperation
    4. operation.cacheOperation 来自于 -[SDImageCache queryImageForKey] 的返回值, queryImageForKey 仅仅是创建一个 NSOperation 就返回
    5. operation.loaderOperation 来自于 -[SDWebImageDownloader requestImageWithURL] 的返回值
    6. UIView(WebCacheOperation) 有个关联属性 sd_operationDictionary 弱引用此 Operation
    7. 详解 operation.loaderOperation
      1. 这里的loader其实是 SDWebImageDownloader, 入口方法是 -[SDWebImageDownloader requestImageWithURL]
      2. 然后调用 downloadImageWithURL,调用 createDownloaderOperationWithUrl 创建 SDWebImageDownloaderOperation
      3. 然后调用 -[SDWebImageDownloadToken initWithDownloadOperation:] 传入 operation,创建 SDWebImageDownloadToken,返回实例
      4. SDWebImageDownloadToken 弱引用 SDWebImageDownloaderOperation
      5. 因此 loaderOperation 属性强引用的是 SDWebImageDownloadToken
      6. SDWebImageDownloader 的 URLOperations(NSMutableDictionary) 属性才是真正强引用 SDWebImageDownloaderOperation 实例的男人

第四部分-SDWebImage小知识点

SDWebImage 主要的解析已经全部完成了,这一部分纯属个人喜好,想记录点自己在 SD 学到的小知识点。

  1. 由于SD很多功能模块建立在类目之上,因此用了大量的关联属性;

  1. SD用了大量的协议进行解耦,使得架构清晰简洁,又易于维护迭代;

  1. SDWebImageManager 其实可以直接单独用:
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager loadImageWithURL:imageURL
                  options:0
                 progress:nil
                completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                    if (image) {
                        // do something with image
                    }
                }];

  1. SDImageTransformer,如果用到了形变类,那么key会被组装成:
`image.png` |> flip(YES,NO) |> rotate(pi/4,YES) => 'image-SDImageFlippingTransformer(1,0)-SDImageRotationTransformer(0.78539816339,1).png'

  1. result = MAX(MIN(progress, 1), 0),无论process是多少,都能保证result在0-1之间。同样是 <=1 和 >=0,SD写的就是那么狂拽炫酷;

  1. 与AFNetworking同样的玩法,在需要一个 void * 作为标识符的时候,可以这么玩,指针指向指针自己的地址;
// 定义
static void * SDMemoryCacheContext = &SDMemoryCacheContext;
// 使用
[_config removeObserver:self forKeyPath:NSStringFromSelector(@selector(maxMemoryCost)) context:SDMemoryCacheContext];

关于这个写法我一开始有些疑问。因为平常开发不会经常去纠结指针的初始化,所以当初在读SD源码的时候脑子里一直在问:

  • 先创建 SDMemoryCacheContext 指针还是先做赋值运算?
  • SDMemoryCacheContext 到底有没有被初始化?
  • SDMemoryCacheContext 会不会指向随机或者无效的地址?

正常来讲,AFN和SD都这么写,大概率是不会有问题的。所以我自己验证了一下,以下是验证流程:

void *p;
NSString *a;

NSLog(@"指针p指向的地址:%p", p);
NSLog(@"指针p自己的地址:%p", &p);

NSLog(@"指针a指向的地址:%p", a);
NSLog(@"指针a自己的地址:%p", &a);

这个例子中,p未被初始化,但是p已经被分配了地址。换句话说,p指针指向随机或无效地址,但p自己的地址是固定且有效的;a被初始化了,a指向的地址为0x0,就是nil,a自己的地址也是固定有效的。

void *p = &p;

这么写就是系统先为p指针分配了有效地址,然后取地址操作 &p,然后赋值操作。

小结:C指针不会被自动初始化,OC指针会被自动初始化。虽然一开始p指针没有被初始化,指向了随机地址。但是p指针自己的地址是有效的,于是取自己的地址赋值给自己。


图解.png