首页 > 技术文章 > NSURLSession 后台断点下载

liuyongfa 2019-02-19 14:49 原文

•只支持同时一个下载任务

•注释部分可能有理解的不对的地方

•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

 

推荐阅读