SDWebImage源码解析<二>

前言

我们在第一篇文章《SDWebImage源码解析<一>》已经了解到SDWebImage是通过 SDWebImageManager 类进行协调,调用 SDImageCacheSDWebImageDownloader 来实现图片的缓存查询与网络下载的。今天我们就来分析一下SDImageCacheSDWebImageDownloader

SDImageCache

该类维护了一个内存缓存与一个可选的磁盘缓存。同时,磁盘缓存的写操作是异步的,所以它不会对 UI 造成不必要的影响。

存储图片

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
    if (!image || !key) {
        return;
    }
    // 内存缓存 前提是设置了需要进行,将其存入 NSCache 中,同时传入图片的消耗值,cost 为像素值(当内存受限或者所有缓存对象的总代价超过了最大允许的值时,缓存会移除其中的一些对象)
    if (self.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }
    // 磁盘缓存
    if (toDisk) {
        // 将缓存操作作为一个任务放入ioQueue中异步执行
        dispatch_async(self.ioQueue, ^{
            NSData *data = imageData;

            if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
                // 需要确定图片是PNG还是JPEG。PNG图片容易检测,因为有一个唯一签名。PNG图像的前8个字节总是包含以下值:137 80 78 71 13 10 26 10
                // 在imageData为nil的情况下假定图像为PNG。我们将其当作PNG以避免丢失透明度。
                int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
                BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                                  alphaInfo == kCGImageAlphaNoneSkipFirst ||
                                  alphaInfo == kCGImageAlphaNoneSkipLast);
                BOOL imageIsPng = hasAlpha;
                // 而当有图片数据时,我们检测其前缀,确定图片的类型
                if ([imageData length] >= [kPNGSignatureData length]) {
                    imageIsPng = ImageDataHasPNGPreffix(imageData);
                }
               // 如果 image 是 PNG 格式,就是用 UIImagePNGRepresentation 将其转化为 NSData,否则按照 JPEG 格式转化,并且压缩质量为 1,即无压缩
                if (imageIsPng) {
                    data = UIImagePNGRepresentation(image);
                }
                else {
                    data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
                }
#else
                data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
            }
           // 创建缓存文件并存储图片(使用 fileManager)
            if (data) {
                if (![_fileManager fileExistsAtPath:_diskCachePath]) {
                    [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
                }

                // 根据image的key获取缓存路径
                NSString *cachePathForKey = [self defaultCachePathForKey:key];
                NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];

                [_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];

                // 不适用iCloud备份
                if (self.shouldDisableiCloud) {
                    [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
                }
            }
        });
    }
}

