Authored by Olivier Poitrey

Use dispatch_barrier to handle NSMutableDictionary thread unsafety instead of ma…

…in thread dispatching
@@ -20,6 +20,7 @@ NSString *const kCompletedCallbackKey = @"completed"; @@ -20,6 +20,7 @@ NSString *const kCompletedCallbackKey = @"completed";
20 20
21 @property (strong, nonatomic) NSOperationQueue *downloadQueue; 21 @property (strong, nonatomic) NSOperationQueue *downloadQueue;
22 @property (strong, nonatomic) NSMutableDictionary *URLCallbacks; 22 @property (strong, nonatomic) NSMutableDictionary *URLCallbacks;
  23 +@property (assign, nonatomic) dispatch_queue_t barrierQueue;
23 24
24 @end 25 @end
25 26
@@ -65,6 +66,7 @@ NSString *const kCompletedCallbackKey = @"completed"; @@ -65,6 +66,7 @@ NSString *const kCompletedCallbackKey = @"completed";
65 _downloadQueue = NSOperationQueue.new; 66 _downloadQueue = NSOperationQueue.new;
66 _downloadQueue.maxConcurrentOperationCount = 10; 67 _downloadQueue.maxConcurrentOperationCount = 10;
67 _URLCallbacks = NSMutableDictionary.new; 68 _URLCallbacks = NSMutableDictionary.new;
  69 + _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
