MCDownloadOperation.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. //
  2. // MCDownloadOperation.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 "MCDownloadOperation.h"
  27. NS_ASSUME_NONNULL_BEGIN
  28. NSString *const MCDownloadStartNotification = @"MCDownloadStartNotification";
  29. NSString *const MCDownloadReceiveResponseNotification = @"MCDownloadReceiveResponseNotification";
  30. NSString *const MCDownloadStopNotification = @"MCDownloadStopNotification";
  31. NSString *const MCDownloadFinishNotification = @"MCDownloadFinishNotification";
  32. static NSString *const kProgressCallbackKey = @"progress";
  33. static NSString *const kCompletedCallbackKey = @"completed";
  34. typedef NSMutableDictionary<NSString *, id> MCCallbacksDictionary;
  35. @interface MCDownloadOperation ()
  36. @property (strong, nonatomic, nonnull) NSMutableArray<MCCallbacksDictionary *> *callbackBlocks;
  37. @property (assign, nonatomic, getter = isExecuting) BOOL executing;
  38. @property (assign, nonatomic, getter = isFinished) BOOL finished;
  39. // 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
  40. // the task associated with this operation
  41. @property (weak, nonatomic, nullable) NSURLSession *unownedSession;
  42. // This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one
  43. @property (strong, nonatomic, nullable) NSURLSession *ownedSession;
  44. @property (strong, nonatomic, readwrite, nullable) NSURLSessionTask *dataTask;
  45. @property (strong, nonatomic, nullable) dispatch_queue_t barrierQueue;
  46. @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
  47. @property (assign, nonatomic) long long totalBytesWritten;
  48. @property (assign, nonatomic) long long totalBytesExpectedToWrite;
  49. @property (strong, nonatomic) MCDownloadReceipt *receipt;
  50. @end
  51. @implementation MCDownloadOperation
  52. {
  53. BOOL responseFromCached;
  54. }
  55. @synthesize executing = _executing;
  56. @synthesize finished = _finished;
  57. - (MCDownloadReceipt *)receipt {
  58. if (_receipt == nil) {
  59. _receipt = [[MCDownloader sharedDownloader] downloadReceiptForURLString:self.request.URL.absoluteString];
  60. }
  61. return _receipt;
  62. }
  63. - (nonnull instancetype)init {
  64. return [self initWithRequest:nil inSession:nil];
  65. }
  66. - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request inSession:(nullable NSURLSession *)session {
  67. if ((self = [super init])) {
  68. _request = [request copy];
  69. _callbackBlocks = [NSMutableArray new];
  70. _executing = NO;
  71. _finished = NO;
  72. _expectedSize = 0;
  73. _unownedSession = session;
  74. responseFromCached = YES; // Initially wrong until `- URLSession:dataTask:willCacheResponse:completionHandler: is called or not called
  75. _barrierQueue = dispatch_queue_create("com.machao.MCDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
  76. [self.receipt setState:MCDownloadStateWillResume];
  77. }
  78. return self;
  79. }
  80. - (void)dealloc {
  81. }
  82. - (nullable id)addHandlersForProgress:(nullable MCDownloaderProgressBlock)progressBlock
  83. completed:(nullable MCDownloaderCompletedBlock)completedBlock {
  84. MCCallbacksDictionary *callbacks = [NSMutableDictionary new];
  85. if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
  86. if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
  87. dispatch_barrier_async(self.barrierQueue, ^{
  88. [self.callbackBlocks addObject:callbacks];
  89. });
  90. return callbacks;
  91. }
  92. - (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
  93. __block NSMutableArray<id> *callbacks = nil;
  94. dispatch_sync(self.barrierQueue, ^{
  95. // We need to remove [NSNull null] because there might not always be a progress block for each callback
  96. callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
  97. [callbacks removeObjectIdenticalTo:[NSNull null]];
  98. });
  99. return [callbacks copy]; // strip mutability here
  100. }
  101. - (BOOL)cancel:(nullable id)token {
  102. __block BOOL shouldCancel = NO;
  103. dispatch_barrier_sync(self.barrierQueue, ^{
  104. [self.callbackBlocks removeAllObjects];
  105. if (self.callbackBlocks.count == 0) {
  106. shouldCancel = YES;
  107. }
  108. });
  109. if (shouldCancel) {
  110. [self cancel];
  111. }
  112. return shouldCancel;
  113. }
  114. - (void)start {
  115. @synchronized (self) {
  116. if (self.isCancelled) {
  117. self.finished = YES;
  118. [self reset];
  119. return;
  120. }
  121. #if TARGET_OS_IOS
  122. Class UIApplicationClass = NSClassFromString(@"UIApplication");
  123. BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
  124. if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
  125. __weak __typeof__ (self) wself = self;
  126. UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
  127. self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
  128. __strong __typeof (wself) sself = wself;
  129. if (sself) {
  130. [sself cancel];
  131. [app endBackgroundTask:sself.backgroundTaskId];
  132. sself.backgroundTaskId = UIBackgroundTaskInvalid;
  133. }
  134. }];
  135. }
  136. #endif
  137. NSURLSession *session = self.unownedSession;
  138. if (!self.unownedSession) {
  139. NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
  140. sessionConfig.timeoutIntervalForRequest = 15;
  141. /**
  142. * Create the session for this task
  143. * We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
  144. * method calls and completion handler calls.
  145. */
  146. self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
  147. delegate:self
  148. delegateQueue:nil];
  149. session = self.ownedSession;
  150. }
  151. self.dataTask = [session dataTaskWithRequest:self.request];
  152. self.executing = YES;
  153. }
  154. [self.dataTask resume];
  155. if (self.dataTask) {
  156. for (MCDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
  157. progressBlock(0, NSURLResponseUnknownLength, 0, self.request.URL);
  158. }
  159. [self.receipt setState:MCDownloadStateDownloading];
  160. dispatch_async(dispatch_get_main_queue(), ^{
  161. [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadStartNotification object:self];
  162. });
  163. } else {
  164. [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
  165. }
  166. #if TARGET_OS_IOS
  167. Class UIApplicationClass = NSClassFromString(@"UIApplication");
  168. if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
  169. return;
  170. }
  171. if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
  172. UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
  173. [app endBackgroundTask:self.backgroundTaskId];
  174. self.backgroundTaskId = UIBackgroundTaskInvalid;
  175. }
  176. #endif
  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. [self.receipt setState:MCDownloadStateNone];
  189. dispatch_async(dispatch_get_main_queue(), ^{
  190. [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadStopNotification object:self];
  191. });
  192. // As we cancelled the connection, 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. dispatch_barrier_async(self.barrierQueue, ^{
  206. [self.callbackBlocks removeAllObjects];
  207. });
  208. self.dataTask = nil;
  209. if (self.ownedSession) {
  210. [self.ownedSession invalidateAndCancel];
  211. self.ownedSession = nil;
  212. }
  213. }
  214. - (void)setFinished:(BOOL)finished {
  215. [self willChangeValueForKey:@"isFinished"];
  216. _finished = finished;
  217. [self didChangeValueForKey:@"isFinished"];
  218. }
  219. - (void)setExecuting:(BOOL)executing {
  220. [self willChangeValueForKey:@"isExecuting"];
  221. _executing = executing;
  222. [self didChangeValueForKey:@"isExecuting"];
  223. }
  224. - (BOOL)isConcurrent {
  225. return YES;
  226. }
  227. - (void)URLSession:(NSURLSession *)session
  228. dataTask:(NSURLSessionDataTask *)dataTask
  229. didReceiveResponse:(NSURLResponse *)response
  230. completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
  231. //'304 Not Modified' is an exceptional one
  232. if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode < 400 && ((NSHTTPURLResponse *)response).statusCode != 304)) {
  233. NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
  234. MCDownloadReceipt *receipt = [[MCDownloader sharedDownloader] downloadReceiptForURLString:self.request.URL.absoluteString];
  235. [receipt setTotalBytesExpectedToWrite:expected + receipt.totalBytesWritten];
  236. receipt.date = [NSDate date];
  237. self.expectedSize = expected;
  238. for (MCDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
  239. progressBlock(0, expected, 0,self.request.URL);
  240. }
  241. self.response = response;
  242. dispatch_async(dispatch_get_main_queue(), ^{
  243. [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadReceiveResponseNotification object:self];
  244. });
  245. }else if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode == 416)) {
  246. [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadFinishNotification object:self];
  247. [self callCompletionBlocksWithFileURL:[NSURL fileURLWithPath:self.receipt.filePath] data:[NSData dataWithContentsOfFile:self.receipt.filePath] error:nil finished:YES];
  248. [self done];
  249. }
  250. else {
  251. NSUInteger code = ((NSHTTPURLResponse *)response).statusCode;
  252. //This is the case when server returns '304 Not Modified'. It means that remote image is not changed.
  253. //In case of 304 we need just cancel the operation and return cached image from the cache.
  254. if (code == 304) {
  255. [self cancelInternal];
  256. } else {
  257. [self.dataTask cancel];
  258. [self.receipt setState:MCDownloadStateNone];
  259. }
  260. dispatch_async(dispatch_get_main_queue(), ^{
  261. [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadStopNotification object:self];
  262. });
  263. [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:((NSHTTPURLResponse *)response).statusCode userInfo:nil]];
  264. [self.receipt setState:MCDownloadStateNone];
  265. [self done];
  266. }
  267. if (completionHandler) {
  268. completionHandler(NSURLSessionResponseAllow);
  269. }
  270. }
  271. - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
  272. __block NSError *error = nil;
  273. MCDownloadReceipt *receipt = [[MCDownloader sharedDownloader] downloadReceiptForURLString:self.request.URL.absoluteString];
  274. // Speed
  275. receipt.totalRead += data.length;
  276. NSDate *currentDate = [NSDate date];
  277. if ([currentDate timeIntervalSinceDate:receipt.date] >= 1) {
  278. double time = [currentDate timeIntervalSinceDate:receipt.date];
  279. long long speed = receipt.totalRead/time;
  280. receipt.speed = [self formatByteCount:speed];
  281. receipt.totalRead = 0.0;
  282. receipt.date = currentDate;
  283. }
  284. // Write Data
  285. NSInputStream *inputStream = [[NSInputStream alloc] initWithData:data];
  286. NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:[NSURL fileURLWithPath:receipt.filePath] append:YES];
  287. [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
  288. [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
  289. [inputStream open];
  290. [outputStream open];
  291. while ([inputStream hasBytesAvailable] && [outputStream hasSpaceAvailable]) {
  292. uint8_t buffer[1024];
  293. NSInteger bytesRead = [inputStream read:buffer maxLength:1024];
  294. if (inputStream.streamError || bytesRead < 0) {
  295. error = inputStream.streamError;
  296. break;
  297. }
  298. NSInteger bytesWritten = [outputStream write:buffer maxLength:(NSUInteger)bytesRead];
  299. if (outputStream.streamError || bytesWritten < 0) {
  300. error = outputStream.streamError;
  301. break;
  302. }
  303. if (bytesRead == 0 && bytesWritten == 0) {
  304. break;
  305. }
  306. }
  307. [outputStream close];
  308. [inputStream close];
  309. receipt.progress.totalUnitCount = receipt.totalBytesExpectedToWrite;
  310. receipt.progress.completedUnitCount = receipt.totalBytesWritten;
  311. dispatch_main_async_safe(^{
  312. for (MCDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
  313. progressBlock(receipt.progress.completedUnitCount, receipt.progress.totalUnitCount, receipt.speed.integerValue, self.request.URL);
  314. }
  315. if (self.receipt.downloaderProgressBlock) {
  316. self.receipt.downloaderProgressBlock(receipt.progress.completedUnitCount, receipt.progress.totalUnitCount, receipt.speed.integerValue, self.request.URL);
  317. }
  318. });
  319. }
  320. - (void)URLSession:(NSURLSession *)session
  321. dataTask:(NSURLSessionDataTask *)dataTask
  322. willCacheResponse:(NSCachedURLResponse *)proposedResponse
  323. completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
  324. responseFromCached = NO; // If this method is called, it means the response wasn't read from cache
  325. NSCachedURLResponse *cachedResponse = proposedResponse;
  326. if (completionHandler) {
  327. completionHandler(cachedResponse);
  328. }
  329. }
  330. #pragma mark NSURLSessionTaskDelegate
  331. - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error {
  332. @synchronized(self) {
  333. self.dataTask = nil;
  334. dispatch_async(dispatch_get_main_queue(), ^{
  335. [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadStopNotification object:self];
  336. if (!error) {
  337. [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadFinishNotification object:self];
  338. }
  339. });
  340. }
  341. if (error) {
  342. [self callCompletionBlocksWithError:error];
  343. } else {
  344. MCDownloadReceipt *receipt = self.receipt;
  345. [receipt setState:MCDownloadStateCompleted];
  346. if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
  347. [self callCompletionBlocksWithFileURL:[NSURL fileURLWithPath:receipt.filePath] data:[NSData dataWithContentsOfFile:receipt.filePath] error:nil finished:YES];
  348. }
  349. dispatch_main_async_safe(^{
  350. if (self.receipt.downloaderCompletedBlock) {
  351. self.receipt.downloaderCompletedBlock(receipt, nil, YES);
  352. }
  353. });
  354. }
  355. [self done];
  356. }
  357. - (BOOL)shouldContinueWhenAppEntersBackground {
  358. return YES;
  359. }
  360. - (void)callCompletionBlocksWithError:(nullable NSError *)error {
  361. [self callCompletionBlocksWithFileURL:nil data:nil error:error finished:YES];
  362. }
  363. - (void)callCompletionBlocksWithFileURL:(nullable NSURL *)fileURL
  364. data:(nullable NSData *)data
  365. error:(nullable NSError *)error
  366. finished:(BOOL)finished {
  367. if (error) {
  368. [self.receipt setState:MCDownloadStateFailed];
  369. }else {
  370. [self.receipt setState:MCDownloadStateCompleted];
  371. }
  372. NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
  373. dispatch_main_async_safe(^{
  374. for (MCDownloaderCompletedBlock completedBlock in completionBlocks) {
  375. completedBlock(self.receipt, error, finished);
  376. }
  377. if (self.receipt.downloaderCompletedBlock) {
  378. self.receipt.downloaderCompletedBlock(self.receipt, error, YES);
  379. }
  380. });
  381. }
  382. - (NSString*)formatByteCount:(long long)size
  383. {
  384. return [NSByteCountFormatter stringFromByteCount:size countStyle:NSByteCountFormatterCountStyleFile];
  385. }
  386. @end
  387. NS_ASSUME_NONNULL_END