查询图片

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    // 对doneBlock、key判空 查找内存缓存
    if (!doneBlock) {
        return nil;
    }

    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // 首先检查内存缓存(查询是同步的),如果查找到,则直接回调 doneBlock 并返回

    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }

    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) { // isCancelled初始默认值为NO
            return;
        }

        @autoreleasepool {
// 检查磁盘缓存(查询是异步的),如果查找到,则将其放到内存缓存,并调用 doneBlock 回调
            UIImage *diskImage = [self diskImageForKey:key];
            if (diskImage && self.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
// 缓存至内存(NSCache)中
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
// 返回主线程设置图片
            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

    return operation;
}

通过代码可以看到operation虽然没有具体的内容,但是我们可以在外部调用operation的cancel方法来改变isCancelled的值。这样做对从内存缓存中查找到图片的本次操作查询过程没有影响,但是如果本次查询过程是在磁盘缓存中进行的,就会受到影响,autoreleasepool{}代码块不再执行。而在这段代码块完成了这样的工作:将磁盘缓存取出进行内存缓存,在线程执行完成回调。因此可以看到这个返回的NSOpeation值可以帮助我们在外部控制不再进行磁盘缓存查询和内存缓存备份的操作,归根结底就是向外部暴漏了取消操作的接口。

清除图片

对于清理方法cleanDiskWithCompletionBlock:,有两个条件:文件的缓存有效期及最大缓存空间大小。

  • 文件的缓存有效期可以通过maxCacheAge属性来设置,默认是1周的时间。如果文件的缓存时间超过这个时间值,则将其移除。
  • 最大缓存空间大小是通过maxCacheSize属性来设置的,如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间。
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

        // 枚举器预先获取缓存文件的有用的属性
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];

        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
        NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;

        // 枚举缓存文件夹中所有文件,该迭代有两个目的:移除比过期日期更老的文件;存储文件属性以备后面执行基于缓存大小的清理操作
        NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
            // 跳过文件夹
            if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            // 移除早于有效期的老文件
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }

            // 存储文件的引用并计算所有文件的总大小
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
            [cacheFiles setObject:resourceValues forKey:fileURL];
        }
        
        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }

        // 如果磁盘缓存的大小超过我们配置的最大大小,则执行基于文件大小的清理,我们首先删除最老的文件
        if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
            // 以设置的最大缓存大小的一半值作为清理目标
            const NSUInteger desiredCacheSize = self.maxCacheSize / 2;

            // 按照最后修改时间来排序剩下的缓存文件
            NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                            usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                            }];

            // 删除文件,直到缓存总大小降到我们期望的大小
            for (NSURL *fileURL in sortedFiles) {
                if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];

                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

SDWebImageDownloaderOptions

在下载的过程中,程序会根据设置的不同的下载选项,而执行不同的操作。下载选项由枚举 SDWebImageDownloaderOptions定义,具体如下:

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    SDWebImageDownloaderLowPriority = 1 << 0,
    /// 渐进式下载,如果设置了这个选项,会在下载过程中,每次接收到一段chunk数据就调用一次完成回调(注意是完成回调)回调中的image参数为未下载完成的部分图像
    SDWebImageDownloaderProgressiveDownload = 1 << 1,

    /// 通常情况下request阻止使用NSURLCache. 这个选项会用默认策略使用NSURLCache 
    SDWebImageDownloaderUseNSURLCache = 1 << 2,

    /// 如果从NSURLCache中读取图片,会在调用完成block时,传递空的image或imageData \
     * (to be combined with `SDWebImageDownloaderUseNSURLCache`).
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,

    /// 系统为iOS 4+时,如果应用进入后台,继续下载。这个选项是为了实现在后台申请额外的时间来完成请求。如果后台任务到期,操作会被取消。
    SDWebImageDownloaderContinueInBackground = 1 << 4,

    /// 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES的方式来处理存储在NSHTTPCookieStore的cookies
    SDWebImageDownloaderHandleCookies = 1 << 5,

    /// 允许不受信任的SSL证书,在测试环境中很有用,在生产环境中要谨慎使用
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,

    /// 将图片下载放到高优先级队列中
    SDWebImageDownloaderHighPriority = 1 << 7,
};

下面我们看一下SDWebImageDownloaderOperation对NSOperation的-start方法的重写,毕竟这是完成下载任务的核心代码

- (void)start {
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
// 将各个属性置空。包括取消回调、完成回调、进度回调,用于网络连接的connection,用于拼接数据的imageData、记录当前线程的属性thread。
            [self reset];
            return;
        }

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
// 使用UIApplication的beginBackgroundTaskWithExpirationHandler方法向系统借用一点时间,继续执行下面的代码来完成connection的创建和进行下载任务。
 // 在后台任务执行时间超过最大时间时,也就是后台任务过期执行过期回调。在回调主动将这个后台任务结束。
        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 (!self.unownedSession) {
            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.
             */
            self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];
            session = self.ownedSession;
        }
        
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;// 标记状态
        self.thread = [NSThread currentThread]; // 记录当前线程
    }
    
    [self.dataTask resume];

    if (self.dataTask) {
        if (self.progressBlock) {
            self.progressBlock(0, NSURLResponseUnknownLength);
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
    }
    else {
        if (self.completedBlock) {
            self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
        }
    }

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
    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
}

