学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

SDWebImage

消息传递栈

从官方 github Demo开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//MasterViewController.m

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";

static UIImage *placeholderImage = nil;
if (!placeholderImage) {
placeholderImage = [UIImage imageNamed:@"placeholder"];
}

MyCustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[MyCustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.customImageView.sd_imageTransition = SDWebImageTransition.fadeTransition;
cell.customImageView.sd_imageIndicator = SDWebImageActivityIndicator.grayIndicator;
}

cell.customTextLabel.text = [NSString stringWithFormat:@"Image #%ld", (long)indexPath.row];
__weak SDAnimatedImageView *imageView = cell.customImageView;
[imageView sd_setImageWithURL:[NSURL URLWithString:self.objects[indexPath.row]]
placeholderImage:placeholderImage
options:0
context:@{SDWebImageContextImageThumbnailPixelSize : @(CGSizeMake(180, 120))}
progress:nil
completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
SDWebImageCombinedOperation *operation = [imageView sd_imageLoadOperationForKey:imageView.sd_latestOperationKey];
SDWebImageDownloadToken *token = operation.loaderOperation;
if (@available(iOS 10.0, *)) {
NSURLSessionTaskMetrics *metrics = token.metrics;
if (metrics) {
printf("Metrics: %s download in (%f) seconds\n", [imageURL.absoluteString cStringUsingEncoding:NSUTF8StringEncoding], metrics.taskInterval.duration);
}
}
}];
return cell;
}

从上面的UML类图可以看出,整个SDK消息调用的过程可以简单理解为给 SDWebImageCombinedOperation *对象赋值,其中成员对象cacheOperationSDImageCache类实例方法返回

1
- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;

成员对象loaderOperationSDWebImageDownloader类实例方法返回

1
2
3
4
5
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

UIView(WebCache)

