AFImageDownloader.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. // AFImageDownloader.m
  2. // Copyright (c) 2011–2016 Alamofire Software Foundation ( http://alamofire.org/ )
  3. //
  4. // Permission is hereby granted, free of charge, to any person obtaining a copy
  5. // of this software and associated documentation files (the "Software"), to deal
  6. // in the Software without restriction, including without limitation the rights
  7. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. // copies of the Software, and to permit persons to whom the Software is
  9. // furnished to do so, subject to the following conditions:
  10. //
  11. // The above copyright notice and this permission notice shall be included in
  12. // all copies or substantial portions of the Software.
  13. //
  14. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20. // THE SOFTWARE.
  21. #import <TargetConditionals.h>
  22. #if TARGET_OS_IOS || TARGET_OS_TV
  23. #import "AFImageDownloader.h"
  24. #import "AFHTTPSessionManager.h"
  25. @interface AFImageDownloaderResponseHandler : NSObject
  26. @property (nonatomic, strong) NSUUID *uuid;
  27. @property (nonatomic, copy) void (^successBlock)(NSURLRequest *, NSHTTPURLResponse *, UIImage *);
  28. @property (nonatomic, copy) void (^failureBlock)(NSURLRequest *, NSHTTPURLResponse *, NSError *);
  29. @end
  30. @implementation AFImageDownloaderResponseHandler
  31. - (instancetype)initWithUUID:(NSUUID *)uuid
  32. success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *responseObject))success
  33. failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure {
  34. if (self = [self init]) {
  35. self.uuid = uuid;
  36. self.successBlock = success;
  37. self.failureBlock = failure;
  38. }
  39. return self;
  40. }
  41. - (NSString *)description {
  42. return [NSString stringWithFormat: @"<AFImageDownloaderResponseHandler>UUID: %@", [self.uuid UUIDString]];
  43. }
  44. @end
  45. @interface AFImageDownloaderMergedTask : NSObject
  46. @property (nonatomic, strong) NSString *URLIdentifier;
  47. @property (nonatomic, strong) NSUUID *identifier;
  48. @property (nonatomic, strong) NSURLSessionDataTask *task;
  49. @property (nonatomic, strong) NSMutableArray <AFImageDownloaderResponseHandler*> *responseHandlers;
  50. @end
  51. @implementation AFImageDownloaderMergedTask
  52. - (instancetype)initWithURLIdentifier:(NSString *)URLIdentifier identifier:(NSUUID *)identifier task:(NSURLSessionDataTask *)task {
  53. if (self = [self init]) {
  54. self.URLIdentifier = URLIdentifier;
  55. self.task = task;
  56. self.identifier = identifier;
  57. self.responseHandlers = [[NSMutableArray alloc] init];
  58. }
  59. return self;
  60. }
  61. - (void)addResponseHandler:(AFImageDownloaderResponseHandler *)handler {
  62. [self.responseHandlers addObject:handler];
  63. }
  64. - (void)removeResponseHandler:(AFImageDownloaderResponseHandler *)handler {
  65. [self.responseHandlers removeObject:handler];
  66. }
  67. @end
  68. @implementation AFImageDownloadReceipt
  69. - (instancetype)initWithReceiptID:(NSUUID *)receiptID task:(NSURLSessionDataTask *)task {
  70. if (self = [self init]) {
  71. self.receiptID = receiptID;
  72. self.task = task;
  73. }
  74. return self;
  75. }
  76. @end
  77. @interface AFImageDownloader ()
  78. @property (nonatomic, strong) dispatch_queue_t synchronizationQueue;
  79. @property (nonatomic, strong) dispatch_queue_t responseQueue;
  80. @property (nonatomic, assign) NSInteger maximumActiveDownloads;
  81. @property (nonatomic, assign) NSInteger activeRequestCount;
  82. @property (nonatomic, strong) NSMutableArray *queuedMergedTasks;
  83. @property (nonatomic, strong) NSMutableDictionary *mergedTasks;
  84. @end
  85. @implementation AFImageDownloader
  86. + (NSURLCache *)defaultURLCache {
  87. NSUInteger memoryCapacity = 20 * 1024 * 1024; // 20MB
  88. NSUInteger diskCapacity = 150 * 1024 * 1024; // 150MB
  89. NSURL *cacheURL = [[[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory
  90. inDomain:NSUserDomainMask
  91. appropriateForURL:nil
  92. create:YES
  93. error:nil]
  94. URLByAppendingPathComponent:@"com.alamofire.imagedownloader"];
  95. #if TARGET_OS_MACCATALYST
  96. return [[NSURLCache alloc] initWithMemoryCapacity:memoryCapacity
  97. diskCapacity:diskCapacity
  98. directoryURL:cacheURL];
  99. #else
  100. return [[NSURLCache alloc] initWithMemoryCapacity:memoryCapacity
  101. diskCapacity:diskCapacity
  102. diskPath:[cacheURL path]];
  103. #endif
  104. }
  105. + (NSURLSessionConfiguration *)defaultURLSessionConfiguration {
  106. NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
  107. //TODO set the default HTTP headers
  108. configuration.HTTPShouldSetCookies = YES;
  109. configuration.HTTPShouldUsePipelining = NO;
  110. configuration.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
  111. configuration.allowsCellularAccess = YES;
  112. configuration.timeoutIntervalForRequest = 60.0;
  113. configuration.URLCache = [AFImageDownloader defaultURLCache];
  114. return configuration;
  115. }
  116. - (instancetype)init {
  117. NSURLSessionConfiguration *defaultConfiguration = [self.class defaultURLSessionConfiguration];
  118. return [self initWithSessionConfiguration:defaultConfiguration];
  119. }
  120. - (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
  121. AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration];
  122. sessionManager.responseSerializer = [AFImageResponseSerializer serializer];
  123. return [self initWithSessionManager:sessionManager
  124. downloadPrioritization:AFImageDownloadPrioritizationFIFO
  125. maximumActiveDownloads:4
  126. imageCache:[[AFAutoPurgingImageCache alloc] init]];
  127. }
  128. - (instancetype)initWithSessionManager:(AFHTTPSessionManager *)sessionManager
  129. downloadPrioritization:(AFImageDownloadPrioritization)downloadPrioritization
  130. maximumActiveDownloads:(NSInteger)maximumActiveDownloads
  131. imageCache:(id <AFImageRequestCache>)imageCache {
  132. if (self = [super init]) {
  133. self.sessionManager = sessionManager;
  134. self.downloadPrioritization = downloadPrioritization;
  135. self.maximumActiveDownloads = maximumActiveDownloads;
  136. self.imageCache = imageCache;
  137. self.queuedMergedTasks = [[NSMutableArray alloc] init];
  138. self.mergedTasks = [[NSMutableDictionary alloc] init];
  139. self.activeRequestCount = 0;
  140. NSString *name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.synchronizationqueue-%@", [[NSUUID UUID] UUIDString]];
  141. self.synchronizationQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL);
  142. name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.responsequeue-%@", [[NSUUID UUID] UUIDString]];
  143. self.responseQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);
  144. }
  145. return self;
  146. }
  147. + (instancetype)defaultInstance {
  148. static AFImageDownloader *sharedInstance = nil;
  149. static dispatch_once_t onceToken;
  150. dispatch_once(&onceToken, ^{
  151. sharedInstance = [[self alloc] init];
  152. });
  153. return sharedInstance;
  154. }
  155. - (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request
  156. success:(void (^)(NSURLRequest * _Nonnull, NSHTTPURLResponse * _Nullable, UIImage * _Nonnull))success
  157. failure:(void (^)(NSURLRequest * _Nonnull, NSHTTPURLResponse * _Nullable, NSError * _Nonnull))failure {
  158. return [self downloadImageForURLRequest:request withReceiptID:[NSUUID UUID] success:success failure:failure];
  159. }
  160. - (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request
  161. withReceiptID:(nonnull NSUUID *)receiptID
  162. success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *responseObject))success
  163. failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure {
  164. __block NSURLSessionDataTask *task = nil;
  165. dispatch_sync(self.synchronizationQueue, ^{
  166. NSString *URLIdentifier = request.URL.absoluteString;
  167. if (URLIdentifier == nil) {
  168. if (failure) {
  169. NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil];
  170. dispatch_async(dispatch_get_main_queue(), ^{
  171. failure(request, nil, error);
  172. });
  173. }
  174. return;
  175. }
  176. // 1) Append the success and failure blocks to a pre-existing request if it already exists
  177. AFImageDownloaderMergedTask *existingMergedTask = self.mergedTasks[URLIdentifier];
  178. if (existingMergedTask != nil) {
  179. AFImageDownloaderResponseHandler *handler = [[AFImageDownloaderResponseHandler alloc] initWithUUID:receiptID success:success failure:failure];
  180. [existingMergedTask addResponseHandler:handler];
  181. task = existingMergedTask.task;
  182. return;
  183. }
  184. // 2) Attempt to load the image from the image cache if the cache policy allows it
  185. switch (request.cachePolicy) {
  186. case NSURLRequestUseProtocolCachePolicy:
  187. case NSURLRequestReturnCacheDataElseLoad:
  188. case NSURLRequestReturnCacheDataDontLoad: {
  189. UIImage *cachedImage = [self.imageCache imageforRequest:request withAdditionalIdentifier:nil];
  190. if (cachedImage != nil) {
  191. if (success) {
  192. dispatch_async(dispatch_get_main_queue(), ^{
  193. success(request, nil, cachedImage);
  194. });
  195. }
  196. return;
  197. }
  198. break;
  199. }
  200. default:
  201. break;
  202. }
  203. // 3) Create the request and set up authentication, validation and response serialization
  204. NSUUID *mergedTaskIdentifier = [NSUUID UUID];
  205. NSURLSessionDataTask *createdTask;
  206. __weak __typeof__(self) weakSelf = self;
  207. createdTask = [self.sessionManager
  208. dataTaskWithRequest:request
  209. uploadProgress:nil
  210. downloadProgress:nil
  211. completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
  212. dispatch_async(self.responseQueue, ^{
  213. __strong __typeof__(weakSelf) strongSelf = weakSelf;
  214. AFImageDownloaderMergedTask *mergedTask = [strongSelf safelyGetMergedTask:URLIdentifier];
  215. if ([mergedTask.identifier isEqual:mergedTaskIdentifier]) {
  216. mergedTask = [strongSelf safelyRemoveMergedTaskWithURLIdentifier:URLIdentifier];
  217. if (error) {
  218. for (AFImageDownloaderResponseHandler *handler in mergedTask.responseHandlers) {
  219. if (handler.failureBlock) {
  220. dispatch_async(dispatch_get_main_queue(), ^{
  221. handler.failureBlock(request, (NSHTTPURLResponse *)response, error);
  222. });
  223. }
  224. }
  225. } else {
  226. if ([strongSelf.imageCache shouldCacheImage:responseObject forRequest:request withAdditionalIdentifier:nil]) {
  227. [strongSelf.imageCache addImage:responseObject forRequest:request withAdditionalIdentifier:nil];
  228. }
  229. for (AFImageDownloaderResponseHandler *handler in mergedTask.responseHandlers) {
  230. if (handler.successBlock) {
  231. dispatch_async(dispatch_get_main_queue(), ^{
  232. handler.successBlock(request, (NSHTTPURLResponse *)response, responseObject);
  233. });
  234. }
  235. }
  236. }
  237. }
  238. [strongSelf safelyDecrementActiveTaskCount];
  239. [strongSelf safelyStartNextTaskIfNecessary];
  240. });
  241. }];
  242. // 4) Store the response handler for use when the request completes
  243. AFImageDownloaderResponseHandler *handler = [[AFImageDownloaderResponseHandler alloc] initWithUUID:receiptID
  244. success:success
  245. failure:failure];
  246. AFImageDownloaderMergedTask *mergedTask = [[AFImageDownloaderMergedTask alloc]
  247. initWithURLIdentifier:URLIdentifier
  248. identifier:mergedTaskIdentifier
  249. task:createdTask];
  250. [mergedTask addResponseHandler:handler];
  251. self.mergedTasks[URLIdentifier] = mergedTask;
  252. // 5) Either start the request or enqueue it depending on the current active request count
  253. if ([self isActiveRequestCountBelowMaximumLimit]) {
  254. [self startMergedTask:mergedTask];
  255. } else {
  256. [self enqueueMergedTask:mergedTask];
  257. }
  258. task = mergedTask.task;
  259. });
  260. if (task) {
  261. return [[AFImageDownloadReceipt alloc] initWithReceiptID:receiptID task:task];
  262. } else {
  263. return nil;
  264. }
  265. }
  266. - (void)cancelTaskForImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt {
  267. dispatch_sync(self.synchronizationQueue, ^{
  268. NSString *URLIdentifier = imageDownloadReceipt.task.originalRequest.URL.absoluteString;
  269. AFImageDownloaderMergedTask *mergedTask = self.mergedTasks[URLIdentifier];
  270. NSUInteger index = [mergedTask.responseHandlers indexOfObjectPassingTest:^BOOL(AFImageDownloaderResponseHandler * _Nonnull handler, __unused NSUInteger idx, __unused BOOL * _Nonnull stop) {
  271. return handler.uuid == imageDownloadReceipt.receiptID;
  272. }];
  273. if (index != NSNotFound) {
  274. AFImageDownloaderResponseHandler *handler = mergedTask.responseHandlers[index];
  275. [mergedTask removeResponseHandler:handler];
  276. NSString *failureReason = [NSString stringWithFormat:@"ImageDownloader cancelled URL request: %@",imageDownloadReceipt.task.originalRequest.URL.absoluteString];
  277. NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey:failureReason};
  278. NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:userInfo];
  279. if (handler.failureBlock) {
  280. dispatch_async(dispatch_get_main_queue(), ^{
  281. handler.failureBlock(imageDownloadReceipt.task.originalRequest, nil, error);
  282. });
  283. }
  284. }
  285. if (mergedTask.responseHandlers.count == 0) {
  286. [mergedTask.task cancel];
  287. [self removeMergedTaskWithURLIdentifier:URLIdentifier];
  288. }
  289. });
  290. }
  291. - (AFImageDownloaderMergedTask *)safelyRemoveMergedTaskWithURLIdentifier:(NSString *)URLIdentifier {
  292. __block AFImageDownloaderMergedTask *mergedTask = nil;
  293. dispatch_sync(self.synchronizationQueue, ^{
  294. mergedTask = [self removeMergedTaskWithURLIdentifier:URLIdentifier];
  295. });
  296. return mergedTask;
  297. }
  298. //This method should only be called from safely within the synchronizationQueue
  299. - (AFImageDownloaderMergedTask *)removeMergedTaskWithURLIdentifier:(NSString *)URLIdentifier {
  300. AFImageDownloaderMergedTask *mergedTask = self.mergedTasks[URLIdentifier];
  301. [self.mergedTasks removeObjectForKey:URLIdentifier];
  302. return mergedTask;
  303. }
  304. - (void)safelyDecrementActiveTaskCount {
  305. dispatch_sync(self.synchronizationQueue, ^{
  306. if (self.activeRequestCount > 0) {
  307. self.activeRequestCount -= 1;
  308. }
  309. });
  310. }
  311. - (void)safelyStartNextTaskIfNecessary {
  312. dispatch_sync(self.synchronizationQueue, ^{
  313. if ([self isActiveRequestCountBelowMaximumLimit]) {
  314. while (self.queuedMergedTasks.count > 0) {
  315. AFImageDownloaderMergedTask *mergedTask = [self dequeueMergedTask];
  316. if (mergedTask.task.state == NSURLSessionTaskStateSuspended) {
  317. [self startMergedTask:mergedTask];
  318. break;
  319. }
  320. }
  321. }
  322. });
  323. }
  324. - (void)startMergedTask:(AFImageDownloaderMergedTask *)mergedTask {
  325. [mergedTask.task resume];
  326. ++self.activeRequestCount;
  327. }
  328. - (void)enqueueMergedTask:(AFImageDownloaderMergedTask *)mergedTask {
  329. switch (self.downloadPrioritization) {
  330. case AFImageDownloadPrioritizationFIFO:
  331. [self.queuedMergedTasks addObject:mergedTask];
  332. break;
  333. case AFImageDownloadPrioritizationLIFO:
  334. [self.queuedMergedTasks insertObject:mergedTask atIndex:0];
  335. break;
  336. }
  337. }
  338. - (AFImageDownloaderMergedTask *)dequeueMergedTask {
  339. AFImageDownloaderMergedTask *mergedTask = nil;
  340. mergedTask = [self.queuedMergedTasks firstObject];
  341. [self.queuedMergedTasks removeObject:mergedTask];
  342. return mergedTask;
  343. }
  344. - (BOOL)isActiveRequestCountBelowMaximumLimit {
  345. return self.activeRequestCount < self.maximumActiveDownloads;
  346. }
  347. - (AFImageDownloaderMergedTask *)safelyGetMergedTask:(NSString *)URLIdentifier {
  348. __block AFImageDownloaderMergedTask *mergedTask;
  349. dispatch_sync(self.synchronizationQueue, ^(){
  350. mergedTask = self.mergedTasks[URLIdentifier];
  351. });
  352. return mergedTask;
  353. }
  354. @end
  355. #endif