SDWebImage源码解读

SDWebImage是一个开源的第三方库,它提供了UIImageView的分类来实现从网络端下载数据并缓存到内存和磁盘。

SDWebImage有如下特点:

  • 提供了UIImageView和UIButton的分类。以支持加载网络图片并缓存。
  • 一个异步的图片下载器
  • 提供异步的内存和磁盘缓存,并自动处理缓存过期。
  • 后台图片解压缩处理。
  • 确保同一个URL不会被下载多次。
  • 确保主线程永远不会阻塞。

一.储备知识

SDWebImage中每一个下载任务都是一个SDWebImageDownloaderOperation,而SDWebImageDownloaderOperation又是继承自NSOperation,所以每一个下载任务对应一个NSOperation。在SDWebImage中使用SDWebImageDownloader来管理
多个下载任务,在SDWebImageDownloader中有一个downloadedQueue这个属性,这个属性是NSOperationQueue类型的,也就是用NSOperationQueue来管理NSOperation
下面我们就一起学习一下NSOperationNSOperationQueue

NSOperation和NSOperationQueue

NSOperation是一个抽象类,用来表示与单个任务相关联的代码和数据。

NSOperation类是一个抽象类,我们不能直接去使用它而是应该创建一个子类来继承它。虽然它只是一个抽象类,但是它的基本实现还是提供了很有价值的逻辑来确保任务的安全执行。这种原生的逻辑可以让我们专注于任务的真正的实现,而不需要用一些胶水代码去确保这个任务能正常工作在其他地方。

我们可以把一个NSOperation对象加入到一个operation queue中,让这个operation queue去决定什么时候执行这个operation。当使用operation queue去管理operation时,轮到某个operation执行时实际是去执行这个operation的start方法,所以我们一个operation对象实际要执行的任务应该放在start方法里面。如果我们不想使用operation queue,也可以通过手动调用NSOperation的start方法来执行任务。

我们可以通过添加依赖来确定operation queue中operation的具体执行顺序。添加依赖和移除依赖可以使用下列的API:

//添加依赖
- (void)addDependency:(NSOperation *)op;
//移除依赖
- (void)removeDependency:(NSOperation *)op;

只要当一个operation对象的所有依赖都执行完成的时候,其才可以变成熟ready状态,然后才可以被执行。如果一个operation没有添加依赖,直接加入了operation queue中,那么就会按照加入队列的先后顺序,当这个operation的前一个operation执行完成以后,其状态才会变成ready,才能被执行。

NSOperation对象有下列四个比较重要的状态:

  • isCancelled
  • isExecuting
  • isFinished
  • isReady
    其中isExecutingisFinishedisReady这三种状态相当于是operation对象的生命周期:
    operation生命周期.png

    isCancelled这种状态则比较特殊,当我们对operation对象调用- (void)cancel方法时,其isCancelled属性会被置为YES。这个时候要分两种情况:
  • operation正在执行
    也就是说其状态现在是isExecuting,调用- (void)cancel方法后会马上停止执行当前任务,并且状态变为isFinishedisCancelled = Yes
  • operation还没开始执行
    这个时候operation的状态其实是isReady这个状态之前,operation还没开始执行,调用- (void)cancel方法后会去调用operation的start方法,在start方法里我们要去处理cancel事件,并设置isFinished = YES
    调用cancel方法.png
SDWebImageOptions

在SDWebImage中大量使用了option类型,通过判断option类型的值来决定下一步应该怎么做,所以如果对这些option值一点都不了解的话可能理解起源码来也会非常难受。SDWebImageOptions是暴露在外的可供使用者使用的option。还有一些option比如SDImageCacheOptions, SDWebImageDownloaderOptions这些都是不暴露给用户使用的。源码中是根据用户设置的SDWebImageOptions这个option来确定另外两个option的值。
下面我们来具体看一下SDWebImageOptions

SDWebImageOptions.png
SDImageCacheOptions

SDImageCacheOptions.png

这里我们也可以看到,SDImageCacheOptions中的三个选项在SDWebImageOptions中都有对应的选项。

  • SDImageCacheQueryDataWhenInMemory对应SDWebImageQueryDataWhenInMemory
  • SDImageCacheQueryDiskSync对应SDWebImageQueryDiskSync
  • SDImageCacheScaleDownLargeImages对应SDWebImageScaleDownLargeImages

SDWebImageDownloaderOptions