SDWebImageDownloader

SDWebImageDownloader有一个重要的属性executionOrder代表着下载操作执行的顺序,它是一个SDWebImageDownloaderExecutionOrder枚举类型

typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
    // 默认值,所有的下载操作以队列类型 (先进先出)执行.
    SDWebImageDownloaderFIFOExecutionOrder,

    // 所有的下载操作以栈类型 (后进先出)执行.
    SDWebImageDownloaderLIFOExecutionOrder
};

默认是SDWebImageDownloaderFIFOExecutionOrder,是在init方法中设置的。如果设置了后进先出,在下载操作添加到下载队列中时,会依据这个值添加依赖关系,使得最后添加操作出在依赖关系链条中的第一项,因而会优先下载最后添加的操作任务。
SDWebImageDownloader还提供了其他几个重要的对外接口(包括属性和方法):
1.BOOL shouldDecompressImages
是否需要解压,在init中设置默认值为YES,在下载操作创建之后将值传递给操作的同名属性。
解压下载或缓存的图片可以提升性能,但是会消耗很多内存
默认是YES,如果你会遇到因为过高的内存消耗引起的崩溃将它设置为NO。
2.NSInteger maxConcurrentDownloads
放到下载队列中的下载操作的总数,是一个瞬间值,因为下载操作一旦执行完成,就会从队列中移除。
3.NSUInteger currentDownloadCount
下载操作的超时时长默认是15.0,即request的超时时长,若设置为0,在创建request的时候依然使用15.0。
只读。
4.NSURLCredential *urlCredential
为request操作设置默认的URL凭据,具体实施为:在将操作添加到队列之前,将操作的credential属性值设置为urlCredential
5.NSString *username和NSString *passwords
如果设置了用户名和密码:在将操作添加到队列之前,会将操作的credential属性值设置为[NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession],而忽略了属性值urlCredential。
6.- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
为HTTP header设置value,用来追加到每个下载对应的HTTP request, 若传递的value为nil,则将对应的field移除。
扩展里面定义了一个HTTPHeaders属性(NSMutableDictionary类型)用来存储所有设置好的header和对应value。
在创建request之后紧接着会将HTTPHeaders赋给request,request.allHTTPHeaderFields = self.HTTPHeaders;
7.- (NSString *)valueForHTTPHeaderField:(NSString *)field;
返回指定的HTTP header field对应的value
8.SDWebImageDownloaderHeadersFilterBlock headersFilter
设置一个过滤器,为下载图片的HTTP request选取header.意味着最终使用的headers是经过这个block过滤之后的返回值。
9.- (void)setOperationClass:(Class)operationClass;
设置一个SDWebImageDownloaderOperation的子类 ,在每次 SDWebImage 构建一个下载图片的请求操作的时候作为默认的NSOperation使用.
参数operationClass为要设置的默认下载操作的SDWebImageDownloaderOperation的子类。 传递 nil 会恢复为SDWebImageDownloaderOperation
以下两个方法是下载控制方法了
- (id <SDWebImageOperation>)downloadImageWithURL: options: progress: completed:
这个方法用指定的URL创建一个异步下载实例。
有关completedBlock回调的一些解释:下载完成的时候block会调用一次.
没有使用SDWebImageDownloaderProgressiveDownload选项的情况下,如果下载成功会设置image参数,如果出错,会根据错误设置error参数. 最后一个参数总是YES. 如果使用了SDWebImageDownloaderProgressiveDownload选项,这个block会使用部分image的对象有间隔地重复调用,同时finished参数设置为NO,直到使用完整的image对象和值为YES的finished参数进行最后一次调用.如果出错,finished参数总是YES.
- (void)setSuspended:(BOOL)suspended;
设置下载队列的挂起(暂停)状态。若为YES,队列不再开启新的下载操作,再向队列里面添加的操作也不会被开启,但是正在执行的操作依然继续执行。