入口函数[imageView sd_setImageWithURL:...placeholderImage:... 通过拼凑默认入参的方式最终进入核心函数sd_internalSetImageWithURL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
- (nullable id<SDWebImageOperation>)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock{
if (context) {
...
///SetImageOperationKey
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
if (!validOperationKey) {
// pass through the operation key to downstream, which can used for tracing operation or image view class
validOperationKey = NSStringFromClass([self class]);
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
context = [mutableContext copy];
}
self.sd_latestOperationKey = validOperationKey;
[self sd_cancelImageLoadOperationWithKey:validOperationKey];//从队列中取消正在进行的操作

...
SDWebImageManager *manager = context[SDWebImageContextCustomManager];
if (!manager) {
manager = [SDWebImageManager sharedManager];
} else {
// remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextCustomManager] = nil;
context = [mutableContext copy];
}

...
id <SDWebImageOperation> operation = nil;

if (url) {
// reset the progress

...
operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {

...
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
}
return operation;
}

这里可看出id <SDWebImageOperation> operation的值由loadImageWithURL 返回,类型为SDWebImageCombinedOperation *

SDWebImageManager -> loadImageWithURL

属性值的设置

可以看到,这个方法主要实现UIView(WebCache)里的几个属性方法值的设置

  • 通过分类的方式UIView (WebCacheOperation)关联了属性NSMapTable<NSString *, id<SDWebImageOperation>> sd_operationDictionary;

    key = sd_latestOperationKey

  • UIView (WebCacheState)关联属性NSMutableDictionary<NSString *, SDWebImageLoadState *> sd_imageLoadStateDictionary;

    key = sd_latestOperationKey

重复调用方法

前面文章中提到,当一个cell.imageView快速上下滑动,而刚好进入缓存池中时,如果不取消未完成的图片operation,会造成系统资源浪费,还可能由于这个异步操作不知道返回到哪个UITableViewCell而导致UITableView一些诡异的行为。

1
2
self.sd_latestOperationKey = validOperationKey;
[self sd_cancelImageLoadOperationWithKey:validOperationKey];

可以看到当同一个imageView实例对象重复调用入口方法时,会根据sd_latestOperationKey去取消未完成的operation

SDWebImageManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock {

SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;

...
// Start the entry to load image from cache, the longest steps are below
// Steps without transformer:
// 1. query image from cache, miss
// 2. download data and image
// 3. store image to cache

// Steps with transformer:
// 1. query transformed image from cache, miss
// 2. query original image from cache, miss
// 3. download data and image
// 4. do transform in CPU
// 5. store original image to cache
// 6. store transformed image to cache
[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];

}

这个方法通过new一个SDWebImageCombinedOperation *operation实例,然后作为参数传入下一个方法中。

self -> callCacheProcessForOperation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Query normal cache process
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Grab the image cache to use
id<SDImageCache> imageCache = context[SDWebImageContextImageCache];
if (!imageCache) {
imageCache = self.imageCache;
}
// Get the query cache type
...

// Check whether we should query cache
BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
if (shouldQueryCache) {
// transformed cache key
NSString *key = [self cacheKeyForURL:url context:context];
@weakify(operation);
operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
@strongify(operation);
if (!operation || operation.isCancelled) {
// Image combined operation cancelled by user
...
return;
} else if (!cachedImage) {
NSString *originKey = [self originalCacheKeyForURL:url context:context];
BOOL mayInOriginalCache = ![key isEqualToString:originKey];
// Have a chance to query original cache instead of downloading, then applying transform
// Thumbnail decoding is done inside SDImageCache's decoding part, which does not need post processing for transform
if (mayInOriginalCache) {
[self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
return;
}
}
// Continue download process 缓存miss 进行下载
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}];
} else {
// Continue download process
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
}

这个方法主要是给operation.cacheOperation赋值,通过id<SDImageCache>queryImageForKey ...方法,返回类型为nullable SDImageCacheToken *

这里会有两种情况,其一是需要查询并获取缓存:首先去SDImageCache 类中查询缓存,包括Memory缓存和Disk缓存,如果缓存命中miss,会进入下载callDownloadProcessForOperation ,第二种是直接去下载更新缓存callDownloadProcessForOperation

  1. SDImageCache -> queryImageForKey -> queryCacheOperationForKey
  2. self -> callDownloadProcessForOperation

注意
默认imageCache = self.imageCache;,默认情况下self.imageCache;是在SDWebImageManager单例初始化时init方法中[SDImageCache sharedImageCache]赋值的。

transformed cache key

看下这个方法的功能实现 NSString *key = [self cacheKeyForURL:url context:context];

1
2
3
4
5
6
7
8
9
10
11
12
/*
Pragma: Context
{
animatedImageClass = SDAnimatedImage;
imageThumbnailPixelSize = "NSSize: {180, 120}";
setImageOperationKey = SDAnimatedImageView;
}
*/
Printing description of In url:
http://www.httpwatch.com/httpgallery/authentication/authenticatedimage/default.aspx?0.35786508303135633
Printing description of Out key:
http://www.httpwatch.com/httpgallery/authentication/authenticatedimage/default-Thumbnail(%7B180.000000,120.000000%7D,1).aspx?0.35786508303135633

SDImageCache缓存查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {

...
// First check the in-memory cache...
UIImage *image;
if (queryCacheType != SDImageCacheTypeDisk) {
image = [self imageFromMemoryCacheForKey:key];
}

...
// Second check the disk cache...
SDCallbackQueue *queue = context[SDWebImageContextCallbackQueue];
SDImageCacheToken *operation = [[SDImageCacheToken alloc] initWithDoneBlock:doneBlock];
operation.key = key;
operation.callbackQueue = queue;
// Check whether we need to synchronously query disk
// 1. in-memory cache hit & memoryDataSync
// 2. in-memory cache miss & diskDataSync
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));
NSData* (^queryDiskDataBlock)(void) = ^NSData* {
@synchronized (operation) {
if (operation.isCancelled) {
return nil;
}
}

return [self diskImageDataBySearchingAllPathsForKey:key];
};

UIImage* (^queryDiskImageBlock)(NSData*) = ^UIImage*(NSData* diskData) {
...
};

// Query in ioQueue to keep IO-safe
if (shouldQueryDiskSync) {
__block NSData* diskData;
__block UIImage* diskImage;
dispatch_sync(self.ioQueue, ^{
diskData = queryDiskDataBlock();
diskImage = queryDiskImageBlock(diskData);
});
if (doneBlock) {
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
}
} else {
dispatch_async(self.ioQueue, ^{
NSData* diskData = queryDiskDataBlock();
UIImage* diskImage = queryDiskImageBlock(diskData);
@synchronized (operation) {
if (operation.isCancelled) {
return;
}
}
if (doneBlock) {
[(queue ?: SDCallbackQueue.mainQueue) async:^{
// Dispatch from IO queue to main queue need time, user may call cancel during the dispatch timing
// This check is here to avoid double callback (one is from `SDImageCacheToken` in sync)
@synchronized (operation) {
if (operation.isCancelled) {
return;
}
}
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
}];
}
});
}

return operation;
}