SDWebImageDownloaderOptions.png

SDWebImageDownloaderOptions中所有选项在SDWebImageOptions中也有相对应的选项,这里不再赘述。

SDImageCacheType

    //从缓存中得不到数据
    SDImageCacheTypeNone,
    
    //从磁盘缓存中得到数据
    SDImageCacheTypeDisk,
    
    //从内存缓存中得到数据
    SDImageCacheTypeMemory

框架的主要类和一次图片加载的主要流程

框架的主要类

主要类之间的关系.png
从上图也可以看出,整个框架主要分为三部分,即图片的下载,图片的缓存,和处理图片相关的类。

一次图片加载的主要流程

加载图片流程图.png

针对上图中一次图片加载的主要流程,每一步做介绍:

  • 1.SDWebImage为UIImagView创建了一个分类UIImageView (WebCache),然后UIImageView对象可以调用这个分类的方法来下载图片:
    [imageView sd_setImageWithURL:[NSURL URLWithString:@""]];
  • 2.UIImageView (WebCache)- (void)sd_setImageWithURL:(nullable NSURL *)url方法实际调用了UIView (WebCache)的下列方法:
- (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;
  • 3.UIView (WebCache)的上述方法在实现时会创建一个SDWebImageManager的实例对象,然后调用其下列方法来加载图片:
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock;
  • 4.在SDWebImageManager对象的上述方法里,首先会查询在缓存中有没有这个图片,然后根据各种option的判断决定是否要从网络端下载。查询缓存中有没有是通过调用SDImageCache对象的实例方法来实现的:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key 
                                            options:(SDImageCacheOptions)options 
                                               done:(nullable SDCacheQueryCompletedBlock)doneBlock;
  • 5.返回缓存查询的结果
  • 6.如果需要下载图片,那么调用SDWebImageDownloader对象的下列方法进行下载:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
  • 7.获取从网络端下载的图片。
  • 8.判断是否要将下载的图片进行缓存,如果需要,则缓存。
  • 9.把通过SDWebImageManager对象获取的图片显示在UIImageView上。

源码分析

这一部分我们进行详细的源码分析。
首先从SDWebImageManager类的loadImageWithURL:方法看起:
由于代码比较长,我就采用注释的方式

- (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");
    
    //如果传进来的是一个NSString,则把NSString转化为NSURL
    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;
    
    //self.failedURLs是nsurl的黑名单,一般情况下,如果URL在这个黑名单里,那么就不会尝试加载这个图片,直接返回
    BOOL isFailedUrl = NO;
    if (url) {
        LOCK(self.failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        UNLOCK(self.failedURLsLock);
    }
    
    //SDWebImageRetryFailed即即使URL被加入了黑名单,也要尝试加载这个URL对应的图片
    //如果URL长度为0,或者URL被加入了黑名单并且没有设置SDWebImageRetryFailed,那么就直接回调完成的block
    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];
    
    //由于我们在使用API的时候只设置SDWebImageOptions,所以这里就是根据用户设置的SDWebImageOptions去设置SDImageCacheOptions
    SDImageCacheOptions cacheOptions = 0;
    if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
    if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
    if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
    
    __weak SDWebImageCombinedOperation *weakOperation = operation;
    //这里开始调用SDImageCache对象的queryCacheOperationForKey:方法去缓存中查找有没有这个URL对应的图片
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        __strong __typeof(weakOperation) strongOperation = weakOperation;
        if (!strongOperation || strongOperation.isCancelled) {
            [self safelyRemoveOperationFromRunning:strongOperation];
            return;
        }
        
        // 判断我们是否需要从网络端下载图片
        //首先检查没有设置只能从缓存中获取,然后检查cachedImage = nil或者设置了要刷新缓存,则需要从网络端下载图片
        BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
        && (!cachedImage || options & SDWebImageRefreshCached)
        && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
        if (shouldDownload) {
            //从缓存中获取了图片并且设置了要刷新缓存这个option,则要进行两次完成的回调,这是第一次回调
            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];
            }
            
            // download if no image or requested to refresh anyway, and download allowed by delegate
            //这里是根据用户设置的SDWebImageOptions来手动设置SDWebImageDownloaderOptions
            SDWebImageDownloaderOptions downloaderOptions = 0;
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
            if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
            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 (cachedImage && options & SDWebImageRefreshCached) {
                
                //这里其实就是把SDWebImageDownloaderProgressiveDownload这个option去掉
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // ignore image read from NSURLCache if image if cached but force refreshing
                //加上SDWebImageDownloaderIgnoreCachedResponse这个option,忽略NSURLCache中缓存的response
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
            

            __weak typeof(strongOperation) weakSubOperation = strongOperation;
            
            //l开始进行图片的下载
            strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
                if (!strongSubOperation || strongSubOperation.isCancelled) {
                    // Do nothing if the operation was cancelled
                    // See #699 for more details
                    // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
                } else if (error) {
                    [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
                    BOOL shouldBlockFailedURL;
                    // 后面都是判断在请求失败的情况下是否应该把
                    if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
                        shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
                    } else {
                        shouldBlockFailedURL = (   error.code != NSURLErrorNotConnectedToInternet
                                                && error.code != NSURLErrorCancelled
                                                && error.code != NSURLErrorTimedOut
                                                && error.code != NSURLErrorInternationalRoamingOff
                                                && error.code != NSURLErrorDataNotAllowed
                                                && error.code != NSURLErrorCannotFindHost
                                                && error.code != NSURLErrorCannotConnectToHost
                                                && error.code != NSURLErrorNetworkConnectionLost);
                    }
                    
                    if (shouldBlockFailedURL) {
                        LOCK(self.failedURLsLock);
                        [self.failedURLs addObject:url];
                        UNLOCK(self.failedURLsLock);
                    }
                }
                else {
                    //如果设置了SDWebImageRetryFailed那么就要把URL从黑名单中移除
                    if ((options & SDWebImageRetryFailed)) {
                        LOCK(self.failedURLsLock);
                        [self.failedURLs removeObject:url];
                        UNLOCK(self.failedURLsLock);
                    }
                    
                    //判断是否应该把下载的图片缓存到磁盘,SDWebImageCacheMemoryOnly这个option表示只把图片缓存到内存
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
                    
                    // We've done the scale process in SDWebImageDownloader with the shared manager, this is used for custom manager and avoid extra scale.
                    if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
                        downloadedImage = [self scaledImageForKey:key image:downloadedImage];
                    }
                    
                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            @autoreleasepool {
                                UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
                                
                                if (transformedImage && finished) {
                                    BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                    NSData *cacheData;
                                    // pass nil if the image was transformed, so we can recalculate the data from the image
                                    if (self.cacheSerializer) {
                                        cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
                                    } else {
                                        cacheData = (imageWasTransformed ? nil : downloadedData);
                                    }
                                    [self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
                                }
                                
                                [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                            }
                        });
                    } else {
                        //可以直接看到这一部分
                        if (downloadedImage && finished) {
                            if (self.cacheSerializer) {
                                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                                    @autoreleasepool {
                                        NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
                                        [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
                                    }
                                });
                            } else {
                                //对图片进行缓存
                                [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                        }
                        //第二次调用完成的block
                        [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }
                
                if (finished) {
                    [self safelyRemoveOperationFromRunning:strongSubOperation];
                }
            }];
            //如果从从缓存中获取了图片并且不需要下载
        } else if (cachedImage) {
            //执行完成的回调
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:strongOperation];
        } else {
            // 缓存中没有获取图片,也不用下载
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:strongOperation];
        }
    }];
    
    return operation;
}

