NSURLSession 后台断点下载

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

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

?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

原文地址:https://www.cnblogs.com/liuyongfa/p/10401026.html

时间: 2024-10-10 07:59:05

NSURLSession 后台断点下载的相关文章

NSURLSession实现断点下载

NSURLSession实现断点下载 @interface HMViewController () <NSURLSessionDownloadDelegate, NSURLSessionDataDelegate]]> @property (weak, nonatomic) IBOutlet UIProgressView *progressView; - (IBAction)download:(UIButton *)sender; @property (nonatomic, strong) NS

NSURLSession的断点下载

#import "ViewController.h" @interface ViewController () <NSURLSessionDownloadDelegate, NSURLSessionDataDelegate> @property (weak, nonatomic) IBOutlet UIProgressView *progressView; - (IBAction)download:(UIButton *)sender; @property (nonatom

后台多任务多线程断点下载

忘了上图: 多线程断点下载其实不是很难,主要就是三个方面: 1.根据文件的大小和下载线程的数量,确定每个下载线程要下载的分割文件的大小: 2.记录每个下载线程已经下载完成的进度: 3.将每个线程下载的分割的文件合并到一个文件中. 那么怎么将远程的一个文件分割成三部分来下载呢?其实在HTTP协议中,有一个Range字段,用于客户端到服务器端的请求,可通过该字段指定下载文件的某一段大小,及其单位,格式为:Range: bytes x - y,eg:Range: bytes=0-100 下载从第0 -

iOS- 利用AFNetworking(AFN) - 实现文件断点下载

iOS- 利用AFNetworking(AFN) - 实现文件断点下载 官方建议AFN的使用方法 1. 定义一个全局的AFHttpClient:包含有 1> baseURL 2> 请求 3> 操作队列 NSOperationQueue 2. 由AFHTTPRequestOperation负责所有的网络操作请求 0.导入框架准备工作 •1. 将框架程序拖拽进项目 •2.  添加iOS框架引用 –SystemConfiguration.framework –MobileCoreService

Android的断点下载详细分析二

由于一篇blog写不完,这里是接着上一篇blog的. 写完了MVC中的View,写着我们需要考虑Control层了,他的任务是在后台利用多线程实现断点下载. 先看源码: public class FileDownloader { /* TAG,便于调试 */ private static final String TAG = "FileDownloader"; /* 上下文 */ private Context context; /* 用于对数据库的操作 */ private File

Android/java http多线程断点下载(附源码)

先看下项目结构: http多线程断点下载涉及到 数据库,多线程和http请求等几个模块,东西不是很多,想弄清楚也不是很困难,接下来我和大家分享下我的做法. 一.先看MainActivity.java 成员变量,主要是一些下载过程的变量和handler private String path = "http://192.168.1.3:8080/wanmei/yama.apk"; private String sdcardPath; private int threadNum = 5;

iOS开发网络请求——大文件的多线程断点下载

iOS开发中网络请求技术已经是移动app必备技术,而网络中文件传输就是其中重点了.网络文件传输对移动客户端而言主要分为文件的上传和下载.作为开发者从技术角度会将文件分为小文件和大文件.小文件因为文件大小比较小导致传输所需时间少传输就快,因此不太容易影响用户体验,可用的技术就多.而大文件因为文件大小比较大导致传输时间长,因此就需要考虑到各种用户体验,比如避免在上传下载文件过程中阻塞主线程影响用户体验,就需要使用到多线程技术:为了给用户友好的进度提示,因此又需要开发中跟踪数据上传和下载数据的变化:为

ios开发网络学习十一:NSURLSessionDataTask离线断点下载(断点续传)

#import "ViewController.h" #define FileName @"121212.mp4" @interface ViewController ()<NSURLSessionDataDelegate> @property (weak, nonatomic) IBOutlet UIProgressView *proessView; /** 接受响应体信息 */ @property (nonatomic, strong) NSFile

iOS网络-NSURLSessionDataTask大文件离线断点下载

什么叫离线断点下载,就是用户下载中关闭程序重新打开可以继续下载 代码实现如下: #import "ViewController.h" @interface ViewController ()<NSURLSessionDataDelegate> //输出流 @property (nonatomic, strong) NSOutputStream *stream ; //Task对象 @property (nonatomic, strong) NSURLSessionDataT