这个方法主要是实例化SDImageCacheToken *operation并返回给上一层

这个 imageFromMemoryCacheForKey 方法会在 SDWebImageCache 维护的缓存 memCache 中查找是否有对应的数据, 而 memCache 就是一个 NSCache.如果在内存中并没有找到图片的缓存的话, 就需要在磁盘中寻找

在这里会调用一个方法 diskImageForKey,这里文件名字的存储使用 MD5 处理过后的文件名.

1
2
3
4
// SDImageCache
// cachedFileNameForKey: #6

CC_MD5(str, (CC_LONG)strlen(str), r);

如果在磁盘中查找到对应的图片, 我们会将它复制到内存中, 以便下次的使用.

1
2
3
4
5
6
7
8
// SDImageCache
// queryDiskCacheForKey:done: #24

UIImage *diskImage = [self diskImageForKey:key];
if (diskImage) {
CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale;
[self.memCache setObject:diskImage forKey:key cost:cost];
}

二级缓存查询,Memory和Disk,缓存未命中,会进入下载流程

下载更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Download process
- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
cachedImage:(nullable UIImage *)cachedImage
cachedData:(nullable NSData *)cachedData
cacheType:(SDImageCacheType)cacheType
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Mark the cache operation end
@synchronized (operation) {
operation.cacheOperation = nil;
}

// Grab the image loader to use
id<SDImageLoader> imageLoader = context[SDWebImageContextImageLoader];
if (!imageLoader) {
imageLoader = self.imageLoader;
}

// Check whether we should download image from network
BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
...
if (shouldDownload) {
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:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];
// Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.
...
}

@weakify(operation);
operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
...
// Continue transform process
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData cacheType:SDImageCacheTypeNone finished:finished completed:completedBlock];
}

if (finished) {
[self safelyRemoveOperationFromRunning:operation];
}
}];
}
...
}

可以看到operation.loaderOperation是由id<SDImageLoader> imageLoader 代理执行的,默认使用的就是SDWebImageDownloader

SDWebImageDownloader