下面我们就来看一下下载方法的实现细节:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
    __block SDWebImageDownloaderOperation *operation;
    __weak __typeof(self)wself = self;

    [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
        NSTimeInterval timeoutInterval = wself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

       // 创建请求对象,并根据 options 参数设置其属性  
    // 为了避免潜在的重复缓存(NSURLCache + SDImageCache),如果没有明确告知需要缓存,则禁用图片请求的缓存操作 
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        if (wself.headersFilter) {
            request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = wself.HTTPHeaders;
        }
// 创建 SDWebImageDownloaderOperation 操作对象,传入进度回调、完成回调、取消回调  
    // 配置信息包括是否需要认证、优先级
        operation = [[wself.operationClass alloc] initWithRequest:request
                                                        inSession:self.session
                                                          options:options
                                                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// 从管理器的 callbacksForURL 中找出该 URL 所有的进度处理回调并调用
// 将删除所有回调的block放到队列barrierQueue中使用barrier_sync方式执行,确保了在进行调用完成回调之前所有的使用url对应的回调的地方都是正确的数据。
                                                             SDWebImageDownloader *sself = wself;
                                                             if (!sself) return;
                                                             __block NSArray *callbacksForURL;
                                                             dispatch_sync(sself.barrierQueue, ^{
                                                                 callbacksForURL = [sself.URLCallbacks[url] copy];
                                                             });
                                                             for (NSDictionary *callbacks in callbacksForURL) {
                                                                 dispatch_async(dispatch_get_main_queue(), ^{
                                                                     SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
                                                                     if (callback) callback(receivedSize, expectedSize);
                                                                 });
                                                             }
                                                         }
                                                        completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
// 从管理器的 callbacksForURL 中找出该 URL 所有的完成处理回调并调用  
                               // 如果 finished 为 YES,则将该 url 对应的回调信息从 URLCallbacks 中删除 
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;
                                                            __block NSArray *callbacksForURL;
                                                            dispatch_barrier_sync(sself.barrierQueue, ^{
                                                                callbacksForURL = [sself.URLCallbacks[url] copy];
                                                                if (finished) {
                                                                    [sself.URLCallbacks removeObjectForKey:url];
                                                                }
                                                            });
                                                            for (NSDictionary *callbacks in callbacksForURL) {
                                                                SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                                                                if (callback) callback(image, data, error, finished);
                                                            }
                                                        }
                                                        cancelled:^{
// 取消操作将该 url 对应的回调信息从 URLCallbacks 中删除 
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;
                                                            dispatch_barrier_async(sself.barrierQueue, ^{
                                                                [sself.URLCallbacks removeObjectForKey:url];
                                                            });
                                                        }];
// 设置是否需要解压
        operation.shouldDecompressImages = wself.shouldDecompressImages;
        // 设置进行网络访问验证的凭据
        if (wself.urlCredential) {
            operation.credential = wself.urlCredential;
        } else if (wself.username && wself.password) {
            operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
        }
        // 根据下载选项SDWebImageDownloaderHighPriority设置优先级
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
// 将操作加入到操作队列 downloadQueue 中  
    // 如果是 LIFO 顺序,则将新的操作作为原队列中最后一个操作的依赖,然后将新操作设置为最后一个操作 
        [wself.downloadQueue addOperation:operation];
// 根据executionOrder设置操作的依赖关系
        if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            [wself.lastAddedOperation addDependency:operation];
            wself.lastAddedOperation = operation;
        }
    }];

    return operation;
}

