SDWebImageDownloaderOperation.m 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  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 "SDWebImageDownloaderOperation.h"
  9. #import "SDWebImageManager.h"
  10. #import "NSImage+WebCache.h"
  11. #import "SDWebImageCodersManager.h"
  12. #import "UIImage+MultiFormat.h"
  13. #define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
  14. #define UNLOCK(lock) dispatch_semaphore_signal(lock);
  15. // iOS 8 Foundation.framework extern these symbol but the define is in CFNetwork.framework. We just fix this without import CFNetwork.framework
  16. #if (__IPHONE_OS_VERSION_MIN_REQUIRED && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0)
  17. const float NSURLSessionTaskPriorityHigh = 0.75;
  18. const float NSURLSessionTaskPriorityDefault = 0.5;
  19. const float NSURLSessionTaskPriorityLow = 0.25;
  20. #endif
  21. NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
  22. NSString *const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification";
  23. NSString *const SDWebImageDownloadStopNotification = @"SDWebImageDownloadStopNotification";
  24. NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification";
  25. static NSString *const kProgressCallbackKey = @"progress";
  26. static NSString *const kCompletedCallbackKey = @"completed";
  27. typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
  28. @interface SDWebImageDownloaderOperation ()
  29. @property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;
  30. @property (assign, nonatomic, getter = isExecuting) BOOL executing;
  31. @property (assign, nonatomic, getter = isFinished) BOOL finished;
  32. @property (strong, nonatomic, nullable) NSMutableData *imageData;
  33. @property (copy, nonatomic, nullable) NSData *cachedData; // for `SDWebImageDownloaderIgnoreCachedResponse`
  34. // This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run
  35. // the task associated with this operation
  36. @property (weak, nonatomic, nullable) NSURLSession *unownedSession;
  37. // This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one
  38. @property (strong, nonatomic, nullable) NSURLSession *ownedSession;
  39. @property (strong, nonatomic, readwrite, nullable) NSURLSessionTask *dataTask;
  40. @property (strong, nonatomic, nonnull) dispatch_semaphore_t callbacksLock; // a lock to keep the access to `callbackBlocks` thread-safe
  41. @property (strong, nonatomic, nonnull) dispatch_queue_t coderQueue; // the queue to do image decoding
  42. #if SD_UIKIT
  43. @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
  44. #endif
  45. @property (strong, nonatomic, nullable) id<SDWebImageProgressiveCoder> progressiveCoder;
  46. @end
  47. @implementation SDWebImageDownloaderOperation
  48. @synthesize executing = _executing;
  49. @synthesize finished = _finished;
  50. - (nonnull instancetype)init {
  51. return [self initWithRequest:nil inSession:nil options:0];
  52. }
  53. - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
  54. inSession:(nullable NSURLSession *)session
  55. options:(SDWebImageDownloaderOptions)options {
  56. if ((self = [super init])) {
  57. _request = [request copy];
  58. _shouldDecompressImages = YES;
  59. _options = options;
  60. _callbackBlocks = [NSMutableArray new];
  61. _executing = NO;
  62. _finished = NO;
  63. _expectedSize = 0;
  64. _unownedSession = session;
  65. _callbacksLock = dispatch_semaphore_create(1);
  66. _coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);
  67. #if SD_UIKIT
  68. _backgroundTaskId = UIBackgroundTaskInvalid;
  69. #endif
  70. }
  71. return self;
  72. }
  73. - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
  74. completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
  75. SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
  76. if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
  77. if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
  78. LOCK(self.callbacksLock);
  79. [self.callbackBlocks addObject:callbacks];
  80. UNLOCK(self.callbacksLock);
  81. return callbacks;
  82. }
  83. - (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
  84. LOCK(self.callbacksLock);
  85. NSMutableArray<id> *callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
  86. UNLOCK(self.callbacksLock);
  87. // We need to remove [NSNull null] because there might not always be a progress block for each callback
  88. [callbacks removeObjectIdenticalTo:[NSNull null]];
  89. return [callbacks copy]; // strip mutability here
  90. }
  91. - (BOOL)cancel:(nullable id)token {
  92. BOOL shouldCancel = NO;
  93. LOCK(self.callbacksLock);
  94. [self.callbackBlocks removeObjectIdenticalTo:token];
  95. if (self.callbackBlocks.count == 0) {
  96. shouldCancel = YES;
  97. }
  98. UNLOCK(self.callbacksLock);
  99. if (shouldCancel) {
  100. [self cancel];
  101. }
  102. return shouldCancel;
  103. }
  104. - (void)start {
  105. @synchronized (self) {
  106. if (self.isCancelled) {
  107. self.finished = YES;
  108. [self reset];
  109. return;
  110. }
  111. #if SD_UIKIT
  112. Class UIApplicationClass = NSClassFromString(@"UIApplication");
  113. BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
  114. if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
  115. __weak __typeof__ (self) wself = self;
  116. UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
  117. self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
  118. [wself cancel];
  119. }];
  120. }
  121. #endif
  122. NSURLSession *session = self.unownedSession;
  123. if (!session) {
  124. NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
  125. sessionConfig.timeoutIntervalForRequest = 15;
  126. /**
  127. * Create the session for this task
  128. * We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
  129. * method calls and completion handler calls.
  130. */
  131. session = [NSURLSession sessionWithConfiguration:sessionConfig
  132. delegate:self
  133. delegateQueue:nil];
  134. self.ownedSession = session;
  135. }
  136. if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
  137. // Grab the cached data for later check
  138. NSURLCache *URLCache = session.configuration.URLCache;
  139. if (!URLCache) {
  140. URLCache = [NSURLCache sharedURLCache];
  141. }
  142. NSCachedURLResponse *cachedResponse;
  143. // NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483
  144. @synchronized (URLCache) {
  145. cachedResponse = [URLCache cachedResponseForRequest:self.request];
  146. }
  147. if (cachedResponse) {
  148. self.cachedData = cachedResponse.data;
  149. }
  150. }
  151. self.dataTask = [session dataTaskWithRequest:self.request];
  152. self.executing = YES;
  153. }
  154. if (self.dataTask) {
  155. #pragma clang diagnostic push
  156. #pragma clang diagnostic ignored "-Wunguarded-availability"
  157. if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
  158. if (self.options & SDWebImageDownloaderHighPriority) {
  159. self.dataTask.priority = NSURLSessionTaskPriorityHigh;
  160. } else if (self.options & SDWebImageDownloaderLowPriority) {
  161. self.dataTask.priority = NSURLSessionTaskPriorityLow;
  162. }
  163. }
  164. #pragma clang diagnostic pop
  165. [self.dataTask resume];
  166. for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
  167. progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
  168. }
  169. __block typeof(self) strongSelf = self;
  170. dispatch_async(dispatch_get_main_queue(), ^{
  171. [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:strongSelf];
  172. });
  173. } else {
  174. [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
  175. [self done];
  176. }
  177. }
  178. - (void)cancel {
  179. @synchronized (self) {
  180. [self cancelInternal];
  181. }
  182. }
  183. - (void)cancelInternal {
  184. if (self.isFinished) return;
  185. [super cancel];
  186. if (self.dataTask) {
  187. [self.dataTask cancel];
  188. __block typeof(self) strongSelf = self;
  189. dispatch_async(dispatch_get_main_queue(), ^{
  190. [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:strongSelf];
  191. });
  192. // As we cancelled the task, its callback won't be called and thus won't
  193. // maintain the isFinished and isExecuting flags.
  194. if (self.isExecuting) self.executing = NO;
  195. if (!self.isFinished) self.finished = YES;
  196. }
  197. [self reset];
  198. }
  199. - (void)done {
  200. self.finished = YES;
  201. self.executing = NO;
  202. [self reset];
  203. }
  204. - (void)reset {
  205. LOCK(self.callbacksLock);
  206. [self.callbackBlocks removeAllObjects];
  207. UNLOCK(self.callbacksLock);
  208. @synchronized (self) {
  209. self.dataTask = nil;
  210. if (self.ownedSession) {
  211. [self.ownedSession invalidateAndCancel];
  212. self.ownedSession = nil;
  213. }
  214. #if SD_UIKIT
  215. if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
  216. // If backgroundTaskId != UIBackgroundTaskInvalid, sharedApplication is always exist
  217. UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
  218. [app endBackgroundTask:self.backgroundTaskId];
  219. self.backgroundTaskId = UIBackgroundTaskInvalid;
  220. }
  221. #endif
  222. }
  223. }
  224. - (void)setFinished:(BOOL)finished {
  225. [self willChangeValueForKey:@"isFinished"];
  226. _finished = finished;
  227. [self didChangeValueForKey:@"isFinished"];
  228. }
  229. - (void)setExecuting:(BOOL)executing {
  230. [self willChangeValueForKey:@"isExecuting"];
  231. _executing = executing;
  232. [self didChangeValueForKey:@"isExecuting"];
  233. }
  234. - (BOOL)isConcurrent {
  235. return YES;
  236. }
  237. #pragma mark NSURLSessionDataDelegate
  238. - (void)URLSession:(NSURLSession *)session
  239. dataTask:(NSURLSessionDataTask *)dataTask
  240. didReceiveResponse:(NSURLResponse *)response
  241. completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
  242. NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;
  243. NSInteger expected = (NSInteger)response.expectedContentLength;
  244. expected = expected > 0 ? expected : 0;
  245. self.expectedSize = expected;
  246. self.response = response;
  247. NSInteger statusCode = [response respondsToSelector:@selector(statusCode)] ? ((NSHTTPURLResponse *)response).statusCode : 200;
  248. BOOL valid = statusCode < 400;
  249. //'304 Not Modified' is an exceptional one. It should be treated as cancelled if no cache data
  250. //URLSession current behavior will return 200 status code when the server respond 304 and URLCache hit. But this is not a standard behavior and we just add a check
  251. if (statusCode == 304 && !self.cachedData) {
  252. valid = NO;
  253. }
  254. if (valid) {
  255. for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
  256. progressBlock(0, expected, self.request.URL);
  257. }
  258. } else {
  259. // Status code invalid and marked as cancelled. Do not call `[self.dataTask cancel]` which may mass up URLSession life cycle
  260. disposition = NSURLSessionResponseCancel;
  261. }
  262. __block typeof(self) strongSelf = self;
  263. dispatch_async(dispatch_get_main_queue(), ^{
  264. [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:strongSelf];
  265. });
  266. if (completionHandler) {
  267. completionHandler(disposition);
  268. }
  269. }
  270. - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
  271. if (!self.imageData) {
  272. self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
  273. }
  274. [self.imageData appendData:data];
  275. if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
  276. // Get the image data
  277. __block NSData *imageData = [self.imageData copy];
  278. // Get the total bytes downloaded
  279. const NSInteger totalSize = imageData.length;
  280. // Get the finish status
  281. BOOL finished = (totalSize >= self.expectedSize);
  282. if (!self.progressiveCoder) {
  283. // We need to create a new instance for progressive decoding to avoid conflicts
  284. for (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedInstance].coders) {
  285. if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
  286. [((id<SDWebImageProgressiveCoder>)coder) canIncrementallyDecodeFromData:imageData]) {
  287. self.progressiveCoder = [[[coder class] alloc] init];
  288. break;
  289. }
  290. }
  291. }
  292. // progressive decode the image in coder queue
  293. dispatch_async(self.coderQueue, ^{
  294. @autoreleasepool {
  295. UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
  296. if (image) {
  297. NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
  298. image = [self scaledImageForKey:key image:image];
  299. if (self.shouldDecompressImages) {
  300. image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
  301. }
  302. // 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.
  303. [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
  304. }
  305. }
  306. });
  307. }
  308. for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
  309. progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
  310. }
  311. }
  312. - (void)URLSession:(NSURLSession *)session
  313. dataTask:(NSURLSessionDataTask *)dataTask
  314. willCacheResponse:(NSCachedURLResponse *)proposedResponse
  315. completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
  316. NSCachedURLResponse *cachedResponse = proposedResponse;
  317. if (!(self.options & SDWebImageDownloaderUseNSURLCache)) {
  318. // Prevents caching of responses
  319. cachedResponse = nil;
  320. }
  321. if (completionHandler) {
  322. completionHandler(cachedResponse);
  323. }
  324. }
  325. #pragma mark NSURLSessionTaskDelegate
  326. - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
  327. @synchronized(self) {
  328. self.dataTask = nil;
  329. __block typeof(self) strongSelf = self;
  330. dispatch_async(dispatch_get_main_queue(), ^{
  331. [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:strongSelf];
  332. if (!error) {
  333. [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:strongSelf];
  334. }
  335. });
  336. }
  337. // make sure to call `[self done]` to mark operation as finished
  338. if (error) {
  339. [self callCompletionBlocksWithError:error];
  340. [self done];
  341. } else {
  342. if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
  343. /**
  344. * If you specified to use `NSURLCache`, then the response you get here is what you need.
  345. */
  346. __block NSData *imageData = [self.imageData copy];
  347. self.imageData = nil;
  348. if (imageData) {
  349. if (self.options & SDWebImageDownloaderGifIncluded && imageData) {
  350. [self callCompletionBlocksWithImage:nil imageData:imageData error:nil finished:YES];
  351. [self done];
  352. return;
  353. }
  354. /** if you specified to only use cached data via `SDWebImageDownloaderIgnoreCachedResponse`,
  355. * then we should check if the cached data is equal to image data
  356. */
  357. if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
  358. // call completion block with nil
  359. [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
  360. [self done];
  361. } else {
  362. // decode the image in coder queue
  363. dispatch_async(self.coderQueue, ^{
  364. @autoreleasepool {
  365. UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
  366. NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
  367. image = [self scaledImageForKey:key image:image];
  368. // Do not force decoding animated images or GIF,
  369. // because there has imageCoder which can change `image` or `imageData` to static image, lose the animated feature totally.
  370. BOOL shouldDecode = !image.images && image.sd_imageFormat != SDImageFormatGIF;
  371. if (shouldDecode) {
  372. if (self.shouldDecompressImages) {
  373. BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
  374. image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
  375. }
  376. }
  377. CGSize imageSize = image.size;
  378. if (imageSize.width == 0 || imageSize.height == 0) {
  379. [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
  380. } else {
  381. [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
  382. }
  383. [self done];
  384. }
  385. });
  386. }
  387. } else {
  388. [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
  389. [self done];
  390. }
  391. } else {
  392. [self done];
  393. }
  394. }
  395. }
  396. - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
  397. NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
  398. __block NSURLCredential *credential = nil;
  399. if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
  400. if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) {
  401. disposition = NSURLSessionAuthChallengePerformDefaultHandling;
  402. } else {
  403. credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
  404. disposition = NSURLSessionAuthChallengeUseCredential;
  405. }
  406. } else {
  407. if (challenge.previousFailureCount == 0) {
  408. if (self.credential) {
  409. credential = self.credential;
  410. disposition = NSURLSessionAuthChallengeUseCredential;
  411. } else {
  412. disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
  413. }
  414. } else {
  415. disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
  416. }
  417. }
  418. if (completionHandler) {
  419. completionHandler(disposition, credential);
  420. }
  421. }
  422. #pragma mark Helper methods
  423. - (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image {
  424. return SDScaledImageForKey(key, image);
  425. }
  426. - (BOOL)shouldContinueWhenAppEntersBackground {
  427. return self.options & SDWebImageDownloaderContinueInBackground;
  428. }
  429. - (void)callCompletionBlocksWithError:(nullable NSError *)error {
  430. [self callCompletionBlocksWithImage:nil imageData:nil error:error finished:YES];
  431. }
  432. - (void)callCompletionBlocksWithImage:(nullable UIImage *)image
  433. imageData:(nullable NSData *)imageData
  434. error:(nullable NSError *)error
  435. finished:(BOOL)finished {
  436. NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
  437. dispatch_main_async_safe(^{
  438. for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
  439. completedBlock(image, imageData, error, finished);
  440. }
  441. });
  442. }
  443. @end