1
2
3
4
- (id<SDWebImageOperation>)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock {
...
return [self downloadImageWithURL:url options:downloaderOptions context:context progress:progressBlock completed:completedBlock];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {

id downloadOperationCancelToken;
// When different thumbnail size download with same url, we need to make sure each callback called with desired size
id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
NSString *cacheKey;
if (cacheKeyFilter) {
cacheKey = [cacheKeyFilter cacheKeyForURL:url];
} else {
cacheKey = url.absoluteString;
}
SDImageCoderOptions *decodeOptions = SDGetDecodeOptionsFromContext(context, [self.class imageOptionsFromDownloaderOptions:options], cacheKey);
SD_LOCK(_operationsLock);
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
// There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.
BOOL shouldNotReuseOperation;
if (operation) {
@synchronized (operation) {
shouldNotReuseOperation = operation.isFinished || operation.isCancelled;
}
} else {
shouldNotReuseOperation = YES;
}
if (shouldNotReuseOperation) {
operation = [self createDownloaderOperationWithUrl:url options:options context:context];
if (!operation) {
SD_UNLOCK(_operationsLock);
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Downloader operation is nil"}];
completedBlock(nil, nil, error, YES);
}
return nil;
}
@weakify(self);
operation.completionBlock = ^{
@strongify(self);
if (!self) {
return;
}
SD_LOCK(self->_operationsLock);
[self.URLOperations removeObjectForKey:url];
SD_UNLOCK(self->_operationsLock);
};
[self.URLOperations setObject:operation forKey:url];
// Add the handlers before submitting to operation queue, avoid the race condition that operation finished before setting handlers.
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions];
// 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];
} else {
// When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue)
// So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes.
@synchronized (operation) {
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions];
}
}
SD_UNLOCK(_operationsLock);

SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
token.url = url;
token.request = operation.request;
token.downloadOperationCancelToken = downloadOperationCancelToken;

return token;
}

这个方法主要是实例化SDWebImageDownloadToken *token,并作为方法返回值返回。真正下载operation ,其实是通过方法createDownloaderOperationWithUrl 生成,然后添加到队列 [self.downloadQueue addOperation:operation];执行的。

  • 真正的operation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context {
...
// Operation Class
Class operationClass = self.config.operationClass;
if (!operationClass) {
operationClass = [SDWebImageDownloaderOperation class];
}
NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
...

}

return operation;
}

这个方法主要是实例化NSOperation<SDWebImageDownloaderOperation> *operation并作为返回值返回。

预加载SDWebImagePrefetcher