总结一下SDWebImageManagerloadImageWithURL:所做的事情:

其实在loadImageWithURL:里面做了加载图片的完整流程。首先检查传入的NSURL的有效性。然后开始从缓存中查找是否有这个图片,得到查询结果之后再根据查询结果和设置的option判断是否需要进行下载操作,如果不需要下载操作那么就直接使用cache image进行下载回调。如果需要进行下载操作那么就开始下载,下载完成后按照设置的option将图片缓存到内存和磁盘,最后进行完成的回调。

然后我们看一下查询缓存的具体过程,也就是SDImageCache这个类的queryCacheOperationForKey:方法:
这里也是采用注释的方式

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    
    // 首先检查内存缓存中有没有这个图片,注意内存缓存使用的是NSCache,它是一个类字典结构,使用图片对应的nsurl作为key,在查询的时候就用这个key去查询
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    
    //是否只查询内存缓存(如果从内存缓存中获取了图片并且没有设置SDImageCacheQueryDataWhenInMemory,那么就只查询内存缓存)
    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;
            //j缓存获取的类型,有三种m类型,none,memory,disk
            SDImageCacheType cacheType = SDImageCacheTypeNone;
            if (image) {
                // 图片是从内存缓存中获取的
                diskImage = image;
                cacheType = SDImageCacheTypeMemory;
            } else if (diskData) {
                //图片是从磁盘缓存中获取的
                cacheType = SDImageCacheTypeDisk;
                // 解压图片
                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;
}

