Authored by DreamPiggy
Committed by GitHub

Merge pull request #2177 from dreampiggy/fix_running_operation_leak

Use the weak-strong dancing and the weak reference to manager instance to avoid the leak of runningOperations
@@ -82,9 +82,16 @@ typedef SDHTTPHeadersDictionary * _Nullable (^SDWebImageDownloaderHeadersFilterB @@ -82,9 +82,16 @@ typedef SDHTTPHeadersDictionary * _Nullable (^SDWebImageDownloaderHeadersFilterB
82 /** 82 /**
83 * A token associated with each download. Can be used to cancel a download 83 * A token associated with each download. Can be used to cancel a download
84 */ 84 */
85 -@interface SDWebImageDownloadToken : NSObject 85 +@interface SDWebImageDownloadToken : NSObject <SDWebImageOperation>
86 86
  87 +/**
  88 + The download's URL. This should be readonly and you should not modify
  89 + */
87 @property (nonatomic, strong, nullable) NSURL *url; 90 @property (nonatomic, strong, nullable) NSURL *url;
  91 +/**
  92 + The cancel token taken from `addHandlersForProgress:completed`. This should be readonly and you should not modify
  93 + @note use `-[SDWebImageDownloadToken cancel]` to cancel the token
  94 + */
88 @property (nonatomic, strong, nullable) id downloadOperationCancelToken; 95 @property (nonatomic, strong, nullable) id downloadOperationCancelToken;
89 96
90 @end 97 @end
@@ -9,7 +9,23 @@ @@ -9,7 +9,23 @@
9 #import "SDWebImageDownloader.h" 9 #import "SDWebImageDownloader.h"
10 #import "SDWebImageDownloaderOperation.h" 10 #import "SDWebImageDownloaderOperation.h"
11 11
  12 +@interface SDWebImageDownloadToken ()
  13 +
  14 +@property (nonatomic, weak, nullable) NSOperation<SDWebImageDownloaderOperationInterface> *downloadOperation;
  15 +
  16 +@end
  17 +
12 @implementation SDWebImageDownloadToken 18 @implementation SDWebImageDownloadToken
  19 +
  20 +- (void)cancel {
  21 + if (self.downloadOperation) {
  22 + SDWebImageDownloadToken *cancelToken = self.downloadOperationCancelToken;
  23 + if (cancelToken) {
  24 + [self.downloadOperation cancel:cancelToken];
  25 + }
  26 + }
  27 +}
  28 +
13 @end 29 @end
14 30
15 31
@@ -258,6 +274,7 @@ @@ -258,6 +274,7 @@
258 id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock]; 274 id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
259 275
260 token = [SDWebImageDownloadToken new]; 276 token = [SDWebImageDownloadToken new];
  277 + token.downloadOperation = operation;
261 token.url = url; 278 token.url = url;
262 token.downloadOperationCancelToken = downloadOperationCancelToken; 279 token.downloadOperationCancelToken = downloadOperationCancelToken;
263 }); 280 });
@@ -19,6 +19,7 @@ FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadFinishNotification @@ -19,6 +19,7 @@ FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadFinishNotification
19 19
20 /** 20 /**
21 Describes a downloader operation. If one wants to use a custom downloader op, it needs to inherit from `NSOperation` and conform to this protocol 21 Describes a downloader operation. If one wants to use a custom downloader op, it needs to inherit from `NSOperation` and conform to this protocol
  22 + For the description about these methods, see `SDWebImageDownloaderOperation`
22 */ 23 */
23 @protocol SDWebImageDownloaderOperationInterface<NSObject> 24 @protocol SDWebImageDownloaderOperationInterface<NSObject>
24 25
@@ -35,6 +36,8 @@ FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadFinishNotification @@ -35,6 +36,8 @@ FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadFinishNotification
35 - (nullable NSURLCredential *)credential; 36 - (nullable NSURLCredential *)credential;
36 - (void)setCredential:(nullable NSURLCredential *)value; 37 - (void)setCredential:(nullable NSURLCredential *)value;
37 38
  39 +- (BOOL)cancel:(nullable id)token;
  40 +
38 @end 41 @end
39 42
40 43
@@ -13,8 +13,9 @@ @@ -13,8 +13,9 @@
13 @interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation> 13 @interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
14 14
15 @property (assign, nonatomic, getter = isCancelled) BOOL cancelled; 15 @property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
16 -@property (copy, nonatomic, nullable) SDWebImageNoParamsBlock cancelBlock; 16 +@property (strong, nonatomic, nullable) SDWebImageDownloadToken *downloadToken;
17 @property (strong, nonatomic, nullable) NSOperation *cacheOperation; 17 @property (strong, nonatomic, nullable) NSOperation *cacheOperation;
  18 +@property (weak, nonatomic, nullable) SDWebImageManager *manager;
18 19
19 @end 20 @end
20 21
@@ -124,8 +125,8 @@ @@ -124,8 +125,8 @@
124 url = nil; 125 url = nil;
125 } 126 }
126 127
127 - __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];  
128 - __weak SDWebImageCombinedOperation *weakOperation = operation; 128 + SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
  129 + operation.manager = self;