如何使用,demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
- (void)test04PrefetchWithMultipleArrayDifferentQueueWorks {
XCTestExpectation *expectation = [self expectationWithDescription:@"Prefetch with multiple array at different queue failed"];

NSArray *imageURLs1 = @[@"https://via.placeholder.com/20x20.jpg",
@"https://via.placeholder.com/30x30.jpg"];
NSArray *imageURLs2 = @[@"https://via.placeholder.com/30x30.jpg",
@"https://via.placeholder.com/40x40.jpg"];
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t queue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
__block int numberOfPrefetched1 = 0;
__block int numberOfPrefetched2 = 0;
__block BOOL prefetchFinished1 = NO;
__block BOOL prefetchFinished2 = NO;

// Clear the disk cache to make it more realistic for multi-thread environment
[[SDImageCache sharedImageCache] clearDiskOnCompletion:^{
dispatch_async(queue1, ^{
[[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:imageURLs1 progress:^(NSUInteger noOfFinishedUrls, NSUInteger noOfTotalUrls) {
numberOfPrefetched1 += 1;
} completed:^(NSUInteger noOfFinishedUrls, NSUInteger noOfSkippedUrls) {
expect(numberOfPrefetched1).to.equal(noOfFinishedUrls);
prefetchFinished1 = YES;
// both completion called
if (prefetchFinished1 && prefetchFinished2) {
[expectation fulfill];
}
}];
});
dispatch_async(queue2, ^{
[[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:imageURLs2 progress:^(NSUInteger noOfFinishedUrls, NSUInteger noOfTotalUrls) {
numberOfPrefetched2 += 1;
} completed:^(NSUInteger noOfFinishedUrls, NSUInteger noOfSkippedUrls) {
expect(numberOfPrefetched2).to.equal(noOfFinishedUrls);
prefetchFinished2 = YES;
// both completion called
if (prefetchFinished1 && prefetchFinished2) {
[expectation fulfill];
}
}];
});
}];

[self waitForExpectationsWithCommonTimeout];
}

入口方法prefetchURLs:....progress:或者其他重栽方法,通过传默认形参的方式,最终会调用下面方法进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock {
if (!urls || urls.count == 0) {
if (completionBlock) {
completionBlock(0, 0);
}
return nil;
}
SDWebImagePrefetchToken *token = [SDWebImagePrefetchToken new];
...
[self addRunningToken:token];
[self startPrefetchWithToken:token];

return token;
}

返回对象类型为SDWebImagePrefetchToken*,可以看下SDWebImagePrefetchToken的定义

  • SDWebImagePrefetchToken
1
2
3
4
5
6
7
8
9
10
11
12
13
@interface SDWebImagePrefetchToken : NSObject <SDWebImageOperation>

/**
* Cancel the current prefetching.
*/
- (void)cancel;

/**
list of URLs of current prefetching.
*/
@property (nonatomic, copy, readonly, nullable) NSArray<NSURL *> *urls;

@end

addRunningToken 会把当前token添加到容器NSMutableSet<SDWebImagePrefetchToken *> *runningTokens;

startPrefetchWithToken

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (void)startPrefetchWithToken:(SDWebImagePrefetchToken * _Nonnull)token {
for (NSURL *url in token.urls) {
@weakify(self);
SDAsyncBlockOperation *prefetchOperation = [SDAsyncBlockOperation blockOperationWithBlock:^(SDAsyncBlockOperation * _Nonnull asyncOperation) {
@strongify(self);
if (!self || asyncOperation.isCancelled) {
return;
}
id<SDWebImageOperation> operation = [self.manager loadImageWithURL:url options:token.options context:token.context progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
@strongify(self);
if (!self) {
return;
}
if (!finished) {
return;
}
...
// Current operation finished
[self callProgressBlockForToken:token imageURL:imageURL];
...
[asyncOperation complete];
}];

...
[token.loadOperations addPointer:(__bridge void *)operation];
...
}];
[token.prefetchOperations addPointer:(__bridge void *)prefetchOperation];
[self.prefetchQueue addOperation:prefetchOperation];
}
}

这里重点看下SDAsyncBlockOperation的初始化,先看下定义

1
2
3
4
5
6
7
8
9
10
typedef void (^SDAsyncBlock)(SDAsyncBlockOperation * __nonnull asyncOperation);

/// A async block operation, success after you call `completer` (not like `NSBlockOperation` which is for sync block, success on return)
@interface SDAsyncBlockOperation : NSOperation

- (nonnull instancetype)initWithBlock:(nonnull SDAsyncBlock)block;
+ (nonnull instancetype)blockOperationWithBlock:(nonnull SDAsyncBlock)block;
- (void)complete;

@end

按照文档注释来看,SDAsyncBlockOperation是一个异步operation ,当你调用complete 方法时结束。

内存管理优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@interface SDAsyncBlockOperation ()

@property (nonatomic, copy, nonnull) SDAsyncBlock executionBlock;

@end

@implementation SDAsyncBlockOperation
- (nonnull instancetype)initWithBlock:(nonnull SDAsyncBlock)block {
self = [super init];
if (self) {
self.executionBlock = block;
}
return self;
}

+ (nonnull instancetype)blockOperationWithBlock:(nonnull SDAsyncBlock)block {
SDAsyncBlockOperation *operation = [[SDAsyncBlockOperation alloc] initWithBlock:block];
return operation;
}
@end

- (void)start {
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
return;
}
self.finished = NO;
self.executing = YES;
}
SDAsyncBlock executionBlock = self.executionBlock;
if (executionBlock) {
@weakify(self);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@strongify(self);
if (!self) return;
executionBlock(self);
});
}
}
  • 这里是否存在内存泄露?

self持有executionBlock,executionBlock(self);并没有持有self。不过这种实现方式看着很别扭,在block里返回self,完全可以在方法

