NSURLSession 后台断点下载
2019-02-19 14:49 法子 阅读(502) 评论(0) 编辑 收藏 举报•只支持同时一个下载任务
•注释部分可能有理解的不对的地方
•GitHub地址:https://github.com/liuyongfa/LYFBackgroundDownloadDemo.git
NSURLSession可以执行长时间的后台下载任务。进入后台后,下载任务可以一直执行。被杀死后,再次进入App会根据NSURLSessionConfiguration的identifier继续下载。下载成功后,可以调用LocalNotification做通知。
AppDelegate.m
#import "AppDelegate.h" #import "LYFBackgroundDownload.h" @interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 后台下载任务在App被杀死后下次进入App仍然有效,所以应该有手机系统管理介入,identifier要拼接上bundlId防止和其他App混淆 [[LYFBackgroundDownload sharedManager] registerDownloadTaskWithIdentifier:[NSString stringWithFormat:@"%@.%@", [NSBundle mainBundle].bundleIdentifier, @"LYFBackgroundDownload"]]; return YES; } //如果不实现该协议,后台一样可以下载,但是不会调用NSURLSessionDownloadDelegate协议,要等到重新回到前台,那些协议才会一股脑被调用。 - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler { if ([identifier isEqualToString:[[LYFBackgroundDownload sharedManager] downloadTaskIdentifier]]) { [[LYFBackgroundDownload sharedManager] setCompletionHandler:completionHandler]; } //在这里调用completionHandler,会使之后的NSURLSessionDownloadDelegate协议只走到didFinishDownloadingToURL,之后的didCompleteWithError,URLSessionDidFinishEventsForBackgroundURLSession不被调用。 // completionHandler(); } - (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { [[UIApplication sharedApplication] cancelAllLocalNotifications]; if ([[LYFBackgroundDownload sharedManager] isDownloadLocalNotification: notification]) {//在后台点击了弹出的横条通知,或者在前台收到了下载完成通知 UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"下载通知" message:notification.alertBody preferredStyle:UIAlertControllerStyleAlert]; alert.title = @"下载通知"; alert.message = notification.alertBody; UIAlertAction *action = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]; [alert addAction:action]; [self.window.rootViewController presentViewController:alert animated:YES completion:nil]; } } @end
LYFBackgroundDownload.h
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN typedef void(^CompletionHandlerType)(void); @class LYFBackgroundDownload; @protocol LYFBackgroundDownloadDelegate <NSObject> @optional - (void)LYFBackgroundDownloadProgress:(CGFloat)progress; - (void)LYFBackgroundDownloadDidFinishDownloadingToURL:(NSURL *)location; @end @interface LYFBackgroundDownload : NSObject + (id)sharedManager; - (void)registerDownloadTaskWithIdentifier:(NSString *)identifier; - (void)setDelegate:(id <LYFBackgroundDownloadDelegate>)delegate; - (void)beginDownloadWithUrl: (NSString *)downloadURLString; - (void)pauseDownload; - (void)continueDownload; - (NSString *)downloadTaskIdentifier; - (void)setCompletionHandler:(CompletionHandlerType )completionHandler; - (BOOL)isDownloadLocalNotification:(UILocalNotification *)localNotification; @end NS_ASSUME_NONNULL_END
LYFBackgroundDownload.m
#import <UIKit/UIKit.h> #import "LYFBackgroundDownload.h" #import "AppDelegate.h" @interface LYFBackgroundDownload() <NSURLSessionDownloadDelegate> @property (strong, nonatomic) UILocalNotification *localNotification; @property (nonatomic, strong) NSOperationQueue *operationQueue; @property (strong, nonatomic) NSURLSession *backgroundSession; @property (strong, nonatomic) NSString *downloadTaskIdentifier; @property (strong, nonatomic) NSURLSessionDownloadTask *downloadTask; @property (strong, nonatomic) CompletionHandlerType completionHandler; @property (weak, nonatomic) id <LYFBackgroundDownloadDelegate> delegate; @end @implementation LYFBackgroundDownload - (instancetype)init { self = [super init]; if (self != nil) { [self initLYFBackgroundDownload]; } return self; } + (id)sharedManager { static LYFBackgroundDownload *staticInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ staticInstance = [[self alloc] init]; }); return staticInstance; } - (void)registerDownloadTaskWithIdentifier: (NSString *)identifier { _downloadTaskIdentifier = identifier; _backgroundSession = [self backgroundURLSession]; _localNotification.userInfo = @{_downloadTaskIdentifier: _downloadTaskIdentifier}; } - (void)setDelegate:(id<LYFBackgroundDownloadDelegate>)delegate { _delegate = delegate; } - (void)setCompletionHandler: (CompletionHandlerType )completionHandler { _completionHandler = completionHandler; } - (NSString *)downloadTaskIdentifier { return _downloadTaskIdentifier; } - (BOOL)isDownloadLocalNotification: (UILocalNotification *)localNotification { return [_localNotification.userInfo[_downloadTaskIdentifier] isEqualToString:localNotification.userInfo[_downloadTaskIdentifier]]; } - (void)initLYFBackgroundDownload { [self registerUserNotification]; [self initLocalNotification]; } - (NSURLSession *)backgroundSession { if (_backgroundSession) { return _backgroundSession; } return [self backgroundURLSession]; } - (NSURLSession *)backgroundURLSession { static NSURLSession *session = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSURLSessionConfiguration* sessionConfig = nil; sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:self.downloadTaskIdentifier]; self.operationQueue = [[NSOperationQueue alloc] init]; //队列可同时执行的任务数为1,即串行 self.operationQueue.maxConcurrentOperationCount = 1; //允许蜂窝网络下载 sessionConfig.allowsCellularAccess = YES; __weak __typeof(self) weakSelf = self; //iOS9之前很多框架的delegate都是强引用:@property (nullable, readonly, retain) id <NSURLSessionDelegate> delegate session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:weakSelf delegateQueue:self.operationQueue]; }); return session; } #pragma mark - LYFBackgroundDownload - (void)beginDownloadWithUrl:(NSString *)downloadURLString { __weak __typeof(self) weakSelf = self; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); //如果backgroundSession已经有downloadTask,就继续,如果没有,就添加。保证backgroundSession最多只有一个downloadTask [self.backgroundSession getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { // for (NSURLSessionDataTask *task in dataTasks) { // } // // for (NSURLSessionUploadTask *uploadTask in uploadTasks) { // } NSAssert(downloadTasks.count <= 1, @"后台下载任务超过1个"); if (downloadTasks.count == 0) { NSURL *downloadURL = [NSURL URLWithString:downloadURLString]; NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL]; weakSelf.downloadTask = [self.backgroundSession downloadTaskWithRequest:request]; } else { weakSelf.downloadTask= downloadTasks[0]; } dispatch_semaphore_signal(semaphore); }]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); [self.downloadTask resume]; } - (void)pauseDownload { [self.downloadTask suspend]; } - (void)continueDownload { [self.downloadTask resume]; } #pragma mark - NSURLSessionDownloadDelegate - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { NSLog(@"downloadTask:%lu didFinishDownloadingToURL:%@", (unsigned long)downloadTask.taskIdentifier, location); // 用 NSFileManager 将文件复制到应用的存储中 NSString *locationString = [location path]; NSString *finalLocation = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory , NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:[NSString stringWithFormat:@"%lufile",(unsigned long)downloadTask.taskIdentifier]]; NSError *error; [[NSFileManager defaultManager] moveItemAtPath:locationString toPath:finalLocation error:&error]; NSLog(@"finalLocation = %@", finalLocation); __weak __typeof(self) weakSlef = self; dispatch_async(dispatch_get_main_queue(), ^{ if ([weakSlef.delegate respondsToSelector:@selector(LYFBackgroundDownloadDidFinishDownloadingToURL:)]) { [weakSlef.delegate LYFBackgroundDownloadDidFinishDownloadingToURL:[NSURL fileURLWithPath:finalLocation]]; } }); } //downloadTaskWithResumeData会触发调用该方法 - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { NSLog(@"fileOffset:%lld expectedTotalBytes:%lld",fileOffset,expectedTotalBytes); } //进入后台后将不再触发 - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { CGFloat progress = (CGFloat)totalBytesWritten / totalBytesExpectedToWrite; NSLog(@"downloadTask:%lu percent:%.2f%%",(unsigned long)downloadTask.taskIdentifier,progress * 100); __weak __typeof(self) weakSlef = self; dispatch_async(dispatch_get_main_queue(), ^{ if ([weakSlef.delegate respondsToSelector:@selector(LYFBackgroundDownloadProgress:)]) { [weakSlef.delegate LYFBackgroundDownloadProgress:progress]; } }); } - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { NSLog(@"Background URL session %@ finished events.\n", session); NSString *identifier = session.configuration.identifier; if ([identifier isEqualToString:_downloadTaskIdentifier]) { // 调用在 -application:handleEventsForBackgroundURLSession: 中保存的 handler if (_completionHandler) { NSLog(@"Calling completion handler for session %@", identifier); _completionHandler(); } } } /* * 该方法下载成功和失败都会回调,只是失败的是error是有值的, * 在下载失败时,error的userinfo属性可以通过NSURLSessionDownloadTaskResumeData * 这个key来取到resumeData(和上面的resumeData是一样的),再通过resumeData恢复下载 */ //下载完成 //•函数里可以做: //1.发出下载完成的本地通知,如果在后台就可以发本地通知,在前台不可以显示本地通知,可以由didReceiveLocalNotification里面来处理本地通知 //2.因为在后台是不会更新下载进度的,所有这个函数里要处理把进度改为100% //•断点下载处理: //如果app退出(发现Xcode重新编译不算app退出),下次进入app会触发该方法,error不为空,可以进行断点下载工作 //这里app退出,重新进入调用该函数也说明了NSURLSession的多任务是由系统管理,所以NSURLSessionConfiguration的identifier要包含bundle id,以防止和其他app混淆。 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { NSLog(@"didCompleteWithError"); if ([session.configuration.identifier isEqualToString:_downloadTaskIdentifier]) { if (error) { if ([error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData]) { NSData *resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData]; //通过之前保存的resumeData,获取断点的NSURLSessionTask,调用resume恢复下载 NSLog(@"self.resumeData.length = %ld, %@", resumeData.length, session.configuration.identifier); // self.downloadTask = [self.backgroundSession downloadTaskWithCorrectResumeData:self.resumeData]; self.downloadTask = [self.backgroundSession downloadTaskWithResumeData: resumeData]; [self.downloadTask resume]; } } else { dispatch_async(dispatch_get_main_queue(), ^{ [self sendLocalNotification]; //更新进度条 if ([self.delegate respondsToSelector:@selector(LYFBackgroundDownloadProgress:)]) { [self.delegate LYFBackgroundDownloadProgress:1]; } }); } } } #pragma mark - Local Notification - (void)registerUserNotification { if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerUserNotificationSettings:)]) { UIUserNotificationType type = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound; UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:type categories:nil]; [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; } } - (void)initLocalNotification { self.localNotification = [[UILocalNotification alloc] init]; self.localNotification.fireDate = [[NSDate date] dateByAddingTimeInterval:5]; self.localNotification.alertAction = nil; self.localNotification.soundName = UILocalNotificationDefaultSoundName; self.localNotification.alertBody = @"下载完成了!"; // self.localNotification.applicationIconBadgeNumber = 1; // self.localNotification.repeatInterval = 0; } - (void)sendLocalNotification { [[UIApplication sharedApplication] scheduleLocalNotification:self.localNotification]; } @end
ViewController.m
#import "ViewController.h" #import "LYFBackgroundDownload.h" @interface ViewController () <LYFBackgroundDownloadDelegate> @property (strong, nonatomic) IBOutlet UIProgressView *downloadProgress; @property (weak, nonatomic) IBOutlet UILabel *progressLabel; @property (weak, nonatomic) IBOutlet UIImageView *imageView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. [[LYFBackgroundDownload sharedManager] setDelegate: self]; } - (void)updateDownloadProgress:(CGFloat)progress { self.progressLabel.text = [NSString stringWithFormat:@"%.2f%%",progress * 100]; self.downloadProgress.progress = progress; } #pragma mark Method - (IBAction)download:(id)sender { // [[LYFBackgroundDownload sharedManager] beginDownloadWithUrl:@"https://www.baidu.com/img/bdlogo.png"]; [[LYFBackgroundDownload sharedManager] beginDownloadWithUrl:@"https://www.apple.com/105/media/cn/iphone-x/2017/01df5b43-28e4-4848-bf20-490c34a926a7/films/feature/iphone-x-feature-cn-20170912_1280x720h.mp4"]; } - (IBAction)pauseDownlaod:(id)sender { [[LYFBackgroundDownload sharedManager] pauseDownload]; } - (IBAction)continueDownlaod:(id)sender { [[LYFBackgroundDownload sharedManager] continueDownload]; } #pragma mark LYFBackgroundDownloadDelegate - (void)LYFBackgroundDownloadProgress:(CGFloat)progress { [self updateDownloadProgress:progress]; } - (void)LYFBackgroundDownloadDidFinishDownloadingToURL:(NSURL *)location { self.imageView.image = [UIImage imageWithContentsOfFile:[location path]]; } @end