No Description

SDWebImageDownloaderOperation.m 22KB

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