在ios7之前,当你的app进入后台之后,那么你能做的事情非常少。只有VOIP和定位服务等一些基本服务可以在后台运行,对于其他的的服务,只能通过后台任务来执行,而且仅限制在几分钟之内。如果你想下载一个很大的视频或者备份你的照片到服务器,那么你可能完不成这些任务,进程就被挂起了。
iOS7添加了两个新的接口,通过这两个接口你能够在后台更新的你UI和内容。第一,background fetch,ios7允许定期从网络获取新的内容。第二,远程通知,这是一个新的特性,利用了推送来通知app某些事件发生了。这两个新的特性能够让你在后台进行网络传输。
以上的两个特性都是通过app代理实现的,在程序被挂起之前你有30秒的时间来执行程序。
对于多任务来所,唯一明显的变化是新的app多任务的切换,当程序进入后台的时候会展示一个应用程序的快照。但是这个快照是可以更新的,在下面我们将看到如何更新这个快照。
Background Fetch
Background Fetch是一种智能的轮训机制,对于更新比较频烦的应用来说能表现出非常好的效果。系统会根据用户的行为来唤醒程序,并且在程序启动之前触发后台获取。例如,用户每天在1点中使用我们的app,系统会学习并且适应这个用户的习惯,所以,系统会每天在1点之前的一段时间内进行后台获取,为了减少电池的损耗,ios统一通过设备的无线电来进行获取。
第一步,如果你想进行后台获取,那么你需要在plist文件中指定UIBackgroundModes键,指定这个键的最简单的办法是利用xcode5的"Capabilities"选项卡,直接打开Background Modes进行勾选,或者你也可以手动编辑:
<key>UIBackgroundModes</key> <array> <string>fetch</string> </array>接下来告诉系统你想多久fetch一次:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum]; return YES; }默认的fetch间隔是永远不进行fetch,所以你需要告诉ios一个时间间隔,否则程序在后台永远不会被唤醒。UIApplicationBackgroundFetchIntervalMinimum告诉系统去管理这个程序的唤醒,越often越好。但是如果你觉得这样做没必要的话那么你需要自己指定一个时间间隔。比如一个天气的app每小时进行一次更新,那么系统至少要等一个小时进行fetch。
如果你的app允许用户logout,并且退出后又没什么新的数据,那么设置这个间隔为UIApplicationBackgroundFetchIntervalNever,这样可以节约资源。
最后一步是在你的delegate实现如下代码:
- (void) application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; NSURL *url = [[NSURL alloc] initWithString:@"http://yourserver.com/data.json"]; NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (error) { completionHandler(UIBackgroundFetchResultFailed); return; } // Parse response/data and determine whether new content was available BOOL hasNewData = ... if (hasNewData) { completionHandler(UIBackgroundFetchResultNewData); } else { completionHandler(UIBackgroundFetchResultNoData); } }]; // Start the task [task resume]; }这个代理就是app被唤醒工作的地方,记住你只有30秒时间进行update,当update结束之后你需要调用处理结束程序。
完成处理程序有两个目的,一是衡量启动这个进程耗费的资源,并且根据传递的UIBackgroundFetchResult这个参数来判断新的数据是否available。二,当你调用完成处理程序的时候,程序的快照被更新了,在多任务的新APIs当中,所有完成处理程序的快照行为是相同的。
在程序的世界当中,你应改传递完成处理程序最为你的程序的子组建。当数据处理结束的时候调用这个完成处理程序,更新你的UI。
在这一点上,你可能好奇ios是如何在后台更新你的UI的,生命周期在后台获取时是什么样的。如果你的程序当前被挂起了,那么系统会唤醒你的程序在调用application:performFetchWithCompletionHandler:之前。如果你当前的程序没有运行,那么系统将会launch它,调用一些usual方法,像application: didFinishLaunchingWithOptions:,你完全可以把这个过程想象为用户从Springboard启动了程序,只不过你看不到UI和渲染的屏幕。
在大多数情况下,在程序启动的时候我们要做的事情都是相同的,不管是前台启动还是后台启动。但是我们仍然可以捕捉到是从什么状态下启动程序的,通过查看UIApplication的applicationState属性:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSLog(@"Launched in background %d", UIApplicationStateBackground == application.applicationState); return YES; }
测试Background Fetch
有两种方法来测试Background fetch,最简单的方法是从xcode启动的你的app,然后点击debug菜单下的Simulate Background Fetch,另外一种方式是制定一个计划,让xcode按照计划来运行程序,在Product菜单下选择Scheme,然后 Manage Schemes,在这你可以编辑一个计或者添加一个计划。
Remote Notifications
推送的意义和功能这里不再赘述。当你接到推送的时候会调用
- (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { NSLog(@"Remote Notification userInfo is %@", userInfo); NSNumber *contentID = userInfo[@"content-id"]; // Do something with the content ID completionHandler(UIBackgroundFetchResultNewData); }
同样,系统给你30秒时间来处理这些事务。
NSURLSession and Background Transfer Service
NSURLSession在ios7 当中是一个新的类,它是在基础网络中的新技术。为了替代NSURLConnection,一些相似的概念和类被封装了,像 NSURL
, NSURLRequest
,和NSURLResponse。所以你将用
NSURLSessionTask替代NSURLConnection来进行网络请求和处理响应。有三种类型的session tasks,data,download,upload, 这三种用法差不多,会了一个其他的就会了。
一个NSURLSession标示了一个或多个NSURLSessionTask,它会根据创建这些task的NSURLSessionConfiguration的不同而作出不同的行为。你可能会创建NSURLSession
s,根据NSURLSessionConfiguration进行分组。为了能够跟后台进行互动,你需要创建[NSURLSessionConfiguration backgroundSessionConfiguration],使用了这个sesion的task在外部的一个进程运行,即使你的程序崩溃了或者挂起了或者被杀死了,那么这个进程不受影响。
NSURLSessionConfiguration允许你设定HTTP头,允许你指定缓存策略,限制使用蜂窝网络等。一个需要注意的地方是discretionary标志,它标识系统是否以最佳性能调度任务,也就是说,你的设备需要在wifi下并且电源充足,否则后台传输不能够被执行。
NSURLSessionDownloadTask
- (NSURLSession *)backgroundURLSession { static NSURLSession *session = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSString *identifier = @"io.objc.backgroundTransferExample"; NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfiguration:identifier]; session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:[NSOperationQueue mainQueue]]; }); return session; }
//接到推送的委托
- (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { NSLog(@"Received remote notification with userInfo %@", userInfo); NSNumber *contentID = userInfo[@"content-id"]; NSString *downloadURLString = [NSString stringWithFormat:@"http://yourserver.com/downloads/%d.mp3", [contentID intValue]]; NSURL* downloadURL = [NSURL URLWithString:downloadURLString]; NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL]; NSURLSessionDownloadTask *task = [[self backgroundURLSession] downloadTaskWithRequest:request]; task.taskDescription = [NSString stringWithFormat:@"Podcast Episode %d", [contentID intValue]]; [task resume]; completionHandler(UIBackgroundFetchResultNewData); }
我们创建了一个下载任务,并对它的请求进行了配置,还提供了接下来要用到的描述等。你必须要要执行[task resume]来启动任务,这样任务才能在app挂起状态下启动执行。
现在我们要做的是实现NSURLSessionDownloadDelegate这个委托,用来接收任务完成的消息。在session的声明周期里如果你想要处理认证或者其他的事件你需要实现
NSURLSessionDelegate
委托或者 NSURLSessionTaskDelegate委托。你应该看看苹果的文档
https://developer.apple.com/library/ios/documentation/cocoa/Conceptual/URLLoadingSystem/NSURLSessionConcepts/NSURLSessionConcepts.html#//apple_ref/doc/uid/10000165i-CH2-SW42,它介绍了各种类型的session tasks的整个声明周期。
NSURLSessionDownloadDelegate
的所有方法都是需要实现的,虽然我们的例子只用了[NSURLSession downloadTask:didFinishDownloadingToURL:] 这个。当任务结束下载的时候,问及那被存储到了一个临时目录,你必须移动或copy这个文件到你的app,否则下次回调这个代理的时候这个文件会被删除。下面是这些代理:
#Pragma Mark - NSURLSessionDownloadDelegate - (void) URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { NSLog(@"downloadTask:%@ didFinishDownloadingToURL:%@", downloadTask.taskDescription, location); // Copy file to your app's storage with NSFileManager // ... // Notify your UI }
- (void) URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { }
- (void) URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { }
如果当session task运行完成了,而此时的app又是在前台活跃的,那么上面的代码就足够了。实际上,在大对数情况下你的app没在运行状态或者被挂起了,在这种情况下你必须实现两个application 委托方法,这样系统就能唤醒的你程序。不想之前的委托,application委托方法被回调两次,这时你的session或者task的委托可能会受到几条消息。application的application: handleEventsForBackgroundURLSession:函数在 NSURLSession
delegate回调之前被调用,URLSessionDidFinishEventsForBackgroundURLSession
在之后被调用,在前面的委托里你需要存储完成处理代码,在接下来的委托里通过完成处理程序更新你的UI。
- (void) application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler { // You must re-establish a reference to the background session, // or NSURLSessionDownloadDelegate and NSURLSessionDelegate methods will not be called // as no delegate is attached to the session. See backgroundURLSession above. NSURLSession *backgroundSession = [self backgroundURLSession]; NSLog(@"Rejoining session with identifier %@ %@", identifier, backgroundSession); // Store the completion handler to update your UI after processing session events [self addCompletionHandler:completionHandler forSession:identifier]; } - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { NSLog(@"Background URL session %@ finished events.\n", session); if (session.configuration.identifier) { // Call the handler we stored in -application:handleEventsForBackgroundURLSession: [self callCompletionHandlerForSession:session.configuration.identifier]; } } - (void)addCompletionHandler:(CompletionHandlerType)handler forSession:(NSString *)identifier { if ([self.completionHandlerDictionary objectForKey:identifier]) { NSLog(@"Error: Got multiple handlers for a single session identifier. This should not happen.\n"); } [self.completionHandlerDictionary setObject:handler forKey:identifier]; } - (void)callCompletionHandlerForSession: (NSString *)identifier { CompletionHandlerType handler = [self.completionHandlerDictionary objectForKey: identifier]; if (handler) { [self.completionHandlerDictionary removeObjectForKey: identifier]; NSLog(@"Calling completion handler for session %@", identifier); handler(); } }
如果你的程序没有在前台运行,那么这两阶段的处理,在background transfer结束的时候,对于更新你的UI来说是必要的。如果在background transfer结束的时候,你的程序根本没启动,那么系统会启动你的程序,以前的application和session的delegate会在application:didFinishLaunchingWithOptions:之后执行。
Configuration and Limitation
我们已经简要的介绍后台传输服务的耗电情况,但是你应该通读上面提供的文档,看看NSURLSessionConfiguration
的options当中哪条最适合你。除了download tasks ,NSURLSession
也全面支持upload tasks。
background sessions也有一些限制,在NSURLSession你不能使用block-based的回调,在后台启动程序是一件很费资源的事,所以http的重定向经常被用到。background sessions只支持http和https。系统会根据可用的资源对background sessions进行优化。所以你的background session不可能总在后台服务。
NSURLSessionDataTasks
只能短期的存活,进行小的请求,不能用来下载和上传。