Skip to content

Commit 43b9413

Browse files
committed
Ensure the DownloaderOperation callback the completion in atomic and never miss one
The downloader will now check and ignore a `transferedDataFinished` operation (which is callbacking its own completion), because it's not safe in multi-thread environment, create new network request instead
1 parent 6c6b951 commit 43b9413

File tree

3 files changed

+40
-42
lines changed

3 files changed

+40
-42
lines changed

SDWebImage/Core/SDWebImageDownloader.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
219219
SDImageCoderOptions *decodeOptions = SDGetDecodeOptionsFromContext(context, [self.class imageOptionsFromDownloaderOptions:options], cacheKey);
220220
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
221221
// 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) {
222+
if (!operation || operation.isFinished || operation.isCancelled || operation.isTransferFinished) {
223223
operation = [self createDownloaderOperationWithUrl:url options:options context:context];
224224
if (!operation) {
225225
SD_UNLOCK(_operationsLock);

SDWebImage/Core/SDWebImageDownloaderOperation.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535

3636
- (BOOL)cancel:(nullable id)token;
3737

38+
@property (assign, readonly) BOOL isTransferFinished;
39+
3840
@property (strong, nonatomic, readonly, nullable) NSURLRequest *request;
3941
@property (strong, nonatomic, readonly, nullable) NSURLResponse *response;
4042

@@ -56,6 +58,10 @@
5658
*/
5759
@interface SDWebImageDownloaderOperation : NSOperation <SDWebImageDownloaderOperation>
5860

61+
/// Whether the operation's network data transfer is finished. This is used by downloader to decide whether to call `addHandlersForProgress:`, or create a new operation instead.
62+
/// @note You must implements this or this will cause downloader attach new handlers for a already finished operation, may cause some callback missing.
63+
@property (assign, readonly) BOOL isTransferFinished;
64+
5965
/**
6066
* The request used by the operation's task.
6167
*/

SDWebImage/Core/SDWebImageDownloaderOperation.m

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ @interface SDWebImageDownloaderOperation ()
7373

7474
@property (strong, nonatomic, readwrite, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
7575

76-
@property (strong, nonatomic, nonnull) NSOperationQueue *coderQueue; // the serial operation queue to do image decoding
76+
@property (strong, nonatomic, nonnull) dispatch_queue_t coderQueue; // the serial operation queue to do image decoding
77+
@property (assign, readwrite) BOOL isTransferFinished; // Whether current operation's network transfer is finished (actually, `didCompleteWithError` already been called)
7778

7879
@property (strong, nonatomic, nonnull) NSMapTable<SDImageCoderOptions *, UIImage *> *imageMap; // each variant of image is weak-referenced to avoid too many re-decode during downloading
7980
#if SD_UIKIT
@@ -110,8 +111,7 @@ - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
110111
_finished = NO;
111112
_expectedSize = 0;
112113
_unownedSession = session;
113-
_coderQueue = [NSOperationQueue new];
114-
_coderQueue.maxConcurrentOperationCount = 1;
114+
_coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperation", DISPATCH_QUEUE_SERIAL);
115115
_imageMap = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:1];
116116
#if SD_UIKIT
117117
_backgroundTaskId = UIBackgroundTaskInvalid;
@@ -231,13 +231,10 @@ - (void)start {
231231
if (self.dataTask) {
232232
if (self.options & SDWebImageDownloaderHighPriority) {
233233
self.dataTask.priority = NSURLSessionTaskPriorityHigh;
234-
self.coderQueue.qualityOfService = NSQualityOfServiceUserInteractive;
235234
} else if (self.options & SDWebImageDownloaderLowPriority) {
236235
self.dataTask.priority = NSURLSessionTaskPriorityLow;
237-
self.coderQueue.qualityOfService = NSQualityOfServiceBackground;
238236
} else {
239237
self.dataTask.priority = NSURLSessionTaskPriorityDefault;
240-
self.coderQueue.qualityOfService = NSQualityOfServiceDefault;
241238
}
242239
[self.dataTask resume];
243240
NSArray<SDWebImageDownloaderOperationToken *> *tokens;
@@ -468,26 +465,23 @@ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)data
468465
NSData *imageData = self.imageData;
469466

470467
// keep maximum one progressive decode process during download
471-
if (self.coderQueue.operationCount == 0) {
472-
// NSOperation have autoreleasepool, don't need to create extra one
473-
@weakify(self);
474-
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
475-
@strongify(self);
476-
if (!self) {
477-
return;
478-
}
479-
UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, NO, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
480-
if (self.isFinished) {
481-
return;
482-
}
483-
if (image) {
484-
// 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.
485-
486-
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
487-
}
488-
}];
489-
[self.coderQueue addOperation:operation];
490-
}
468+
@weakify(self);
469+
dispatch_async(self.coderQueue, ^{
470+
@strongify(self);
471+
if (!self) {
472+
return;
473+
}
474+
// When cancelled or transfer finished (`didCompleteWithError`), cancel the progress callback, only completed block is called and enough
475+
if (self.isCancelled || self.isTransferFinished) {
476+
return;
477+
}
478+
UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, NO, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
479+
if (image) {
480+
// 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.
481+
482+
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
483+
}
484+
});
491485
}
492486

493487
for (SDWebImageDownloaderOperationToken *token in tokens) {
@@ -518,6 +512,7 @@ - (void)URLSession:(NSURLSession *)session
518512
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
519513
// If we already cancel the operation or anything mark the operation finished, don't callback twice
520514
if (self.isFinished) return;
515+
self.isTransferFinished = YES;
521516

522517
NSArray<SDWebImageDownloaderOperationToken *> *tokens;
523518
@synchronized (self) {
@@ -561,19 +556,9 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didComp
561556
[self callCompletionBlocksWithError:self.responseError];
562557
[self done];
563558
} else {
564-
// decode the image in coder queue, cancel all previous decoding process
565-
[self.coderQueue cancelAllOperations];
566559
@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-
}];
575560
for (SDWebImageDownloaderOperationToken *token in tokens) {
576-
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
561+
dispatch_async(self.coderQueue, ^{
577562
@strongify(self);
578563
if (!self) {
579564
return;
@@ -611,12 +596,19 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didComp
611596
} else {
612597
[self callCompletionBlockWithToken:token image:image imageData:imageData error:nil finished:YES];
613598
}
614-
}];
615-
[doneOperation addDependency:operation];
616-
[self.coderQueue addOperation:operation];
599+
});
617600
}
618601
// call [self done] after all completed block was dispatched
619-
[self.coderQueue addOperation:doneOperation];
602+
dispatch_barrier_async(self.coderQueue, ^{
603+
@strongify(self);
604+
if (!self) {
605+
return;
606+
}
607+
[self done];
608+
});
609+
dispatch_async(self.coderQueue, ^{
610+
[self done];
611+
});
620612
}
621613
} else {
622614
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];

0 commit comments

Comments
 (0)