SDWebImageDownloaderOperation.m 21 KB

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