Skip to content

Commit 7d7debb

Browse files
authored
Merge pull request SDWebImage#3380 from dreampiggy/bugfix_edge_case_cancel_cache_callback_twice
Fix the rare case when cancel an async disk cache query may cause twice callback
2 parents b88d576 + bcaf918 commit 7d7debb

File tree

4 files changed

+57
-8
lines changed

4 files changed

+57
-8
lines changed

SDWebImage/Core/SDImageCache.m

+7
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,13 @@ - (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)k
662662
}
663663
if (doneBlock) {
664664
dispatch_async(dispatch_get_main_queue(), ^{
665+
// Dispatch from IO queue to main queue need time, user may call cancel during the dispatch timing
666+
// This check is here to avoid double callback (one is from `SDImageCacheToken` in sync)
667+
@synchronized (operation) {
668+
if (operation.isCancelled) {
669+
return;
670+
}
671+
}
665672
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
666673
});
667674
}

SDWebImage/Core/SDWebImageManager.m

+5
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,11 @@ - (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)o
382382
cacheType:(SDImageCacheType)cacheType
383383
progress:(nullable SDImageLoaderProgressBlock)progressBlock
384384
completed:(nullable SDInternalCompletionBlock)completedBlock {
385+
// Mark the cache operation end
386+
@synchronized (operation) {
387+
operation.cacheOperation = nil;
388+
}
389+
385390
// Grab the image loader to use
386391
id<SDImageLoader> imageLoader;
387392
if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) {

Tests/Tests/SDImageCacheTests.m

+21-2
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,13 @@ - (void)test09RetrieveImageThroughNSOperation {
136136
XCTestExpectation *expectation = [self expectationWithDescription:@"queryCacheOperationForKey"];
137137
UIImage *imageForTesting = [self testJPEGImage];
138138
[[SDImageCache sharedImageCache] storeImage:imageForTesting forKey:kTestImageKeyJPEG completion:nil];
139-
NSOperation *operation = [[SDImageCache sharedImageCache] queryCacheOperationForKey:kTestImageKeyJPEG done:^(UIImage *image, NSData *data, SDImageCacheType cacheType) {
139+
id<SDWebImageOperation> operation = [[SDImageCache sharedImageCache] queryCacheOperationForKey:kTestImageKeyJPEG done:^(UIImage *image, NSData *data, SDImageCacheType cacheType) {
140140
expect(image).to.equal(imageForTesting);
141141
[[SDImageCache sharedImageCache] removeImageForKey:kTestImageKeyJPEG withCompletion:^{
142142
[expectation fulfill];
143143
}];
144144
}];
145145
expect(operation).toNot.beNil;
146-
[operation start];
147146
[self waitForExpectationsWithCommonTimeout];
148147
}
149148

@@ -209,6 +208,26 @@ - (void)test14QueryCacheFirstFrameOnlyHitMemoryCache {
209208
[[SDImageCache sharedImageCache] removeImageFromMemoryForKey:kTestGIFURL];
210209
}
211210

211+
- (void)test15CancelQueryShouldCallbackOnceInSync {
212+
XCTestExpectation *expectation = [self expectationWithDescription:@"Cancel Query Should Callback Once In Sync"];
213+
expectation.expectedFulfillmentCount = 1;
214+
NSString *key = @"test15CancelQueryShouldCallbackOnceInSync";
215+
[SDImageCache.sharedImageCache removeImageFromMemoryForKey:key];
216+
[SDImageCache.sharedImageCache removeImageFromDiskForKey:key];
217+
__block BOOL callced = NO;
218+
SDImageCacheToken *token = [SDImageCache.sharedImageCache queryCacheOperationForKey:key done:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) {
219+
callced = true;
220+
[expectation fulfill]; // callback once fulfill once
221+
}];
222+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
223+
expect(callced).beFalsy();
224+
[token cancel]; // sync
225+
expect(callced).beTruthy();
226+
});
227+
228+
[self waitForExpectationsWithCommonTimeout];
229+
}
230+
212231
- (void)test20InitialCacheSize{
213232
expect([[SDImageCache sharedImageCache] totalDiskSize]).to.equal(0);
214233
}

Tests/Tests/SDWebImageManagerTests.m

+24-6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@
1111
#import "SDWebImageTestCache.h"
1212
#import "SDWebImageTestLoader.h"
1313

14+
// Keep strong references for object
15+
@interface SDObjectContainer<ObjectType> : NSObject
16+
@property (nonatomic, strong, readwrite) ObjectType object;
17+
@end
18+
19+
@implementation SDObjectContainer
20+
@end
21+
1422
@interface SDWebImageManagerTests : SDTestCase
1523

1624
@end
@@ -27,14 +35,24 @@ - (void)test02ThatDownloadInvokesCompletionBlockWithCorrectParamsAsync {
2735

2836
NSURL *originalImageURL = [NSURL URLWithString:kTestJPEGURL];
2937

30-
[[SDWebImageManager sharedManager] loadImageWithURL:originalImageURL
31-
options:SDWebImageRefreshCached
32-
progress:nil
33-
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
38+
[SDImageCache.sharedImageCache removeImageFromMemoryForKey:kTestJPEGURL];
39+
[SDImageCache.sharedImageCache removeImageFromDiskForKey:kTestJPEGURL];
40+
SDObjectContainer<SDWebImageCombinedOperation *> *container = [SDObjectContainer new];
41+
container.object = [[SDWebImageManager sharedManager] loadImageWithURL:originalImageURL
42+
options:0
43+
progress:nil
44+
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
3445
expect(image).toNot.beNil();
3546
expect(error).to.beNil();
3647
expect(originalImageURL).to.equal(imageURL);
37-
48+
49+
// When download, the cache operation will reset to nil since it's always finished
50+
SDWebImageCombinedOperation *operation = container.object;
51+
expect(container).notTo.beNil();
52+
expect(operation.cacheOperation).beNil();
53+
expect(operation.loaderOperation).notTo.beNil();
54+
container.object = nil;
55+
3856
[expectation fulfill];
3957
expectation = nil;
4058
}];
@@ -49,7 +67,7 @@ - (void)test03ThatDownloadWithIncorrectURLInvokesCompletionBlockWithAnErrorAsync
4967
NSURL *originalImageURL = [NSURL URLWithString:@"http://static2.dmcdn.net/static/video/656/177/44771656:jpeg_preview_small.png"];
5068

5169
[[SDWebImageManager sharedManager] loadImageWithURL:originalImageURL
52-
options:SDWebImageRefreshCached
70+
options:0
5371
progress:nil
5472
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
5573
expect(image).to.beNil();

0 commit comments

Comments
 (0)