Skip to content

Commit a052164

Browse files
authored
Merge pull request SDWebImage#3477 from dreampiggy/bugfix/ensure_downloader_callback_atomic
Ensure the Downloader and DownloaderOperation callback the completion in atomic and never miss one
2 parents f25cc6b + 1ec6cd0 commit a052164

File tree

9 files changed

+115
-62
lines changed

9 files changed

+115
-62
lines changed

Examples/SDWebImage Demo/MasterViewController.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibB
7474
@"https://raw.githubusercontent.com/icons8/flat-color-icons/master/pdf/stack_of_photos.pdf",
7575
@"https://nr-platform.s3.amazonaws.com/uploads/platform/published_extension/branding_icon/275/AmazonS3.png",
7676
@"https://res.cloudinary.com/dwpjzbyux/raw/upload/v1666474070/RawDemo/raw_vebed5.NEF",
77-
@"http://via.placeholder.com/200x200.jpg",
77+
@"https://via.placeholder.com/200x200.jpg",
7878
nil];
7979

8080
for (int i=1; i<25; i++) {

SDWebImage/Core/SDAnimatedImagePlayer.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ - (NSOperationQueue *)fetchQueue {
9090
if (!_fetchQueue) {
9191
_fetchQueue = [[NSOperationQueue alloc] init];
9292
_fetchQueue.maxConcurrentOperationCount = 1;
93+
_fetchQueue.name = @"com.hackemist.SDAnimatedImagePlayer.fetchQueue";
9394
}
9495
return _fetchQueue;
9596
}

SDWebImage/Core/SDImageCache.m

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ @interface SDImageCache ()
6363
@property (nonatomic, strong, readwrite, nonnull) id<SDDiskCache> diskCache;
6464
@property (nonatomic, copy, readwrite, nonnull) SDImageCacheConfig *config;
6565
@property (nonatomic, copy, readwrite, nonnull) NSString *diskCachePath;
66-
@property (nonatomic, strong, nullable) dispatch_queue_t ioQueue;
66+
@property (nonatomic, strong, nonnull) dispatch_queue_t ioQueue;
6767

6868
@end
6969

@@ -118,7 +118,7 @@ - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
118118

119119
// Create IO queue
120120
dispatch_queue_attr_t ioQueueAttributes = _config.ioQueueAttributes;
121-
_ioQueue = dispatch_queue_create("com.hackemist.SDImageCache", ioQueueAttributes);
121+
_ioQueue = dispatch_queue_create("com.hackemist.SDImageCache.ioQueue", ioQueueAttributes);
122122
NSAssert(_ioQueue, @"The IO queue should not be nil. Your configured `ioQueueAttributes` may be wrong");
123123

124124
// Init the memory cache
@@ -508,10 +508,6 @@ - (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *
508508

509509
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
510510
NSData *data = [self diskImageDataForKey:key];
511-
return [self diskImageForKey:key data:data];
512-
}
513-
514-
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data {
515511
return [self diskImageForKey:key data:data options:0 context:nil];
516512
}
517513

SDWebImage/Core/SDWebImageDownloader.m

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,30 @@
1313
#import "SDWebImageCacheKeyFilter.h"
1414
#import "SDImageCacheDefine.h"
1515
#import "SDInternalMacros.h"
16+
#import "objc/runtime.h"
1617

1718
NSNotificationName const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
1819
NSNotificationName const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification";
1920
NSNotificationName const SDWebImageDownloadStopNotification = @"SDWebImageDownloadStopNotification";
2021
NSNotificationName const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification";
2122

2223
static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
24+
static void * SDWebImageDownloaderOperationKey = &SDWebImageDownloaderOperationKey;
25+
26+
BOOL SDWebImageDownloaderOperationGetCompleted(id<SDWebImageDownloaderOperation> operation) {
27+
NSCParameterAssert(operation);
28+
NSNumber *value = objc_getAssociatedObject(operation, SDWebImageDownloaderOperationKey);
29+
if (value) {
30+
return value.boolValue;
31+
} else {
32+
return NO;
33+
}
34+
}
35+
36+
void SDWebImageDownloaderOperationSetCompleted(id<SDWebImageDownloaderOperation> operation, BOOL isCompleted) {
37+
NSCParameterAssert(operation);
38+
objc_setAssociatedObject(operation, SDWebImageDownloaderOperationKey, @(isCompleted), OBJC_ASSOCIATION_RETAIN);
39+
}
2340

2441
@interface SDWebImageDownloadToken ()
2542

@@ -99,7 +116,7 @@ - (instancetype)initWithConfig:(SDWebImageDownloaderConfig *)config {
99116
[_config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxConcurrentDownloads)) options:0 context:SDWebImageDownloaderContext];
100117
_downloadQueue = [NSOperationQueue new];
101118
_downloadQueue.maxConcurrentOperationCount = _config.maxConcurrentDownloads;
102-
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
119+
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader.downloadQueue";
103120
_URLOperations = [NSMutableDictionary new];
104121
NSMutableDictionary<NSString *, NSString *> *headerDictionary = [NSMutableDictionary dictionary];
105122
NSString *userAgent = nil;
@@ -219,7 +236,8 @@ - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
219236
SDImageCoderOptions *decodeOptions = SDGetDecodeOptionsFromContext(context, [self.class imageOptionsFromDownloaderOptions:options], cacheKey);
220237
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
221238
// There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.
222-
if (!operation || operation.isFinished || operation.isCancelled) {
239+
BOOL shouldNotReuseOperation = !operation || operation.isFinished || operation.isCancelled || SDWebImageDownloaderOperationGetCompleted(operation);
240+
if (shouldNotReuseOperation) {
223241
operation = [self createDownloaderOperationWithUrl:url options:options context:context];
224242
if (!operation) {
225243
SD_UNLOCK(_operationsLock);
@@ -499,6 +517,10 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didComp
499517

500518
// Identify the operation that runs this task and pass it the delegate method
501519
NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
520+
if (dataOperation) {
521+
// Mark the downloader operation `isCompleted = YES`, no longer re-use this operation when new request comes in
522+
SDWebImageDownloaderOperationSetCompleted(dataOperation, true);
523+
}
502524
if ([dataOperation respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
503525
[dataOperation URLSession:session task:task didCompleteWithError:error];
504526
}

SDWebImage/Core/SDWebImageDownloaderOperation.m

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#import "SDImageCacheDefine.h"
1515
#import "SDCallbackQueue.h"
1616

17+
BOOL SDWebImageDownloaderOperationGetCompleted(id<SDWebImageDownloaderOperation> operation); // Private currently, mark open if needed
18+
1719
// A handler to represent individual request
1820
@interface SDWebImageDownloaderOperationToken : NSObject
1921

@@ -110,8 +112,9 @@ - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
110112
_finished = NO;
111113
_expectedSize = 0;
112114
_unownedSession = session;
113-
_coderQueue = [NSOperationQueue new];
115+
_coderQueue = [[NSOperationQueue alloc] init];
114116
_coderQueue.maxConcurrentOperationCount = 1;
117+
_coderQueue.name = @"com.hackemist.SDWebImageDownloaderOperation.coderQueue";
115118
_imageMap = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:1];
116119
#if SD_UIKIT
117120
_backgroundTaskId = UIBackgroundTaskInvalid;
@@ -231,13 +234,10 @@ - (void)start {
231234
if (self.dataTask) {
232235
if (self.options & SDWebImageDownloaderHighPriority) {
233236
self.dataTask.priority = NSURLSessionTaskPriorityHigh;
234-
self.coderQueue.qualityOfService = NSQualityOfServiceUserInteractive;
235237
} else if (self.options & SDWebImageDownloaderLowPriority) {
236238
self.dataTask.priority = NSURLSessionTaskPriorityLow;
237-
self.coderQueue.qualityOfService = NSQualityOfServiceBackground;
238239
} else {
239240
self.dataTask.priority = NSURLSessionTaskPriorityDefault;
240-
self.coderQueue.qualityOfService = NSQualityOfServiceDefault;
241241
}
242242
[self.dataTask resume];
243243
NSArray<SDWebImageDownloaderOperationToken *> *tokens;
@@ -471,22 +471,22 @@ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)data
471471
if (self.coderQueue.operationCount == 0) {
472472
// NSOperation have autoreleasepool, don't need to create extra one
473473
@weakify(self);
474-
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
474+
[self.coderQueue addOperationWithBlock:^{
475475
@strongify(self);
476476
if (!self) {
477477
return;
478478
}
479-
UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, NO, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
480-
if (self.isFinished) {
479+
// When cancelled or transfer finished (`didCompleteWithError`), cancel the progress callback, only completed block is called and enough
480+
if (self.isCancelled || SDWebImageDownloaderOperationGetCompleted(self)) {
481481
return;
482482
}
483+
UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, NO, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
483484
if (image) {
484485
// We do not keep the progressive decoding image even when `finished`=YES. Because they are for view rendering but not take full function from downloader options. And some coders implementation may not keep consistent between progressive decoding and normal decoding.
485486

486487
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
487488
}
488489
}];
489-
[self.coderQueue addOperation:operation];
490490
}
491491
}
492492

@@ -564,16 +564,8 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didComp
564564
// decode the image in coder queue, cancel all previous decoding process
565565
[self.coderQueue cancelAllOperations];
566566
@weakify(self);
567-
// call done after all different variant completed block was dispatched
568-
NSOperation *doneOperation = [NSBlockOperation blockOperationWithBlock:^{
569-
@strongify(self);
570-
if (!self) {
571-
return;
572-
}
573-
[self done];
574-
}];
575567
for (SDWebImageDownloaderOperationToken *token in tokens) {
576-
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
568+
[self.coderQueue addOperationWithBlock:^{
577569
@strongify(self);
578570
if (!self) {
579571
return;
@@ -612,11 +604,22 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didComp
612604
[self callCompletionBlockWithToken:token image:image imageData:imageData error:nil finished:YES];
613605
}
614606
}];
615-
[doneOperation addDependency:operation];
616-
[self.coderQueue addOperation:operation];
617607
}
618608
// call [self done] after all completed block was dispatched
619-
[self.coderQueue addOperation:doneOperation];
609+
dispatch_block_t doneBlock = ^{
610+
@strongify(self);
611+
if (!self) {
612+
return;
613+
}
614+
[self done];
615+
};
616+
if (@available(iOS 13, tvOS 13, macOS 10.15, watchOS 6, *)) {
617+
// seems faster than `addOperationWithBlock`
618+
[self.coderQueue addBarrierBlock:doneBlock];
619+
} else {
620+
// serial queue, this does the same effect in semantics
621+
[self.coderQueue addOperationWithBlock:doneBlock];
622+
}
620623
}
621624
} else {
622625
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];

Tests/Tests/SDTestCase.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111

1212
const int64_t kAsyncTestTimeout = 5;
1313
const int64_t kMinDelayNanosecond = NSEC_PER_MSEC * 100; // 0.1s
14-
NSString *const kTestJPEGURL = @"http://via.placeholder.com/50x50.jpg";
14+
NSString *const kTestJPEGURL = @"https://via.placeholder.com/50x50.jpg";
1515
NSString *const kTestProgressiveJPEGURL = @"https://raw.githubusercontent.com/ibireme/YYImage/master/Demo/YYImageDemo/mew_progressive.jpg";
16-
NSString *const kTestPNGURL = @"http://via.placeholder.com/50x50.png";
16+
NSString *const kTestPNGURL = @"https://via.placeholder.com/50x50.png";
1717
NSString *const kTestGIFURL = @"https://media.giphy.com/media/UEsrLdv7ugRTq/giphy.gif";
1818
NSString *const kTestAPNGPURL = @"https://upload.wikimedia.org/wikipedia/commons/1/14/Animated_PNG_example_bouncing_beach_ball.png";
1919

Tests/Tests/SDWebImageDownloaderTests.m

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ - (void)test10That404CaseCallsCompletionWithError {
174174
- (void)test11ThatCancelWorks {
175175
XCTestExpectation *expectation = [self expectationWithDescription:@"Cancel"];
176176

177-
NSURL *imageURL = [NSURL URLWithString:@"http://via.placeholder.com/1000x1000.png"];
177+
NSURL *imageURL = [NSURL URLWithString:@"https://via.placeholder.com/1000x1000.png"];
178178
SDWebImageDownloadToken *token = [[SDWebImageDownloader sharedDownloader]
179179
downloadImageWithURL:imageURL options:0 progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
180180
expect(error).notTo.beNil();
@@ -680,7 +680,7 @@ - (void)test26DownloadURLSessionMetrics {
680680
expect(metric.fetchStartDate).notTo.beNil();
681681
expect(metric.connectStartDate).notTo.beNil();
682682
expect(metric.connectEndDate).notTo.beNil();
683-
expect(metric.networkProtocolName).equal(@"http/1.1");
683+
expect(metric.networkProtocolName).equal(@"h2");
684684
expect(metric.resourceFetchType).equal(NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad);
685685
expect(metric.isProxyConnection).beFalsy();
686686
expect(metric.isReusedConnection).beFalsy();
@@ -779,7 +779,7 @@ - (void)test30ThatDifferentThumbnailLoadShouldCallbackDifferentSize {
779779
// We move the logic into SDWebImageDownloaderOperation, which decode each callback's thumbnail size with different decoding pipeline, and callback independently
780780
// Note the progressiveLoad does not support this and always callback first size
781781

782-
NSURL *url = [NSURL URLWithString:@"http://via.placeholder.com/501x501.png"];
782+
NSURL *url = [NSURL URLWithString:@"https://via.placeholder.com/501x501.png"];
783783
NSString *fullSizeKey = [SDWebImageManager.sharedManager cacheKeyForURL:url];
784784
[SDImageCache.sharedImageCache removeImageFromDiskForKey:fullSizeKey];
785785
for (int i = 490; i < 500; i++) {
@@ -799,8 +799,39 @@ - (void)test30ThatDifferentThumbnailLoadShouldCallbackDifferentSize {
799799
[self waitForExpectationsWithTimeout:kAsyncTestTimeout * 5 handler:nil];
800800
}
801801

802+
- (void)test31ThatMultipleRequestForSameURLNeverSkipCallback {
803+
// See #3475
804+
// When multiple download request for same URL, the SDWebImageDownloader will try to `Re-use` URLSessionTask to avoid duplicate actual network request
805+
// However, if caller submit too frequently in another queue, we should stop attaching more callback once the URLSessionTask `didCompleteWithError:` is called
806+
NSURL *url = [NSURL fileURLWithPath:[self testPNGPath]];
807+
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray arrayWithCapacity:100];
808+
__block void (^recursiveBlock)(int);
809+
void (^mainBlock)(int) = ^(int i) {
810+
if (i > 200) return;
811+
NSString *desc = [NSString stringWithFormat:@"Local url with index %d not callback!", i];
812+
XCTestExpectation *expectation = [self expectationWithDescription:desc];
813+
[expectations addObject:expectation];
814+
// Delay 0.01s ~ 0.99s for each download request, simulate the real-world call site
815+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(i * 10000000ull)), dispatch_get_main_queue(), ^{
816+
[SDWebImageDownloader.sharedDownloader downloadImageWithURL:url completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
817+
if (image) {
818+
NSLog(@"Local url callback with index: %d", i);
819+
[expectation fulfill];
820+
} else {
821+
XCTFail(@"Something went wrong: %@", error.description);
822+
}
823+
}];
824+
});
825+
recursiveBlock(i+1);
826+
};
827+
recursiveBlock = mainBlock;
828+
recursiveBlock(0);
829+
830+
[self waitForExpectations:expectations timeout:kAsyncTestTimeout * 2];
831+
}
832+
802833
#pragma mark - SDWebImageLoader
803-
- (void)test30CustomImageLoaderWorks {
834+
- (void)testCustomImageLoaderWorks {
804835
XCTestExpectation *expectation = [self expectationWithDescription:@"Custom image not works"];
805836
SDWebImageTestLoader *loader = [[SDWebImageTestLoader alloc] init];
806837
NSURL *imageURL = [NSURL URLWithString:kTestJPEGURL];
@@ -820,7 +851,7 @@ - (void)test30CustomImageLoaderWorks {
820851
[self waitForExpectationsWithCommonTimeout];
821852
}
822853

823-
- (void)test31ThatLoadersManagerWorks {
854+
- (void)testThatLoadersManagerWorks {
824855
XCTestExpectation *expectation = [self expectationWithDescription:@"Loaders manager not works"];
825856
SDWebImageTestLoader *loader = [[SDWebImageTestLoader alloc] init];
826857
SDImageLoadersManager *manager = [[SDImageLoadersManager alloc] init];

0 commit comments

Comments
 (0)