美文网首页
SDWebImage4.4.2 简单分析

SDWebImage4.4.2 简单分析

作者: saige2011 | 来源:发表于2018-08-18 17:44 被阅读0次

    先简单看一下 Masonry 主要的设计以及 Class 结构方法

    top

    调用时序图


    top2

    整体的关键步骤是这样的:

    1. UIImageView 通过 SDWebImage 请求一个 URL 获取图片
    2. SDWebImage 根据这个 URL 先去 内存中寻找,如果找不到去硬盘中寻找(这里忽略一些 ignore cache 的 case)
    3. 如果第二步再找不到,SDWebImage 会先检查之前是否有这个 URL 正在下载,之后创建一个 NSMutableURLRequest: request
    4. 将第三步的 request 封装成一个SDWebImageDownloaderOperation(subclass of NSOperation) operation
    5. 将第四步的 operation 根据 option 的配置,加入到队列中(提供先进先出以及先进后出两种)
    6. operation 线程开始,开启 第三步 NSMutableURLRequest 的 request 配置的 NSURLSessionDataTask
    7. 第一个 NSURLSessionDataTaskDelegate 接收 response, 此时,配置好 expectedSize, 如果 responseCode 在 400 以下(除 304,304 是缓存 code),则开始接收 data;如果是 304,则不更新本地缓存,否则按照错误处理。
    8. 接收返回 data, 如果有 progressBlock 则根据 data 的百分比调用 progressBlock, 并且把每次接受的 data 加入到 NSMutableData *imageData 里面
    9. 接受完成后,判断是否有错误,如果有错误,返回错误;如果没有,则根据第八步的 NSMutableData *imageData 去组装成一个 UIImage, 根据是否组装成功,以及组装出来的 UIImage 是否 size 出现了 0,然后返回
    10. 如果图片正常返回,开启内存缓存和磁盘缓存(这里都是可以根据 option 配置的)
    11. 使用 NSCache 缓存,更具图片的 width、height 以及 scale 计算 NSCache 的 cost
    12. 根据 URL 的 MD5 值作为 key,在 ioQueue 线程中, 把图片写入到 NSSearchPathForDirectoriesInDomains
    13. 回主线程设置 UIImageView

    源码阅读

    步骤一:请求一个 URL 获取图片

    我们以一个 UIImageView 为入口, 看看整个 SDWebImage 的工作流程

    [self.imageView sd_setImageWithURL:@"http://ovsoxyd37.bkt.clouddn.com/SDWebImageClassDiagram.png" placeholderImage:nil];
    

    调用栈如下:

    - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder {
        [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
    }
    
    - (void)sd_setImageWithURL:(nullable NSURL *)url
              placeholderImage:(nullable UIImage *)placeholder
                       options:(SDWebImageOptions)options
                      progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                     completed:(nullable SDExternalCompletionBlock)completedBlock {
        [self sd_internalSetImageWithURL:url
                        placeholderImage:placeholder
                                 options:options
                            operationKey:nil
                           setImageBlock:nil
                                progress:progressBlock
                               completed:completedBlock];
    }
    
    - (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                      placeholderImage:(nullable UIImage *)placeholder
                               options:(SDWebImageOptions)options
                          operationKey:(nullable NSString *)operationKey
                         setImageBlock:(nullable SDSetImageBlock)setImageBlock
                              progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                             completed:(nullable SDExternalCompletionBlock)completedBlock
                               context:(nullable NSDictionary<NSString *, id> *)context {
        NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
        [self sd_cancelImageLoadOperationWithKey:validOperationKey];
        objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        
        if (!(options & SDWebImageDelayPlaceholder)) {
            dispatch_main_async_safe(^{
                [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
            });
        }
        
        if (url) {
    #if SD_UIKIT
            // check if activityView is enabled or not
            if ([self sd_showActivityIndicatorView]) {
                [self sd_addActivityIndicator];
            }
    #endif
            
            // reset the progress
            self.sd_imageProgress.totalUnitCount = 0;
            self.sd_imageProgress.completedUnitCount = 0;
            
            SDWebImageManager *manager = [context objectForKey:SDWebImageExternalCustomManagerKey];
            if (!manager) {
                manager = [SDWebImageManager sharedManager];
            }
            
            __weak __typeof(self)wself = self;
            SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
                wself.sd_imageProgress.totalUnitCount = expectedSize;
                wself.sd_imageProgress.completedUnitCount = receivedSize;
                if (progressBlock) {
                    progressBlock(receivedSize, expectedSize, targetURL);
                }
            };
            id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                ...//省略零
        } else {
            dispatch_main_async_safe(^{
    #if SD_UIKIT
                [self sd_removeActivityIndicator];
    #endif
                if (completedBlock) {
                    NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                    completedBlock(nil, error, SDImageCacheTypeNone, url);
                }
            });
        }
    }
    

    上面”省略零”的代码是步骤十三,也就是获得完图片之后,返回主线程去设置图片。

    1. 取消了本身下载的图片,有兴趣可以去阅读一下 sd_cancelCurrentImageLoad 的代码,这里简单说一下,当一个 UIView (本例子中是 UIImageView), 发起一个 URL 请求, SDWebImage 会把这个 URL 通过 objc_setAssociatedObject 的方法绑定到这个 UIView 中。如果这个 UIView 再次发起一个新的请求,会把原先的 URL 请求给取消掉。想想 UITableViewCell 的复用场景就知道为什么要这么做了
    2. 设置 placeholderUIActivityIndicatorView
    3. SDWebImageManager 发起查找 URL 图片。

    上述的 dispatch_main_async_safe(同样对应一个 dispatch_main_sync_safe 版本) 是一个简单的宏定义,

    #ifndef dispatch_main_async_safe
    #define dispatch_main_async_safe(block) dispatch_queue_async_safe(dispatch_get_main_queue(), block)
    #endif
    

    切换到主线程操作

    步骤二:寻找缓存

    - (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(nullable SDInternalCompletionBlock)completedBlock {
        // Invoking this method without a completedBlock is pointless
        NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
    
        // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
        // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
        if ([url isKindOfClass:NSString.class]) {
            url = [NSURL URLWithString:(NSString *)url];
        }
    
        // Prevents app crashing on argument type error like sending NSNull instead of NSURL
        if (![url isKindOfClass:NSURL.class]) {
            url = nil;
        }
    
        SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
        operation.manager = self;
    
        BOOL isFailedUrl = NO;
        if (url) {
            LOCK(self.failedURLsLock);
            isFailedUrl = [self.failedURLs containsObject:url];
            UNLOCK(self.failedURLsLock);
        }
    
        if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
            [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
            return operation;
        }
    
        LOCK(self.runningOperationsLock);
        [self.runningOperations addObject:operation];
        UNLOCK(self.runningOperationsLock);
        NSString *key = [self cacheKeyForURL:url];
        
        SDImageCacheOptions cacheOptions = 0;
        if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
        if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
        if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
        
        __weak SDWebImageCombinedOperation *weakOperation = operation;
        operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
            ...//省略一
        }];
    
        return operation;
    }
    
    

    这里,为了方便阅读,我删除了找缓存之后的回调代码。

    这里,简单的判断了 url 的参数是否为 NSURL, 如果是 NSString, 则帮你转化为 NSURL, 所以,我上面写这样的代码(sd_setImageWithURL: 后面跟了一个 NSString)

    然后判断这个 URL 在之前的操作中是否已经失败了,如果失败了,并且不进行重试!(options & SDWebImageRetryFailed),则直接返回错误

    最后,从 SDWebImageCache 中开始查找缓存

    - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
        if (!key) {
            if (doneBlock) {
                doneBlock(nil, nil, SDImageCacheTypeNone);
            }
            return nil;
        }
        
        // First check the in-memory cache...
        UIImage *image = [self imageFromMemoryCacheForKey:key];
        BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
        if (shouldQueryMemoryOnly) {
            if (doneBlock) {
                doneBlock(image, nil, SDImageCacheTypeMemory);
            }
            return nil;
        }
        
        NSOperation *operation = [NSOperation new];
        void(^queryDiskBlock)(void) =  ^{
            if (operation.isCancelled) {
                // do not call the completion if cancelled
                return;
            }
            
            @autoreleasepool {
                NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
                UIImage *diskImage;
                SDImageCacheType cacheType = SDImageCacheTypeDisk;
                if (image) {
                    // the image is from in-memory cache
                    diskImage = image;
                    cacheType = SDImageCacheTypeMemory;
                } else if (diskData) {
                    // decode image data only if in-memory cache missed
                    diskImage = [self diskImageForKey:key data:diskData options:options];
                    if (diskImage && self.config.shouldCacheImagesInMemory) {
                        NSUInteger cost = SDCacheCostForImage(diskImage);
                        [self.memCache setObject:diskImage forKey:key cost:cost];
                    }
                }
                
                if (doneBlock) {
                    if (options & SDImageCacheQueryDiskSync) {
                        doneBlock(diskImage, diskData, cacheType);
                    } else {
                        dispatch_async(dispatch_get_main_queue(), ^{
                            doneBlock(diskImage, diskData, cacheType);
                        });
                    }
                }
            }
        };
        
        if (options & SDImageCacheQueryDiskSync) {
            queryDiskBlock();
        } else {
            dispatch_async(self.ioQueue, queryDiskBlock);
        }
        
        return operation;
    }
    
    - (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
        return [self.memCache objectForKey:key];
    }
    

    self.memCache 是一个 NSCache, 首先检查了 self.memCache 中是否存在,如果不存在则去硬盘中查找,如果找到了,把这个东西放进 self.memCache 中。如果没有找到,也返回 doneBlock 中完成“省略一”的操作

    接着看“省略一”的代码,去除了部分错误处理的代码(因为实在太长。。而且你并不会那么关注)

    ...代码省略
            
    if (cachedImage && options & SDWebImageRefreshCached) {
        // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
        // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
    }
    
    __weak typeof(strongOperation) weakSubOperation = strongOperation;
    strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
        ...省略二        
    }
    

    去除一些错误代码之后,可以看到,当缓存在没有找到之后,此时,会向 SDWebImageDownloader 发起一个下载任务进入步骤三

    步骤三:创建 NSMutableURLRequest

    - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                       options:(SDWebImageDownloaderOptions)options
                                                      progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                     completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
        __weak SDWebImageDownloader *wself = self;
    
        return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^NSOperation<SDWebImageDownloaderOperationInterface> *{
            __strong __typeof (wself) sself = wself;
            NSTimeInterval timeoutInterval = sself.downloadTimeout;
            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
            NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
            NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                        cachePolicy:cachePolicy
                                                                    timeoutInterval:timeoutInterval];
            
            request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
            request.HTTPShouldUsePipelining = YES;
            if (sself.headersFilter) {
                request.allHTTPHeaderFields = sself.headersFilter(url, [sself allHTTPHeaderFields]);
            }
            else {
                request.allHTTPHeaderFields = [sself allHTTPHeaderFields];
            }
        //步骤四和步骤五的代码
    }
    
    - (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                               completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                       forURL:(nullable NSURL *)url
                                               createCallback:(NSOperation<SDWebImageDownloaderOperationInterface> *(^)(void))createCallback {
        // 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 != nil) {
                completedBlock(nil, nil, nil, NO);
            }
            return nil;
        }
        
        LOCK(self.operationsLock);
        NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
        // There is a case that the operation may be marked as finished, but not been removed from `self.URLOperations`.
        if (!operation || operation.isFinished) {
            operation = createCallback();
            __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];
            // 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.
            [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;
    }
    

    addProgressCallback:completedBlock:forURL:createCallback这个函数就是先检查之前是否有这个 URL 正在下载,self.URLCallbacks 是一个 NSMutableDictionary, 这里保存了正在下载的 URL, 在 Kingfisher 中,也有这样一段代码。 紧接着,创建一个 NSMutableURLRequest, 配置好它的参数,进入步骤四

    步骤四:根据NSMutableURLRequest创建一个NSOperation

    @interface SDWebImageDownloadToken : NSObject <SDWebImageOperation>
    
    @end
    
    
    @implementation SDWebImageDownloaderOperation
    
    - (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;
            _callbackBlocks = [NSMutableArray new];
            _executing = NO;
            _finished = NO;
            _expectedSize = 0;
            _unownedSession = session;
            _callbacksLock = dispatch_semaphore_create(1);
            _coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);
        }
        return self;
    }
    @end
    
    
    - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                       options:(SDWebImageDownloaderOptions)options
                                                      progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                     completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
                                                     
        //步骤三
        
        NSOperation<SDWebImageDownloaderOperationInterface> *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        operation.shouldDecompressImages = sself.shouldDecompressImages;
            
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        //步骤五
        
        return operation;
    }
    

    步骤四的代码在 Kingfisher 的代码中并没有被体现,原因是 Kingfisher直接使用了NSURLSessionTask 的优先级(iOS 8 API)

    步骤五:把 operation 加入到队列中

    - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                       options:(SDWebImageDownloaderOptions)options
                                                      progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                     completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
                                                     
        //步骤三
        //步骤四
        
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
            
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }
    }
    

    SDWebImage 存在两种模式,先进先出和先进后出,具体的实现方案是 SDWebImag 使用了一个队列,downloadQueue 以及 lastAddedOperation 记录上一个 operation, 如果是先进先出,则直接加入到 downloadQueue, 如果是先进后出,再加入到 downloadQueue 之后,还要让 lastAddedOperation dependency operation。总之,这里的代码很简单,看一看就明白了

    步骤六:配置 NSURLSessionDataTask,开启 operation

    这里具体的代码在 SDWebImageDownloaderOperation: NSOperationstart 函数中

    - (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) {
                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.
                 */
                session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                        delegate:self
                                                   delegateQueue:nil];
                self.ownedSession = session;
            }
            
            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;
                }
            }
            
            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 {
            [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
    }
    

    单纯的组装代码,没啥好解释的。就是组装一个 NSURLSessionDataTask, 某些情况下,开启后台任务下载。 然后等待回调

    步骤七:接收第一次响应

    - (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 Not Modified' is an exceptional one. It should be treated as cancelled if no cache data
        //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;
        }
        
        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;
        }
        
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:weakSelf];
        });
        
        if (completionHandler) {
            completionHandler(disposition);
        }
    }
    

    根据 http 的响应,判断是否是 正确的 code,如果是 304 code,特殊处理。并且把 self.expectedSize 计算好,用于 process 的计算

    步骤八:接收 data

    - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
        if (!self.imageData) {
            self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
        }
        [self.imageData appendData:data];
    
        if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
            // Get the image data
            __block NSData *imageData = [self.imageData copy];
            // Get the total bytes downloaded
            const NSInteger totalSize = imageData.length;
            // Get the finish status
            BOOL finished = (totalSize >= self.expectedSize);
            
            if (!self.progressiveCoder) {
                // We need to create a new instance for progressive decoding to avoid conflicts
                for (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedInstance].coders) {
                    if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
                        [((id<SDWebImageProgressiveCoder>)coder) canIncrementallyDecodeFromData:imageData]) {
                        self.progressiveCoder = [[[coder class] alloc] init];
                        break;
                    }
                }
            }
            
            // progressive decode the image in coder queue
            dispatch_async(self.coderQueue, ^{
                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)}];
                    }
                    
                    // 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.imageData.length, self.expectedSize, self.request.URL);
        }
    }
    

    接收 data,然后组装成图片

    步骤九:接收 data 结束,解码 image

    - (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) {
                /**
                 *  If you specified to use `NSURLCache`, then the response you get here is what you need.
                 */
                __block NSData *imageData = [self.imageData copy];
                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]) {
                        // 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, ^{
                            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];
            }
        }
    }
    

    这段代码大部分都是在错误处理

    步骤十~十二:启内存缓存和磁盘缓存

    这里的代码调用入口在 “省略二” 的 block 中,由于大部分的代码都是在判断条件是否要缓存,以及一些移除 operation,以及把这个 URL 从正在下载的字典中移除,代码比较杂,所以我直接列了缓存的代码

    - (void)storeImage:(nullable UIImage *)image
             imageData:(nullable NSData *)imageData
                forKey:(nullable NSString *)key
                toDisk:(BOOL)toDisk
            completion:(nullable SDWebImageNoParamsBlock)completionBlock {
        if (!image || !key) {
            if (completionBlock) {
                completionBlock();
            }
            return;
        }
        // if memory cache is enabled
        if (self.config.shouldCacheImagesInMemory) {
            NSUInteger cost = SDCacheCostForImage(image);
            [self.memCache setObject:image forKey:key cost:cost];
        }
        
        if (toDisk) {
            dispatch_async(self.ioQueue, ^{
                @autoreleasepool {
                    NSData *data = imageData;
                    if (!data && image) {
                        // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
                        SDImageFormat format;
                        if (SDCGImageRefContainsAlpha(image.CGImage)) {
                            format = SDImageFormatPNG;
                        } else {
                            format = SDImageFormatJPEG;
                        }
                        data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
                    }
                    [self _storeImageDataToDisk:data forKey:key];
                }
                
                if (completionBlock) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        completionBlock();
                    });
                }
            });
        } else {
            if (completionBlock) {
                completionBlock();
            }
        }
    }
    
    FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
    #if SD_MAC
        return image.size.height * image.size.width;
    #elif SD_UIKIT || SD_WATCH
        return image.size.height * image.size.width * image.scale * image.scale;
    #endif
    

    拿到图片以后,把它写入到 NSCache 中,以 image.size.height * image.size.width * image.scale * image.scalecost 之后,在写入到磁盘中,SDWebImage 默认清除一个星期的缓存,会在每次 app 被杀掉或者进入后台的时候执行

    步骤十三回到主线程

    主线程去设置图片,完成最后的操作相对简单,这里不在叙述

    相关文章

      网友评论

          本文标题:SDWebImage4.4.2 简单分析

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