以content-available为主,对payload的设置对push影响进行测试
case1 不设置content-available,应用只能进入前台后才能处理push消息,无异于iOS7之前的Noraml Push.
case2 设置content-availabel,同时设置其他项,如下:
aps{
alert:"My first push notification",
badge: 1,
sound: "default",
content-available: 1
}
有Remote Notification的通知提示。
case3 设置payload,发送silent Remote Notification,如下:
aps{
alert:"My first push notification",
content-available: 1
}
没有来自系统的通知提示。
以case3为基础进行Remote Notification的能力测试与验证,通过Charles模拟不同的网络环境,控制后台处理时间
case1 应用处于前台时,发送Silent Remote Notification.此时会当作一般的前台任务处理。一旦应用退到后台,或者设备锁屏,任务马上结束。case2 应用处于后台时,发送Silent Remote Notification.应用在后台被唤醒,开始处理下载任务。30s后,无论下载是否结束,应用被挂起。 如果没有调用fetchCompletionHandler通知系统的话,超过了30s,系统会终止应用。 期间,锁屏,任务马上结束。 case3 应用Cransh等Not-Running的状态,发送Silent Remote Notification.应用在后台启动,开始处理下载任务。结果同上case4 应用Force-quit后,发送Silent Remote Notification.应用不会在后台被重新启动。
最后,关于Remote Notification做一下总结:Remote Notification会唤醒后台应用(或在后台重启应用),并最多分配30s的时间处理notification,如下载数据,更新UI等,其间任务的执行并不受系统休眠等影响。如果超过30s的时间,系统会将应用挂起。fetchCompletionHandler是用来通知系统,本地处理已经结束,系统可以更新界面快照并将应用挂起。但是,若系统没有及时收到此通知的话,应用可能会被kill掉。所以,仍然强调的是,Remote Notification多用于:IM,Email更新,RSS内容同步更新等,如果内容更新涉及下载较大文件的话,需要结合Background Transfer Service
Background Transfer Service是iOS7系统以来,作为多任务变革大招中最后一式,既可以作为一个很好的方式独立应用于项目中,也可以与Background Task、Background Fetch、Remote Notification进行完美结合,更好地服务于你的需求。
后台传输服务是基于Background Session来实现的,我们通过backgroundSessionConfiguration创建的session,实际是系统另起了一个后台进程来处理传输任务。所以,当应用退到后台或者是应用崩溃等终止运行了,并不影响后台传输进程正常运行。
当基于此background session的所有任务都完成时(成功or失败),系统都会唤醒后台应用(如果应用是退到后台)或者在后台重启应用(如果应用终止运行了)。并且会触发Application的代理方法:application:handleEventsForBackgroundURLSession:completionHandler,系统通过此代理方法,告知我们:
当与此session相关的所有消息都发送完毕后,应用会收到URLSessionDidFinishEventsForBackgroundURLSession消息。我们需要在方法中完成一些诸如界面更新等的工作,并且调用上述completionHanlder,通知系统应用可以进入休眠状态。
接下来,我们直接根据一个Demo APP,来了解如何使用NSURLSession API实现后台传输服务,包括以下几步:
下面是我们Demo App的截图,主要结合NSURLSession和NSURLSessionDownlaodTask实现简单的支持后台下载的文件下载管理。
为了实现简单的文件下载管理功能,我们需要对下载信息进行持久化,这里通过Sqlite来实现。定义下载信息数据结构如下:
@interface FileDownloadInfo : NSObject
@property (nonatomic, strong) NSString *fileTitle;
@property (nonatomic, strong) NSString *downloadSource;
@property (nonatomic, strong) NSURLSessionDownloadTask *downloadTask;
@property (nonatomic, strong) NSData *taskResumeData;
@property (nonatomic) double downloadProgress;
@property (nonatomic) HTFileTransferState status;
@property (nonatomic) unsigned long taskIdentifier;
@end
前期工作准备好后,我们主要实现以下步骤:
获取文件下载进度,并持久化下载进度信息
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
处理文件下载结束的相关工作:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location;
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error;
实现AppDelegate的代理方法application:handleEventsForBackgroundURLSession:completionHandler:,
下面我们进入关键代码的演示与分析
//创建后台session对象
-(NSURLSession *)backgroundSession
{
static NSURLSession * session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.BGTransferDemo"];
sessionConfiguration.HTTPMaximumConnectionsPerHost = 5;
session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
});
return session;
}
// 初始化下载任务列表,需要与DB里面的下载对象做merge
-(void)initializeFileDownloadDataArray{
self.arrFileDownloadData = [[NSMutableArray alloc] init];
[self.arrFileDownloadData addObject:[[FileDownloadInfo alloc] initWithFileTitle:@"iOS Programming Guide" andDownloadSource:@"https://developer.apple.com/library/ios/documentation/iphone/conceptual/iphoneosprogrammingguide/iphoneappprogrammingguide.pdf"]];
[self.arrFileDownloadData addObject:[[FileDownloadInfo alloc] initWithFileTitle:@"xmind-7" andDownloadSource:@"http://xmind-dl.oss-cn-qingdao.aliyuncs.com/xmind-7-update1-macosx.dmg"]];
NSArray<FileDownloadInfo *> *itemsFromDB = [_databaseManager allDownloadItems];
for (int i = 0; i < _arrFileDownloadData.count; i++) {
NSUInteger foundDownloadItemIndex = [itemsFromDB indexOfObjectPassingTest:^BOOL(FileDownloadInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
FileDownloadInfo * item = _arrFileDownloadData[i];
if ([obj.fileTitle isEqualToString:item.fileTitle]) {
return YES;
}
return NO;
}];
if (foundDownloadItemIndex != NSNotFound) {
FileDownloadInfo * itemFromDB = [itemsFromDB objectAtIndex:foundDownloadItemIndex];
_arrFileDownloadData[i].downloadProgress = itemFromDB.downloadProgress;
_arrFileDownloadData[i].taskResumeData = itemFromDB.taskResumeData;
_arrFileDownloadData[i].status = itemFromDB.status;
_arrFileDownloadData[i].taskIdentifier = itemFromDB.taskIdentifier;
if (itemFromDB.status == HTFileTransferStateTransfering) {
_arrFileDownloadData[i].isDownloading = YES;
}
if (itemFromDB.status == HTFileTransferStateDone) {
_arrFileDownloadData[i].downloadComplete = YES;
}
}
}
}
省略了构建tableview的细节,直接进入下一步
//启动所有下载任务,并更新数据库
- (IBAction)startAllDownloads:(id)sender {
// Access all FileDownloadInfo objects using a loop.
for (int i=0; i<[self.arrFileDownloadData count]; i++) {
FileDownloadInfo *fdi = [self.arrFileDownloadData objectAtIndex:i];
// Check if a file is already being downloaded or not.
if (fdi.downloadComplete) {
continue;
}
if (!fdi.isDownloading) {
// Check if should create a new download task using a URL, or using resume data.
if (fdi.taskIdentifier == -1) {
fdi.downloadTask = [self.session downloadTaskWithURL:[NSURL URLWithString:fdi.downloadSource]];
}
else{
fdi.downloadTask = [self.session downloadTaskWithResumeData:fdi.taskResumeData];
}
// Keep the new taskIdentifier.
fdi.taskIdentifier = fdi.downloadTask.taskIdentifier;
fdi.status = HTFileTransferStateTransfering;
[_databaseManager updateOrInsertDownloadItem:fdi];
// Start the download.
[fdi.downloadTask resume];
// Indicate for each file that is being downloaded.
fdi.isDownloading = YES;
}
else{
}
}
// Reload the table view.
[self.tblFiles reloadData];
}
实现相关Delegate方法
//此代理方法是必须实现的。文件下载完成后,将文件从临时目录写入定制化的其他目录,并更新数据库中的下载信息
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *destinationFilename = downloadTask.originalRequest.URL.lastPathComponent;
NSURL *destinationURL = [self.docDirectoryURL URLByAppendingPathComponent:destinationFilename];
if ([fileManager fileExistsAtPath:[destinationURL path]]) {
[fileManager removeItemAtURL:destinationURL error:nil];
}
BOOL success = [fileManager copyItemAtURL:location
toURL:destinationURL
error:&error];
if (success) {
int index = [self getFileDownloadInfoIndexWithTaskIdentifier:downloadTask.taskIdentifier];
FileDownloadInfo *fdi = [self.arrFileDownloadData objectAtIndex:index];
fdi.isDownloading = NO;
fdi.downloadComplete = YES;
fdi.status = HTFileTransferStateDone;
fdi.taskResumeData = nil;
[_databaseManager updateDownloadItem:fdi];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// Reload the respective table view row using the main thread.
[self.tblFiles reloadRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:index inSection:0]]
withRowAnimation:UITableViewRowAnimationNone];
}];
}
else{
NSLog(@"Unable to copy temp file. Error: %@", [error localizedDescription]);
}
}
// 实现此代理方法,获取文件下载的进度信息,同时将其更新到DB中
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
if (totalBytesExpectedToWrite == NSURLSessionTransferSizeUnknown) {
NSLog(@"Unknown transfer size");
}
else{
// Locate the FileDownloadInfo object among all based on the taskIdentifier property of the task.
int index = [self getFileDownloadInfoIndexWithTaskIdentifier:downloadTask.taskIdentifier];
FileDownloadInfo *fdi = [self.arrFileDownloadData objectAtIndex:index];
fdi.downloadProgress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
UITableViewCell *cell = [self.tblFiles cellForRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]];
UIProgressView *progressView = (UIProgressView *)[cell viewWithTag:CellProgressBarTagValue];
progressView.progress = fdi.downloadProgress;
}];
NSDate * currentDate = [NSDate date];
NSTimeInterval time = [currentDate timeIntervalSinceDate:_lastDate];
if (time >= 1 || totalBytesWritten == totalBytesExpectedToWrite){
_lastDate = currentDate;
[_databaseManager updateDownloadItem:fdi];
NSLog(@"[HTFileDownloader]: Progress update: %f)", fdi.downloadProgress);
}
}
}
处理后台下载
当应用不在前台或未启动,但是后台传输在进行时,每一次后台传输进程有消息到来,都会调用 application:handleEventsForBackgroundURLSession:completionHandler:,来唤醒应用。 此方法有两个参数:
//此处只对completionHandler进入一次copy,backgroundSession的创建在应用启动初始化的工作中一并处理
-(void)application:(U
IApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler{
self.backgroundTransferCompletionHandler = completionHandler;
}
当系统已经没有其他的messages通知给应用时,NSURLSession的代理方法URLSessionDidFinishEvensForBackgroundURLSession:会被调用。所以我们需要实现此方法,并调用上述completionHandler
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session{
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
// Check if all download tasks have been finished.
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([downloadTasks count] == 0) {
if (appDelegate.backgroundTransferCompletionHandler != nil) {
// Copy locally the completion handler.
void(^completionHandler)() = appDelegate.backgroundTransferCompletionHandler;
// Make nil the backgroundTransferCompletionHandler.
appDelegate.backgroundTransferCompletionHandler = nil;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// Call the completion handler to tell the system that there are no other background transfers.
completionHandler();
}
}
}];
}
继续戳->完整的Demo源码
这里列举几种case,通过Charles抓包分析,对后台传输服务的能力进行简单总结。
case1 应用处于前台的时候,进行下载
case2 下载过程中,应用退到后台
下载不受限地在后台继续进行。在下载完成之前,应用切换到前台,下载界面保持最新的状态,直至下载结束; 在所有下载完成后,App Switcher中页面快照被更新。进入前台,下载界面被更新,下载完成。
case3 下载过程中,应用crash而终止,下载在后台继续进行
在下载完成之前,应用启动进入前台,下载界面被更新,下载在前台正常进行,直至结束 在下载完成之后,App Switcher中页面快照被更新。应用启动进入前台,下载界面被更新,下载完成.
case4 下载过程中,应用被强制退出,下载立即停止
So,不同于前面几种后台模式,Background Transfer Service通过独立的后台进程接管下载任务。所以,当应用退到后台或者是应用因crash等被中断运行(除了force-quit),并不影响后台下载进程继续运行。如果下载完成时,应用处于后台suspend的状态,那么系统会在后台唤醒应用;如果应用没有运行的话,会在后台启动应用。然后在相应的回调中,执行下载结束之后所要处理的相关工作。
Background Transfer Service特别适合于一些大文件的传输,如果配合Background Fetch,Remote Notification的话,就可以满足我们应用开发过程中的更多需求。
iOS7以来,强大的多任务和网络API为现有应用和新应用开启了一系列全新的可能性。通过BackgroundTask,我们可以像系统申请更多的时间在后台运行任务;通过Background Fetch和Push,也使得应用启动无需等待数据更新加载成为可能;而Background Transfer Service,通过NSURLSession更好地帮我们实现任何传输任务在后台自由执行。在使用过程中,我们只需要根据需求合理地选择一种或多种后台模式,共同服务于我们的APP,就可以带来不一样的用户体验!
网易云新用户大礼包:https://www.163yun.com/gift
本文来自网易实践者社区,经作者何慧授权发布。