MCDownloader.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. //
  2. // MCDownloader.m
  3. // MCDownloadManager
  4. //
  5. // Created by M.C on 17/4/6. (QQ:714080794 Gmail:chaoma0609@gmail.com)
  6. // Copyright © 2017年 qikeyun. All rights reserved.
  7. //
  8. // Permission is hereby granted, free of charge, to any person obtaining a copy
  9. // of this software and associated documentation files (the "Software"), to deal
  10. // in the Software without restriction, including without limitation the rights
  11. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  12. // copies of the Software, and to permit persons to whom the Software is
  13. // furnished to do so, subject to the following conditions:
  14. //
  15. // The above copyright notice and this permission notice shall be included in
  16. // all copies or substantial portions of the Software.
  17. //
  18. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  19. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  20. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  21. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  22. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  23. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  24. // THE SOFTWARE.
  25. //
  26. #import "MCDownloader.h"
  27. #import "MCDownloadOperation.h"
  28. #import "MCDownloadReceipt.h"
  29. NSString * const MCDownloadCacheFolderName = @"DownloadCache";
  30. static NSString *cacheFolderPath;
  31. NSString * cacheFolder() {
  32. if (!cacheFolderPath) {
  33. NSString *cacheDir = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
  34. cacheFolderPath = [cacheDir stringByAppendingPathComponent:MCDownloadCacheFolderName];
  35. NSFileManager *filemgr = [NSFileManager defaultManager];
  36. NSError *error = nil;
  37. if(![filemgr createDirectoryAtPath:cacheFolderPath withIntermediateDirectories:YES attributes:nil error:&error]) {
  38. NSLog(@"Failed to create cache directory at %@", cacheFolderPath);
  39. cacheFolderPath = nil;
  40. }
  41. }
  42. return cacheFolderPath;
  43. }
  44. static void clearCacheFolder() {
  45. cacheFolderPath = nil;
  46. }
  47. static NSString * LocalReceiptsPath() {
  48. return [cacheFolder() stringByAppendingPathComponent:@"receipts.data"];
  49. }
  50. @interface MCDownloader() <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
  51. @property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;
  52. @property (weak, nonatomic, nullable) NSOperation *lastAddedOperation;
  53. @property (assign, nonatomic, nullable) Class operationClass;
  54. @property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, MCDownloadOperation *> *URLOperations;
  55. @property (strong, nonatomic, nullable) MCHTTPHeadersMutableDictionary *HTTPHeaders;
  56. // This queue is used to serialize the handling of the network responses of all the download operation in a single queue
  57. @property (strong, nonatomic, nullable) dispatch_queue_t barrierQueue;
  58. // The session in which data tasks will run
  59. @property (strong, nonatomic) NSURLSession *session;
  60. @property (nonatomic, strong) NSMutableDictionary *allDownloadReceipts;
  61. @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
  62. @end
  63. @implementation MCDownloader
  64. - (NSMutableDictionary *)allDownloadReceipts {
  65. if (_allDownloadReceipts == nil) {
  66. NSDictionary *receipts = [NSKeyedUnarchiver unarchiveObjectWithFile:LocalReceiptsPath()];
  67. _allDownloadReceipts = receipts != nil ? receipts.mutableCopy : [NSMutableDictionary dictionary];
  68. }
  69. return _allDownloadReceipts;
  70. }
  71. + (nonnull instancetype)sharedDownloader {
  72. static dispatch_once_t once;
  73. static id instance;
  74. dispatch_once(&once, ^{
  75. instance = [self new];
  76. });
  77. return instance;
  78. }
  79. - (nonnull instancetype)init {
  80. return [self initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
  81. }
  82. - (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
  83. if ((self = [super init])) {
  84. _operationClass = [MCDownloadOperation class];
  85. _downloadPrioritizaton = MCDownloadPrioritizationFIFO;
  86. _downloadQueue = [NSOperationQueue new];
  87. _downloadQueue.maxConcurrentOperationCount = 3;
  88. _downloadQueue.name = @"com.machao.MCDownloader";
  89. _URLOperations = [NSMutableDictionary new];
  90. _barrierQueue = dispatch_queue_create("com.machao.MCDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
  91. _downloadTimeout = 15.0;
  92. sessionConfiguration.timeoutIntervalForRequest = _downloadTimeout;
  93. sessionConfiguration.HTTPMaximumConnectionsPerHost = 10;
  94. /**
  95. * Create the session for this task
  96. * We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
  97. * method calls and completion handler calls.
  98. */
  99. self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
  100. delegate:self
  101. delegateQueue:nil];
  102. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil];
  103. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
  104. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
  105. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
  106. }
  107. return self;
  108. }
  109. #pragma mark - NSNotification
  110. - (void)applicationWillTerminate:(NSNotification *)not {
  111. [self setAllStateToNone];
  112. [self saveAllDownloadReceipts];
  113. }
  114. - (void)applicationDidReceiveMemoryWarning:(NSNotification *)not {
  115. [self saveAllDownloadReceipts];
  116. }
  117. - (void)applicationWillResignActive:(NSNotification *)not {
  118. [self saveAllDownloadReceipts];
  119. /// 捕获到失去激活状态后
  120. Class UIApplicationClass = NSClassFromString(@"UIApplication");
  121. BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
  122. if (hasApplication ) {
  123. __weak __typeof__ (self) wself = self;
  124. UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
  125. self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
  126. __strong __typeof (wself) sself = wself;
  127. if (sself) {
  128. [sself setAllStateToNone];
  129. [sself saveAllDownloadReceipts];
  130. [app endBackgroundTask:sself.backgroundTaskId];
  131. sself.backgroundTaskId = UIBackgroundTaskInvalid;
  132. }
  133. }];
  134. }
  135. }
  136. - (void)applicationDidBecomeActive:(NSNotification *)not {
  137. Class UIApplicationClass = NSClassFromString(@"UIApplication");
  138. if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
  139. return;
  140. }
  141. if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
  142. UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
  143. [app endBackgroundTask:self.backgroundTaskId];
  144. self.backgroundTaskId = UIBackgroundTaskInvalid;
  145. }
  146. }
  147. - (void)setAllStateToNone {
  148. [self.allDownloadReceipts enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
  149. if ([obj isKindOfClass:[MCDownloadReceipt class]]) {
  150. MCDownloadReceipt *receipt = obj;
  151. if (receipt.state != MCDownloadStateCompleted) {
  152. [receipt setState:MCDownloadStateNone];
  153. }
  154. }
  155. }];
  156. }
  157. - (void)saveAllDownloadReceipts {
  158. [NSKeyedArchiver archiveRootObject:self.allDownloadReceipts toFile:LocalReceiptsPath()];
  159. }
  160. - (void)dealloc {
  161. [self.session invalidateAndCancel];
  162. self.session = nil;
  163. [self.downloadQueue cancelAllOperations];
  164. }
  165. - (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field {
  166. if (value) {
  167. self.HTTPHeaders[field] = value;
  168. }
  169. else {
  170. [self.HTTPHeaders removeObjectForKey:field];
  171. }
  172. }
  173. - (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field {
  174. return self.HTTPHeaders[field];
  175. }
  176. - (void)setMaxConcurrentDownloads:(NSInteger)maxConcurrentDownloads {
  177. _downloadQueue.maxConcurrentOperationCount = maxConcurrentDownloads;
  178. }
  179. - (NSUInteger)currentDownloadCount {
  180. return _downloadQueue.operationCount;
  181. }
  182. - (NSInteger)maxConcurrentDownloads {
  183. return _downloadQueue.maxConcurrentOperationCount;
  184. }
  185. - (void)setOperationClass:(nullable Class)operationClass {
  186. if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(MCDownloaderOperationInterface)]) {
  187. _operationClass = operationClass;
  188. } else {
  189. _operationClass = [MCDownloadOperation class];
  190. }
  191. }
  192. - (nullable MCDownloadReceipt *)downloadDataWithURL:(nullable NSURL *)url
  193. progress:(nullable MCDownloaderProgressBlock)progressBlock
  194. completed:(nullable MCDownloaderCompletedBlock)completedBlock {
  195. __weak MCDownloader *wself = self;
  196. MCDownloadReceipt *receipt = [self downloadReceiptForURLString:url.absoluteString];
  197. if (receipt.state == MCDownloadStateCompleted) {
  198. dispatch_main_async_safe(^{
  199. [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadFinishNotification object:self];
  200. if (completedBlock) {
  201. completedBlock(receipt ,nil ,YES);
  202. }
  203. if (receipt.downloaderCompletedBlock) {
  204. receipt.downloaderCompletedBlock(receipt, nil, YES);
  205. }
  206. });
  207. return receipt;
  208. }
  209. return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^MCDownloadOperation *{
  210. __strong __typeof (wself) sself = wself;
  211. NSTimeInterval timeoutInterval = sself.downloadTimeout;
  212. if (timeoutInterval == 0.0) {
  213. timeoutInterval = 15.0;
  214. }
  215. NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
  216. MCDownloadReceipt *receipt = [sself downloadReceiptForURLString:url.absoluteString];
  217. if (receipt.totalBytesWritten > 0) {
  218. NSString *range = [NSString stringWithFormat:@"bytes=%zd-", receipt.totalBytesWritten];
  219. [request setValue:range forHTTPHeaderField:@"Range"];
  220. }
  221. request.HTTPShouldUsePipelining = YES;
  222. if (sself.headersFilter) {
  223. request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
  224. }
  225. else {
  226. request.allHTTPHeaderFields = sself.HTTPHeaders;
  227. }
  228. MCDownloadOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session];
  229. [sself.downloadQueue addOperation:operation];
  230. if (sself.downloadPrioritizaton == MCDownloadPrioritizationLIFO) {
  231. // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
  232. [sself.lastAddedOperation addDependency:operation];
  233. sself.lastAddedOperation = operation;
  234. }
  235. return operation;
  236. }];
  237. }
  238. - (MCDownloadReceipt *)downloadReceiptForURLString:(NSString *)URLString {
  239. if (URLString == nil) {
  240. return nil;
  241. }
  242. if (self.allDownloadReceipts[URLString]) {
  243. return self.allDownloadReceipts[URLString];
  244. }else {
  245. MCDownloadReceipt *receipt = [[MCDownloadReceipt alloc] initWithURLString:URLString downloadOperationCancelToken:nil downloaderProgressBlock:nil downloaderCompletedBlock:nil];
  246. self.allDownloadReceipts[URLString] = receipt;
  247. return receipt;
  248. }
  249. return nil;
  250. }
  251. - (nullable MCDownloadReceipt *)addProgressCallback:(MCDownloaderProgressBlock)progressBlock
  252. completedBlock:(MCDownloaderCompletedBlock)completedBlock
  253. forURL:(nullable NSURL *)url
  254. createCallback:(MCDownloadOperation *(^)())createCallback {
  255. // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
  256. if (url == nil) {
  257. if (completedBlock != nil) {
  258. completedBlock(nil, nil, NO);
  259. }
  260. return nil;
  261. }
  262. __block MCDownloadReceipt *token = nil;
  263. dispatch_barrier_sync(self.barrierQueue, ^{
  264. MCDownloadOperation *operation = self.URLOperations[url];
  265. if (!operation) {
  266. operation = createCallback();
  267. self.URLOperations[url] = operation;
  268. __weak MCDownloadOperation *woperation = operation;
  269. operation.completionBlock = ^{
  270. MCDownloadOperation *soperation = woperation;
  271. if (!soperation) return;
  272. if (self.URLOperations[url] == soperation) {
  273. [self.URLOperations removeObjectForKey:url];
  274. };
  275. };
  276. }
  277. id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
  278. if (!self.allDownloadReceipts[url.absoluteString]) {
  279. token = [[MCDownloadReceipt alloc] initWithURLString:url.absoluteString
  280. downloadOperationCancelToken:downloadOperationCancelToken
  281. downloaderProgressBlock:progressBlock
  282. downloaderCompletedBlock:completedBlock];
  283. self.allDownloadReceipts[url.absoluteString] = token;
  284. }else {
  285. token = self.allDownloadReceipts[url.absoluteString];
  286. if (!token.downloaderProgressBlock) {
  287. [token setDownloaderProgressBlock:progressBlock];
  288. }
  289. if (!token.downloaderCompletedBlock) {
  290. [token setDownloaderCompletedBlock:completedBlock];
  291. }
  292. if (!token.downloadOperationCancelToken) {
  293. [token setDownloadOperationCancelToken:downloadOperationCancelToken];
  294. }
  295. }
  296. });
  297. return token;
  298. }
  299. #pragma mark - Control Methods
  300. - (void)cancel:(nullable MCDownloadReceipt *)token completed:(nullable void (^)())completed {
  301. dispatch_barrier_async(self.barrierQueue, ^{
  302. MCDownloadOperation *operation = self.URLOperations[[NSURL URLWithString:token.url]];
  303. BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
  304. if (canceled) {
  305. [self.URLOperations removeObjectForKey:[NSURL URLWithString:token.url]];
  306. [token setState:MCDownloadStateNone];
  307. // [self.allDownloadReceipts removeObjectForKey:token.url];
  308. }
  309. dispatch_main_async_safe(^{
  310. if (completed) {
  311. completed();
  312. }
  313. });
  314. });
  315. }
  316. - (void)remove:(MCDownloadReceipt *)token completed:(nullable void (^)())completed{
  317. [token setState:MCDownloadStateNone];
  318. [self cancel:token completed:^{
  319. NSFileManager *fileManager = [NSFileManager defaultManager];
  320. [fileManager removeItemAtPath:token.filePath error:nil];
  321. dispatch_main_async_safe(^{
  322. if (completed) {
  323. completed();
  324. }
  325. });
  326. }];
  327. }
  328. - (void)setSuspended:(BOOL)suspended {
  329. (self.downloadQueue).suspended = suspended;
  330. }
  331. - (void)cancelAllDownloads {
  332. [self.downloadQueue cancelAllOperations];
  333. [self setAllStateToNone];
  334. [self saveAllDownloadReceipts];
  335. }
  336. - (void)removeAndClearAll {
  337. [self cancelAllDownloads];
  338. NSFileManager *fileManager = [NSFileManager defaultManager];
  339. [fileManager removeItemAtPath:cacheFolder() error:nil];
  340. clearCacheFolder();
  341. }
  342. #pragma mark Helper methods
  343. - (MCDownloadOperation *)operationWithTask:(NSURLSessionTask *)task {
  344. MCDownloadOperation *returnOperation = nil;
  345. for (MCDownloadOperation *operation in self.downloadQueue.operations) {
  346. if (operation.dataTask.taskIdentifier == task.taskIdentifier) {
  347. returnOperation = operation;
  348. break;
  349. }
  350. }
  351. return returnOperation;
  352. }
  353. #pragma mark NSURLSessionDataDelegate
  354. - (void)URLSession:(NSURLSession *)session
  355. dataTask:(NSURLSessionDataTask *)dataTask
  356. didReceiveResponse:(NSURLResponse *)response
  357. completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
  358. // Identify the operation that runs this task and pass it the delegate method
  359. MCDownloadOperation *dataOperation = [self operationWithTask:dataTask];
  360. [dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
  361. }
  362. - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
  363. // Identify the operation that runs this task and pass it the delegate method
  364. MCDownloadOperation *dataOperation = [self operationWithTask:dataTask];
  365. [dataOperation URLSession:session dataTask:dataTask didReceiveData:data];
  366. }
  367. - (void)URLSession:(NSURLSession *)session
  368. dataTask:(NSURLSessionDataTask *)dataTask
  369. willCacheResponse:(NSCachedURLResponse *)proposedResponse
  370. completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
  371. // Identify the operation that runs this task and pass it the delegate method
  372. MCDownloadOperation *dataOperation = [self operationWithTask:dataTask];
  373. [dataOperation URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
  374. }
  375. #pragma mark NSURLSessionTaskDelegate
  376. - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
  377. // Identify the operation that runs this task and pass it the delegate method
  378. MCDownloadOperation *dataOperation = [self operationWithTask:task];
  379. [dataOperation URLSession:session task:task didCompleteWithError:error];
  380. }
  381. @end