Merge pull request #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
This commit is contained in:
DreamPiggy 2022-07-23 23:04:55 +08:00 committed by GitHub
commit 7d7debbd26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 57 additions and 8 deletions

View File

@ -662,6 +662,13 @@ static NSString * _defaultDiskCacheDirectory;
}
if (doneBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
// 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);
});
}

View File

@ -382,6 +382,11 @@ static id<SDImageLoader> _defaultImageLoader;
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;
if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) {

View File

@ -136,14 +136,13 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png";
XCTestExpectation *expectation = [self expectationWithDescription:@"queryCacheOperationForKey"];
UIImage *imageForTesting = [self testJPEGImage];
[[SDImageCache sharedImageCache] storeImage:imageForTesting forKey:kTestImageKeyJPEG completion:nil];
NSOperation *operation = [[SDImageCache sharedImageCache] queryCacheOperationForKey:kTestImageKeyJPEG done:^(UIImage *image, NSData *data, SDImageCacheType cacheType) {
id<SDWebImageOperation> operation = [[SDImageCache sharedImageCache] queryCacheOperationForKey:kTestImageKeyJPEG done:^(UIImage *image, NSData *data, SDImageCacheType cacheType) {
expect(image).to.equal(imageForTesting);
[[SDImageCache sharedImageCache] removeImageForKey:kTestImageKeyJPEG withCompletion:^{
[expectation fulfill];
}];
}];
expect(operation).toNot.beNil;
[operation start];
[self waitForExpectationsWithCommonTimeout];
}
@ -209,6 +208,26 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png";
[[SDImageCache sharedImageCache] removeImageFromMemoryForKey:kTestGIFURL];
}
- (void)test15CancelQueryShouldCallbackOnceInSync {
XCTestExpectation *expectation = [self expectationWithDescription:@"Cancel Query Should Callback Once In Sync"];
expectation.expectedFulfillmentCount = 1;
NSString *key = @"test15CancelQueryShouldCallbackOnceInSync";
[SDImageCache.sharedImageCache removeImageFromMemoryForKey:key];
[SDImageCache.sharedImageCache removeImageFromDiskForKey:key];
__block BOOL callced = NO;
SDImageCacheToken *token = [SDImageCache.sharedImageCache queryCacheOperationForKey:key done:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) {
callced = true;
[expectation fulfill]; // callback once fulfill once
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
expect(callced).beFalsy();
[token cancel]; // sync
expect(callced).beTruthy();
});
[self waitForExpectationsWithCommonTimeout];
}
- (void)test20InitialCacheSize{
expect([[SDImageCache sharedImageCache] totalDiskSize]).to.equal(0);
}

View File

@ -11,6 +11,14 @@
#import "SDWebImageTestCache.h"
#import "SDWebImageTestLoader.h"
// Keep strong references for object
@interface SDObjectContainer<ObjectType> : NSObject
@property (nonatomic, strong, readwrite) ObjectType object;
@end
@implementation SDObjectContainer
@end
@interface SDWebImageManagerTests : SDTestCase
@end
@ -27,14 +35,24 @@
NSURL *originalImageURL = [NSURL URLWithString:kTestJPEGURL];
[[SDWebImageManager sharedManager] loadImageWithURL:originalImageURL
options:SDWebImageRefreshCached
progress:nil
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
[SDImageCache.sharedImageCache removeImageFromMemoryForKey:kTestJPEGURL];
[SDImageCache.sharedImageCache removeImageFromDiskForKey:kTestJPEGURL];
SDObjectContainer<SDWebImageCombinedOperation *> *container = [SDObjectContainer new];
container.object = [[SDWebImageManager sharedManager] loadImageWithURL:originalImageURL
options:0
progress:nil
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
expect(image).toNot.beNil();
expect(error).to.beNil();
expect(originalImageURL).to.equal(imageURL);
// When download, the cache operation will reset to nil since it's always finished
SDWebImageCombinedOperation *operation = container.object;
expect(container).notTo.beNil();
expect(operation.cacheOperation).beNil();
expect(operation.loaderOperation).notTo.beNil();
container.object = nil;
[expectation fulfill];
expectation = nil;
}];
@ -49,7 +67,7 @@
NSURL *originalImageURL = [NSURL URLWithString:@"http://static2.dmcdn.net/static/video/656/177/44771656:jpeg_preview_small.png"];
[[SDWebImageManager sharedManager] loadImageWithURL:originalImageURL
options:SDWebImageRefreshCached
options:0
progress:nil
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
expect(image).to.beNil();