SDWebImageManager.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. /*
  2. * This file is part of the SDWebImage package.
  3. * (c) Olivier Poitrey <rs@dailymotion.com>
  4. *
  5. * For the full copyright and license information, please view the LICENSE
  6. * file that was distributed with this source code.
  7. */
  8. #import "SDWebImageManager.h"
  9. #import "NSImage+WebCache.h"
  10. #import <objc/message.h>
  11. #define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
  12. #define UNLOCK(lock) dispatch_semaphore_signal(lock);
  13. @interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
  14. @property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
  15. @property (strong, nonatomic, nullable) SDWebImageDownloadToken *downloadToken;
  16. @property (strong, nonatomic, nullable) NSOperation *cacheOperation;
  17. @property (weak, nonatomic, nullable) SDWebImageManager *manager;
  18. @end
  19. @interface SDWebImageManager ()
  20. @property (strong, nonatomic, readwrite, nonnull) SDImageCache *imageCache;
  21. @property (strong, nonatomic, readwrite, nonnull) SDWebImageDownloader *imageDownloader;
  22. @property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs;
  23. @property (strong, nonatomic, nonnull) dispatch_semaphore_t failedURLsLock; // a lock to keep the access to `failedURLs` thread-safe
  24. @property (strong, nonatomic, nonnull) NSMutableSet<SDWebImageCombinedOperation *> *runningOperations;
  25. @property (strong, nonatomic, nonnull) dispatch_semaphore_t runningOperationsLock; // a lock to keep the access to `runningOperations` thread-safe
  26. @end
  27. @implementation SDWebImageManager
  28. + (nonnull instancetype)sharedManager {
  29. static dispatch_once_t once;
  30. static id instance;
  31. dispatch_once(&once, ^{
  32. instance = [self new];
  33. });
  34. return instance;
  35. }
  36. - (nonnull instancetype)init {
  37. SDImageCache *cache = [SDImageCache sharedImageCache];
  38. SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
  39. return [self initWithCache:cache downloader:downloader];
  40. }
  41. - (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
  42. if ((self = [super init])) {
  43. _imageCache = cache;
  44. _imageDownloader = downloader;
  45. _failedURLs = [NSMutableSet new];
  46. _failedURLsLock = dispatch_semaphore_create(1);
  47. _runningOperations = [NSMutableSet new];
  48. _runningOperationsLock = dispatch_semaphore_create(1);
  49. }
  50. return self;
  51. }
  52. - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
  53. if (!url) {
  54. return @"";
  55. }
  56. if (self.cacheKeyFilter) {
  57. return self.cacheKeyFilter(url);
  58. } else {
  59. return url.absoluteString;
  60. }
  61. }
  62. - (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image {
  63. return SDScaledImageForKey(key, image);
  64. }
  65. - (void)cachedImageExistsForURL:(nullable NSURL *)url
  66. completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {
  67. NSString *key = [self cacheKeyForURL:url];
  68. BOOL isInMemoryCache = ([self.imageCache imageFromMemoryCacheForKey:key] != nil);
  69. if (isInMemoryCache) {
  70. // making sure we call the completion block on the main queue
  71. dispatch_async(dispatch_get_main_queue(), ^{
  72. if (completionBlock) {
  73. completionBlock(YES);
  74. }
  75. });
  76. return;
  77. }
  78. [self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {
  79. // the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch
  80. if (completionBlock) {
  81. completionBlock(isInDiskCache);
  82. }
  83. }];
  84. }
  85. - (void)diskImageExistsForURL:(nullable NSURL *)url
  86. completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {
  87. NSString *key = [self cacheKeyForURL:url];
  88. [self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {
  89. // the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch
  90. if (completionBlock) {
  91. completionBlock(isInDiskCache);
  92. }
  93. }];
  94. }
  95. - (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
  96. options:(SDWebImageOptions)options
  97. progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
  98. completed:(nullable SDInternalCompletionBlock)completedBlock {
  99. // Invoking this method without a completedBlock is pointless
  100. NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
  101. // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
  102. // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
  103. if ([url isKindOfClass:NSString.class]) {
  104. url = [NSURL URLWithString:(NSString *)url];
  105. }
  106. // Prevents app crashing on argument type error like sending NSNull instead of NSURL
  107. if (![url isKindOfClass:NSURL.class]) {
  108. url = nil;
  109. }
  110. SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
  111. operation.manager = self;
  112. BOOL isFailedUrl = NO;
  113. if (url) {
  114. LOCK(self.failedURLsLock);
  115. isFailedUrl = [self.failedURLs containsObject:url];
  116. UNLOCK(self.failedURLsLock);
  117. }
  118. if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
  119. [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
  120. return operation;
  121. }
  122. LOCK(self.runningOperationsLock);
  123. [self.runningOperations addObject:operation];
  124. UNLOCK(self.runningOperationsLock);
  125. NSString *key = [self cacheKeyForURL:url];
  126. SDImageCacheOptions cacheOptions = 0;
  127. if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
  128. if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
  129. if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
  130. __weak SDWebImageCombinedOperation *weakOperation = operation;
  131. operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
  132. __strong __typeof(weakOperation) strongOperation = weakOperation;
  133. if (!strongOperation || strongOperation.isCancelled) {
  134. [self safelyRemoveOperationFromRunning:strongOperation];
  135. return;
  136. }
  137. // Check whether we should download image from network
  138. BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
  139. && (!cachedImage || options & SDWebImageRefreshCached)
  140. && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
  141. if (shouldDownload) {
  142. if (cachedImage && options & SDWebImageRefreshCached) {
  143. // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
  144. // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
  145. [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
  146. }
  147. // download if no image or requested to refresh anyway, and download allowed by delegate
  148. SDWebImageDownloaderOptions downloaderOptions = 0;
  149. if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
  150. if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
  151. if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
  152. if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
  153. if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
  154. if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
  155. if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
  156. if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
  157. if (cachedImage && options & SDWebImageRefreshCached) {
  158. // force progressive off if image already cached but forced refreshing
  159. downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
  160. // ignore image read from NSURLCache if image if cached but force refreshing
  161. downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
  162. }
  163. if (options & SDWebImageGifImageIncluded) downloaderOptions |= SDWebImageDownloaderGifIncluded;
  164. // `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block below, so we need weak-strong again to avoid retain cycle
  165. __weak typeof(strongOperation) weakSubOperation = strongOperation;
  166. strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
  167. __strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
  168. if (!strongSubOperation || strongSubOperation.isCancelled) {
  169. // Do nothing if the operation was cancelled
  170. // See #699 for more details
  171. // 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
  172. } else if (error) {
  173. [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
  174. BOOL shouldBlockFailedURL;
  175. // Check whether we should block failed url
  176. if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
  177. shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
  178. } else {
  179. shouldBlockFailedURL = ( error.code != NSURLErrorNotConnectedToInternet
  180. && error.code != NSURLErrorCancelled
  181. && error.code != NSURLErrorTimedOut
  182. && error.code != NSURLErrorInternationalRoamingOff
  183. && error.code != NSURLErrorDataNotAllowed
  184. && error.code != NSURLErrorCannotFindHost
  185. && error.code != NSURLErrorCannotConnectToHost
  186. && error.code != NSURLErrorNetworkConnectionLost);
  187. }
  188. if (shouldBlockFailedURL) {
  189. LOCK(self.failedURLsLock);
  190. [self.failedURLs addObject:url];
  191. UNLOCK(self.failedURLsLock);
  192. }
  193. }
  194. else {
  195. if ((options & SDWebImageRetryFailed)) {
  196. LOCK(self.failedURLsLock);
  197. [self.failedURLs removeObject:url];
  198. UNLOCK(self.failedURLsLock);
  199. }
  200. BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
  201. // We've done the scale process in SDWebImageDownloader with the shared manager, this is used for custom manager and avoid extra scale.
  202. if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
  203. downloadedImage = [self scaledImageForKey:key image:downloadedImage];
  204. }
  205. if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
  206. // Image refresh hit the NSURLCache cache, do not call the completion block
  207. } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
  208. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
  209. @autoreleasepool {
  210. UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
  211. if (transformedImage && finished) {
  212. BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
  213. NSData *cacheData;
  214. // pass nil if the image was transformed, so we can recalculate the data from the image
  215. if (self.cacheSerializer) {
  216. cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
  217. } else {
  218. cacheData = (imageWasTransformed ? nil : downloadedData);
  219. }
  220. [self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
  221. }
  222. [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
  223. }
  224. });
  225. } else {
  226. if (downloadedImage && finished) {
  227. if (self.cacheSerializer) {
  228. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
  229. @autoreleasepool {
  230. NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
  231. [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
  232. }
  233. });
  234. } else {
  235. [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
  236. }
  237. }
  238. [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
  239. }
  240. }
  241. if (finished) {
  242. [self safelyRemoveOperationFromRunning:strongSubOperation];
  243. }
  244. }];
  245. } else if (cachedImage) {
  246. [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
  247. [self safelyRemoveOperationFromRunning:strongOperation];
  248. } else {
  249. // Image not in cache and download disallowed by delegate
  250. [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
  251. [self safelyRemoveOperationFromRunning:strongOperation];
  252. }
  253. }];
  254. return operation;
  255. }
  256. - (void)saveImageToCache:(nullable UIImage *)image forURL:(nullable NSURL *)url {
  257. if (image && url) {
  258. NSString *key = [self cacheKeyForURL:url];
  259. [self.imageCache storeImage:image forKey:key toDisk:YES completion:nil];
  260. }
  261. }
  262. - (void)cancelAll {
  263. LOCK(self.runningOperationsLock);
  264. NSSet<SDWebImageCombinedOperation *> *copiedOperations = [self.runningOperations copy];
  265. UNLOCK(self.runningOperationsLock);
  266. [copiedOperations makeObjectsPerformSelector:@selector(cancel)]; // This will call `safelyRemoveOperationFromRunning:` and remove from the array
  267. }
  268. - (BOOL)isRunning {
  269. BOOL isRunning = NO;
  270. LOCK(self.runningOperationsLock);
  271. isRunning = (self.runningOperations.count > 0);
  272. UNLOCK(self.runningOperationsLock);
  273. return isRunning;
  274. }
  275. - (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation {
  276. if (!operation) {
  277. return;
  278. }
  279. LOCK(self.runningOperationsLock);
  280. [self.runningOperations removeObject:operation];
  281. UNLOCK(self.runningOperationsLock);
  282. }
  283. - (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
  284. completion:(nullable SDInternalCompletionBlock)completionBlock
  285. error:(nullable NSError *)error
  286. url:(nullable NSURL *)url {
  287. [self callCompletionBlockForOperation:operation completion:completionBlock image:nil data:nil error:error cacheType:SDImageCacheTypeNone finished:YES url:url];
  288. }
  289. - (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
  290. completion:(nullable SDInternalCompletionBlock)completionBlock
  291. image:(nullable UIImage *)image
  292. data:(nullable NSData *)data
  293. error:(nullable NSError *)error
  294. cacheType:(SDImageCacheType)cacheType
  295. finished:(BOOL)finished
  296. url:(nullable NSURL *)url {
  297. dispatch_main_async_safe(^{
  298. if (operation && !operation.isCancelled && completionBlock) {
  299. completionBlock(image, data, error, cacheType, finished, url);
  300. }
  301. });
  302. }
  303. @end
  304. @implementation SDWebImageCombinedOperation
  305. - (void)cancel {
  306. @synchronized(self) {
  307. self.cancelled = YES;
  308. if (self.cacheOperation) {
  309. [self.cacheOperation cancel];
  310. self.cacheOperation = nil;
  311. }
  312. if (self.downloadToken) {
  313. [self.manager.imageDownloader cancel:self.downloadToken];
  314. }
  315. [self.manager safelyRemoveOperationFromRunning:self];
  316. }
  317. }
  318. @end