129 130
130 BOOL isFailedUrl = NO; 131 BOOL isFailedUrl = NO;
131 if (url) { 132 if (url) {
@@ -148,10 +149,12 @@ @@ -148,10 +149,12 @@
148 if (options & SDWebImageCacheMemoryOnly) cacheOptions |= SDImageCacheQueryMemoryOnly; 149 if (options & SDWebImageCacheMemoryOnly) cacheOptions |= SDImageCacheQueryMemoryOnly;
149 if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory; 150 if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
150 if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync; 151 if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
151 - 152 +
  153 + __weak SDWebImageCombinedOperation *weakOperation = operation;
152 operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) { 154 operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
153 - if (operation.isCancelled) {  
154 - [self safelyRemoveOperationFromRunning:operation]; 155 + __strong __typeof(weakOperation) strongOperation = weakOperation;
  156 + if (!strongOperation || strongOperation.isCancelled) {
  157 + [self safelyRemoveOperationFromRunning:strongOperation];
155 return; 158 return;
156 } 159 }
157 160
@@ -159,7 +162,7 @@ @@ -159,7 +162,7 @@
159 if (cachedImage && options & SDWebImageRefreshCached) { 162 if (cachedImage && options & SDWebImageRefreshCached) {
160 // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image 163 // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
161 // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server. 164 // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
162 - [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url]; 165 + [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
163 } 166 }
164 167
165 // download if no image or requested to refresh anyway, and download allowed by delegate 168 // download if no image or requested to refresh anyway, and download allowed by delegate
@@ -180,14 +183,16 @@ @@ -180,14 +183,16 @@
180 downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse; 183 downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
181 } 184 }
182 185
183 - SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {  
184 - __strong __typeof(weakOperation) strongOperation = weakOperation;  
185 - if (!strongOperation || strongOperation.isCancelled) { 186 + // `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block bellow, so we need weak-strong again to avoid retain cycle
  187 + __weak typeof(strongOperation) weakSubOperation = strongOperation;
  188 + strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
  189 + __strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
  190 + if (!strongSubOperation || strongSubOperation.isCancelled) {
186 // Do nothing if the operation was cancelled 191 // Do nothing if the operation was cancelled
187 // See #699 for more details 192 // See #699 for more details
188 // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data 193 // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
189 } else if (error) { 194 } else if (error) {
190 - [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url]; 195 + [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
191 196
192 if ( error.code != NSURLErrorNotConnectedToInternet 197 if ( error.code != NSURLErrorNotConnectedToInternet
193 && error.code != NSURLErrorCancelled 198 && error.code != NSURLErrorCancelled
@@ -228,37 +233,27 @@ @@ -228,37 +233,27 @@
228 [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil]; 233 [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
229 } 234 }
230 235
231 - [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; 236 + [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
232 }); 237 });
233 } else { 238 } else {
234 if (downloadedImage && finished) { 239 if (downloadedImage && finished) {
235 [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil]; 240 [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
236 } 241 }
237 - [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; 242 + [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
238 } 243 }
239 } 244 }
240 245
241 if (finished) { 246 if (finished) {
242 - [self safelyRemoveOperationFromRunning:strongOperation]; 247 + [self safelyRemoveOperationFromRunning:strongSubOperation];
243 } 248 }
244 }]; 249 }];
245 - @synchronized(operation) {  
246 - // Need same lock to ensure cancelBlock called because cancel method can be called in different queue  
247 - operation.cancelBlock = ^{  
248 - [self.imageDownloader cancel:subOperationToken];  
249 - __strong __typeof(weakOperation) strongOperation = weakOperation;  
250 - [self safelyRemoveOperationFromRunning:strongOperation];  
251 - };  
252 - }  
253 } else if (cachedImage) { 250 } else if (cachedImage) {
254 - __strong __typeof(weakOperation) strongOperation = weakOperation;  
255 [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url]; 251 [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
256 - [self safelyRemoveOperationFromRunning:operation]; 252 + [self safelyRemoveOperationFromRunning:strongOperation];
257 } else { 253 } else {
258 // Image not in cache and download disallowed by delegate 254 // Image not in cache and download disallowed by delegate
259 - __strong __typeof(weakOperation) strongOperation = weakOperation;  
260 [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url]; 255 [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
261 - [self safelyRemoveOperationFromRunning:operation]; 256 + [self safelyRemoveOperationFromRunning:strongOperation];
262 } 257 }
263 }]; 258 }];
264 259
@@ -323,18 +318,6 @@ @@ -323,18 +318,6 @@
323 318
324 @implementation SDWebImageCombinedOperation 319 @implementation SDWebImageCombinedOperation
325 320
326 -- (void)setCancelBlock:(nullable SDWebImageNoParamsBlock)cancelBlock {  
327 - // check if the operation is already cancelled, then we just call the cancelBlock  
328 - if (self.isCancelled) {  
329 - if (cancelBlock) {  
330 - cancelBlock();  
331 - }  
332 - _cancelBlock = nil; // don't forget to nil the cancelBlock, otherwise we will get crashes  
333 - } else {  
334 - _cancelBlock = [cancelBlock copy];  
335 - }  
336 -}  
337 -  
338 - (void)cancel { 321 - (void)cancel {
339 @synchronized(self) { 322 @synchronized(self) {
340 self.cancelled = YES; 323 self.cancelled = YES;
@@ -342,10 +325,10 @@ @@ -342,10 +325,10 @@
342 [self.cacheOperation cancel]; 325 [self.cacheOperation cancel];
343 self.cacheOperation = nil; 326 self.cacheOperation = nil;
344 } 327 }
345 - if (self.cancelBlock) {  
346 - self.cancelBlock();  
347 - self.cancelBlock = nil; 328 + if (self.downloadToken) {
  329 + [self.manager.imageDownloader cancel:self.downloadToken];
348 } 330 }
  331 + [self.manager safelyRemoveOperationFromRunning:self];
349 } 332 }
350 } 333 }
351 334