1
2
3
SDAsyncBlockOperation *prefetchOperation = [SDAsyncBlockOperation blockOperationWithBlock:^(SDAsyncBlockOperation * _Nonnull asyncOperation) {
...
}
  1. 用block + nil方式
1
2
3
SDAsyncBlockOperation * __block  prefetchOperation = [SDAsyncBlockOperation blockOperationWithBlock:^() {
...
prefetchOperation = nil;
  1. 用block + weak方式
1
2
SDAsyncBlockOperation * __block __weak prefetchOperation = [SDAsyncBlockOperation blockOperationWithBlock:^() {
...

参数context

1
2
typedef NSString * SDWebImageContextOption NS_EXTENSIBLE_STRING_ENUM;
typedef NSDictionary<SDWebImageContextOption, id> SDWebImageContext;

SDWebImageContextSetImageOperationKey

一个字符串,用作 view category 的操作键,用于存储图像加载operation。这是用于视图实例,它支持不同的图像加载过程(process)。如果为nil,将使用类名作为操作键。(NSString *)

1
2
3
@interface UIView (WebCache)
@property (nonatomic, strong, readonly, nullable) NSString *sd_latestOperationKey;//获取当前图像操作键。操作键用于标识一个视图实例(如UIButton)的不同查询
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[imageView sd_setImageWithURL:[NSURL URLWithString:self.objects[indexPath.row]]
placeholderImage:placeholderImage
options:0
context:@{SDWebImageContextImageThumbnailPixelSize : @(CGSizeMake(180, 120))}
progress:nil
completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
SDWebImageCombinedOperation *operation = [imageView sd_imageLoadOperationForKey:imageView.sd_latestOperationKey];
SDWebImageDownloadToken *token = operation.loaderOperation;
if (@available(iOS 10.0, *)) {
NSURLSessionTaskMetrics *metrics = token.metrics;
if (metrics) {
printf("Metrics: %s download in (%f) seconds\n", [imageURL.absoluteString cStringUsingEncoding:NSUTF8StringEncoding], metrics.taskInterval.duration);
}
}
}];

应用场景

自定义缓存key

默认是以图片url作为key,有点情况,比如url会动态改变的情形,比如?…携带参数的情况,或者用了云服务,url会动态改变的情形,需要自定义key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (void)imageView:(UIImageView *)imageView displayWithKey:(NSString *)key displayWithUrl:(NSString *)urlString{
UIImage *cacheImage = [[SDImageCache sharedImageCache] imageFromCacheForKey:key];
if (cacheImage != nil) {
imageView.image = cacheImage;
} else {
NSURL *url = [NSURL URLWithString:urlString];
//通过context参数指定存储的key,指定的key为截取urlString中"?"前面的内容
SDWebImageCacheKeyFilter *cacheKeyFilter = [SDWebImageCacheKeyFilter cacheKeyFilterWithBlock:^NSString * _Nullable(NSURL * _Nonnull imageURL) {
if ([url isEqual:imageURL]) {
return key;
} else {
return url.absoluteString;
}
}];
SDWebImageContext *context = [NSDictionary dictionaryWithObject:cacheKeyFilter forKey:SDWebImageContextCacheKeyFilter];
[imageView sd_setImageWithURL:[NSURL URLWithString:cacheImage != nil ? key : urlString] placeholderImage:[UIImage imageNamed:@"head_default"] options:0 context:context];
}
}

添加自定义disk缓存路径

1
2
3
4
5
NSString *bundledPath = [[NSBundle mainBundle].resourcePath stringByAppendingPathComponent:@"CustomPathImages"];
[SDImageCache sharedImageCache].additionalCachePathBlock = ^NSString * _Nullable(NSString * _Nonnull key) {
NSString *fileName = [[SDImageCache sharedImageCache] cachePathForKey:key].lastPathComponent;
return [bundledPath stringByAppendingPathComponent:fileName.stringByDeletingPathExtension];
};

http://assets.sbnation.com/assets/2512203/dogflops.gif缓存key经过md5后匹配

参考

  1. md5 online
  2. SDWebImage Demo
  3. sdwebimage
  4. 一道Block面试题的深入挖掘