重点就是addProgressCallback: completedBlock: forURL: createCallback:的执行了,SDWebImageDownloader将外部传来的进度回调、完成回调、url直接传递给这个方法,并实现创建下载操作的代码块作为这个方法的createCallback参数值。下面就看一下这个方法的实现细节:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    // 对URL判空,如果为空,直接执行完成回调。
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }
    /*
    对dispatch_barrier_sync函数的解释:
     向分配队列提交一个同步执行的barrier block。与dispatch_barrier_async不同,这个函数直到barrier block执行完毕才会返回,在当前队列调用这个函数会导致死锁。当barrier block被放进一个私有的并行队列后,它不会被立刻执行。实际为,队列会等待直到当前正在执行的blocks执行完毕。到那个时刻,队列才会自己执行barrier block。而任何放到 barrier block之后的block直到 barrier block执行完毕才会执行。
     传递的队列参数应该是你自己用dispatch_queue_create函数创建的一个并行队列。如果你传递一个串行队列或者全局并行队列,这个函数的行为和 dispatch_sync相同。
     与dispatch_barrier_async不同,它不会对目标队列进行强引用(retain操作)。因为调用这个方法是同步的,它“借用”了调用者的引用。而且,没有对block进行Block_copy操作。
     作为对其优化,这个函数会在可能的情况下在当前线程唤起barrier block。
     */
    
    // 为确保不会死锁,当前队列是另一个队列,而不能是self.barrierQueue。
    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }
        /*
        URLCallbacks字典类型key为NSURL类型,value为NSMutableArray类型,value只包含着一个元素,这个元素是一个NSMutableDictionary类型,它的key为NSString代表着回调类型,value为block,是对应的回调
        */
        // 同一时刻对相同url的多个下载请求只进行一次下载
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        self.URLCallbacks[url] = callbacksForURL;

        if (first) {
            createCallback();
            /* 解释
            若url第一次绑定它的回调,也就是第一次使用这个url创建下载任务,则执行一次创建回调。
            在创建回调中创建下载操作,dispatch_barrier_sync执行确保同一时间只有一个线程操作URLCallbacks属性,也就是确保了下面创建过程中在给operation传递回调的时候能取到正确的self.URLCallbacks[url]值。同时保证后面有相同的url再次创建时,if (!self.URLCallbacks[url])分支不再进入,first==NO,也就不再继续调用创建回调。这样就确保了同一个url对应的图片不会被重复下载。

            而下载器的完成回调中会将url从self.URLCallbacks中remove,虽然remove掉了,但是再次使用这个url进行下载图片的时候,Manager会向缓存中读取下载成功的图片了,而不是无脑地直接添加下载任务;即使之前的下载是失败的(也就是说没有缓存),这样继续添加下载任务也是合情合理的。
            // 因此准确地说,将这个block放到并行队列dispatch_barrier_sync执行确保了,同一个url的图片不会同一时刻进行多次下载.
            
            // 这样做还使得下载操作的创建同步进行,因为一个新的下载操作还没有创建完成,self.barrierQueue会继续等待它完成,然后才能执行下一个添加下载任务的block。所以说SD添加下载任务是同步的,而且都是在self.barrierQueue这个并行队列中,同步添加任务。这样也保证了根据executionOrder设置依赖关是正确的。换句话说如果创建下载任务不是使用dispatch_barrier_sync完成的,而是使用异步方法 ,虽然依次添加创建下载操作A、B、C的任务,但实际创建顺序可能为A、C、B,这样当executionOrder的值是SDWebImageDownloaderLIFOExecutionOrder,设置的操作依赖关系就变成了A依赖C,C依赖B
            // 但是添加之后的下载依然是在下载队列downloadQueue中异步执行,丝毫不会影响到下载效率。

            // 以上就是说了SD下载的关键点:创建下载任务在barrierQueue队列中,执行下载在downloadQueue队列中。
            */
        }
    });
}

关于SDWebImage的源码阅读就到这里结束,有什么不对的地方,欢迎指正。

相关资料:
通读SDWebImage①--总体梳理、下载和缓存
SDWebImage 源码阅读笔记(三)

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

推荐阅读更多精彩内容