68 } 70 }
69 return self; 71 return self;
70 } 72 }
@@ -82,71 +84,85 @@ NSString *const kCompletedCallbackKey = @"completed"; @@ -82,71 +84,85 @@ NSString *const kCompletedCallbackKey = @"completed";
82 - (NSOperation *)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(void (^)(NSUInteger, long long))progressBlock completed:(void (^)(UIImage *, NSError *, BOOL))completedBlock 84 - (NSOperation *)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(void (^)(NSUInteger, long long))progressBlock completed:(void (^)(UIImage *, NSError *, BOOL))completedBlock
83 { 85 {
84 __block SDWebImageDownloaderOperation *operation; 86 __block SDWebImageDownloaderOperation *operation;
  87 + __weak SDWebImageDownloader *wself = self;
85 88
86 - dispatch_async(dispatch_get_main_queue(), ^ // NSDictionary isn't thread safe 89 + [self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^
87 { 90 {
88 - BOOL performDownload = NO; 91 + // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests
  92 + NSMutableURLRequest *request = [NSMutableURLRequest.alloc initWithURL:url cachePolicy:NSURLCacheStorageNotAllowed timeoutInterval:15];
  93 + request.HTTPShouldHandleCookies = NO;
  94 + request.HTTPShouldUsePipelining = YES;
  95 + [request addValue:@"image/*" forHTTPHeaderField:@"Accept"];
  96 + operation = [SDWebImageDownloaderOperation.alloc initWithRequest:request options:options progress:^(NSUInteger receivedSize, long long expectedSize)
  97 + {
  98 + if (!wself) return;
  99 + SDWebImageDownloader *sself = wself;
  100 + NSArray *callbacksForURL = [sself removeAndReturnCallbacksForURL:url];
  101 + for (NSDictionary *callbacks in callbacksForURL)
  102 + {
  103 + SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
  104 + if (callback) callback(receivedSize, expectedSize);
  105 + }
  106 + }
  107 + completed:^(UIImage *image, NSError *error, BOOL finished)
  108 + {
  109 + if (!wself) return;
  110 + SDWebImageDownloader *sself = wself;
  111 + NSArray *callbacksForURL = [sself removeAndReturnCallbacksForURL:url];
  112 + for (NSDictionary *callbacks in callbacksForURL)
  113 + {
  114 + SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
  115 + if (callback) callback(image, error, finished);
  116 + }
  117 + }
  118 + cancelled:^
  119 + {
  120 + if (!wself) return;
  121 + SDWebImageDownloader *sself = wself;
  122 + [sself removeAndReturnCallbacksForURL:url];
  123 + }];
  124 + [self.downloadQueue addOperation:operation];
  125 + }];
89 126
  127 +
  128 + return operation;
  129 +}
  130 +
  131 +- (void)addProgressCallback:(void (^)(NSUInteger, long long))progressBlock andCompletedBlock:(void (^)(UIImage *, NSError *, BOOL))completedBlock forURL:(NSURL *)url createCallback:(void (^)())createCallback
  132 +{
  133 + dispatch_barrier_async(self.barrierQueue, ^
  134 + {
  135 + BOOL first = NO;
90 if (!self.URLCallbacks[url]) 136 if (!self.URLCallbacks[url])
91 { 137 {
92 self.URLCallbacks[url] = NSMutableArray.new; 138 self.URLCallbacks[url] = NSMutableArray.new;
93 - performDownload = YES; 139 + first = YES;
94 } 140 }
95 141
96 // Handle single download of simultaneous download request for the same URL 142 // Handle single download of simultaneous download request for the same URL
  143 + NSMutableArray *callbacksForURL = self.URLCallbacks[url];
  144 + NSMutableDictionary *callbacks = NSMutableDictionary.new;
  145 + if (progressBlock) callbacks[kProgressCallbackKey] = progressBlock;
  146 + if (completedBlock) callbacks[kCompletedCallbackKey] = completedBlock;
  147 + [callbacksForURL addObject:callbacks];
  148 + self.URLCallbacks[url] = callbacksForURL;
  149 +
  150 + if (first)
97 { 151 {
98 - NSMutableArray *callbacksForURL = self.URLCallbacks[url];  
99 - NSMutableDictionary *callbacks = NSMutableDictionary.new;  
100 - if (progressBlock) callbacks[kProgressCallbackKey] = progressBlock;  
101 - if (completedBlock) callbacks[kCompletedCallbackKey] = completedBlock;  
102 - [callbacksForURL addObject:callbacks];  
103 - self.URLCallbacks[url] = callbacksForURL;  
104 - }  
105 -  
106 - if (performDownload)  
107 - {  
108 - // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests  
109 - NSMutableURLRequest *request = [NSMutableURLRequest.alloc initWithURL:url cachePolicy:NSURLCacheStorageNotAllowed timeoutInterval:15];  
110 - request.HTTPShouldHandleCookies = NO;  
111 - request.HTTPShouldUsePipelining = YES;  
112 - [request addValue:@"image/*" forHTTPHeaderField:@"Accept"];  
113 - operation = [SDWebImageDownloaderOperation.alloc initWithRequest:request options:options progress:^(NSUInteger receivedSize, long long expectedSize)  
114 - {  
115 - dispatch_async(dispatch_get_main_queue(), ^  
116 - {  
117 - NSMutableArray *callbacksForURL = self.URLCallbacks[url];  
118 - for (NSDictionary *callbacks in callbacksForURL)  
119 - {  
120 - SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];  
121 - if (callback) callback(receivedSize, expectedSize);  
122 - }  
123 - });  
124 - }  
125 - completed:^(UIImage *image, NSError *error, BOOL finished)  
126 - {  
127 - dispatch_async(dispatch_get_main_queue(), ^  
128 - {  
129 - NSMutableArray *callbacksForURL = self.URLCallbacks[url];  
130 - [self.URLCallbacks removeObjectForKey:url];  
131 - for (NSDictionary *callbacks in callbacksForURL)  
132 - {  
133 - SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];  
134 - if (callback) callback(image, error, finished);  
135 - }  
136 - });  
137 - }  
138 - cancelled:^  
139 - {  
140 - dispatch_async(dispatch_get_main_queue(), ^  
141 - {  
142 - [self.URLCallbacks removeObjectForKey:url];  
143 - });  
144 - }];  
145 - [self.downloadQueue addOperation:operation]; 152 + createCallback();
146 } 153 }
147 }); 154 });
  155 +}
148 156
149 - return operation; 157 +- (NSArray *)removeAndReturnCallbacksForURL:(NSURL *)url
  158 +{
  159 + __block NSArray *callbacksForURL;
  160 + dispatch_barrier_sync(self.barrierQueue, ^
  161 + {
  162 + callbacksForURL = self.URLCallbacks[url];
  163 + [self.URLCallbacks removeObjectForKey:url];
  164 + });
  165 + return callbacksForURL;
150 } 166 }
151 167
152 @end 168 @end