总结一下queryCacheOperationForKey:方法所做的事情:

SDImageCache这个类是专门负责缓存相关的问题的,包括查询缓存和将图片进行缓存。SDImageCache使用了一个NSCache对象来进行内存缓存,磁盘缓存则是把图片数据存放在应用沙盒的Caches这个文件夹下。

首先查询内存缓存,内存缓存查询完了以后再判断是否需要查询磁盘缓存。如果查询内存缓存已经有了结果并且没有设置一定要查询磁盘缓存,那么就不查询磁盘缓存,否则就要查询磁盘缓存。内存缓存没有查询到图片,并且磁盘缓存查询到了图片,那么就要把这个内容缓存到内存缓存中。

图片的缓存查询完成后我们再来看一下下载操作,即SDWebImageDownloaderdownloadImageWithURL:方法

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  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 != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }
    
    LOCK(self.operationsLock);
    NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
    if (!operation || operation.isFinished) {
        
        //创建一下下载的operation
        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];
        // 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加入到nNSOperationQueue中去
        [self.downloadQueue addOperation:operation];
    }
    UNLOCK(self.operationsLock);

    //这一部分代码是在取消operation的时候使用
    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    
    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

SDWebImageDownloader这个类是专门管理下载的,它有一个属性是downloadQueue,这是一个NSOperationQueue,每创建一个新的下载任务都把它加入到这个downloadQueue中,让downloadQueue去管理任务的开始,取消,结束。

上面的方法其实做的事情很简单,就是创建了一个下载图片的operation,然后把它加入到了downloadQueue中去。

下面我们来具体看一下创建下载图片的operation的过程,即SDWebImageDownloader类的createDownloaderOperationWithUrl:方法:

- (NSOperation<SDWebImageDownloaderOperationInterface> *)createDownloaderOperationWithUrl:(nullable NSURL *)url
                                                                                  options:(SDWebImageDownloaderOptions)options {
    NSTimeInterval timeoutInterval = self.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 (self.headersFilter) {
        request.allHTTPHeaderFields = self.headersFilter(url, [self allHTTPHeaderFields]);
    }
    else {
        request.allHTTPHeaderFields = [self allHTTPHeaderFields];
    }
    
    //前面都是为了创建一个request,然后使用request和session对象去创建下载的operation
    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];
    }
    
    //设置operation的队列优先级
    if (options & SDWebImageDownloaderHighPriority) {
        operation.queuePriority = NSOperationQueuePriorityHigh;
    } else if (options & SDWebImageDownloaderLowPriority) {
        operation.queuePriority = NSOperationQueuePriorityLow;
    }
    
    //如果设置的执行顺序是xLIFI,即后进先出,则要把queue中的最后一个加入的operation的依赖设置为该operation,这样来保证这个operation最先执行
    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;
}

这个方法就是创建了一个request对象,然后使用这个request对象和session对象去创建下载的operation对象。

我们看一下负责单个下载任务的operation对象到底是怎么创建的,即SDWebImageDownloaderOperation类的- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request inSession:(nullable NSURLSession *)session options:(SDWebImageDownloaderOptions)options方法:

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

这个初始化方法其实也很简单,就是给自己的成员变量赋值

我们知道,NSOperation类的真正执行任务是在其start方法里面,那么我们看一下SDWebImageDownloaderOperationstart方法的具体实现:
代码比较长,我在关键部分加了注释

- (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;
        //创建一个session对象,因为后面要创建NSURLSessionTask,需要session对象
        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;
            }
        }
        
        //创建dataTask,这个才是真正执行下载任务的
        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
}

这里就是通过一个session对象和一个request对象创建了一个dataTask对象,这个dataTask对象才是真正用来下载的,然后调用[self.dataTask resume]执行下载。

到这里SDWebImage的源码分析就结束啦。

参考:
SDWebImage实现分析

这篇文章在简书的地址:SDWebImage源码解读

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,348评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,122评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,936评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,427评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,467评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,785评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,931评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,696评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,141评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,483评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,625评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,291评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,892评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,492评论 2 348

推荐阅读更多精彩内容