关闭

【iOS开发系列】后台模式

标签: background后台iOS
676人阅读 评论(0) 收藏 举报
分类:
IOS里面的后台模式
    本文介绍下ios的后台模式,本文是一篇译文,原文地址:点击打开链接
        从ios4开始,当用户点击home键之后,你可以设计你的应用,使它在内存中挂起。虽然应用还在内存中,但是所有的操作都被暂停了,除非用户重新开启应用。是吧?
        当然也有一些例外不遵循这个规则。在某种情况下,应用依旧在后台运行代码,本篇教程就来告诉你什么时候、以及怎么应用这些后台的操作。
       ios对后台应用有一些严格的限制,对于在ios平台“真正的”多任务,这并不是一个神奇的解决方案。当用户切换到其他的ap时,大部分的应用依然会被完全挂起。你的应用只会在以下特殊情况被允许后台运行------正在播放音频、获取位置更新、正在进行voip电话、杂志类应用的正在下载新闻。
      如果你的应用不需要做这些事情,那么很不幸。。。有一个例外就是:所有的应用在被挂起之前都有10分钟的时间来结束现在正在做的事情。因此这种后台运行可能并不适合你。如果适合你,请继续阅读吧。
       接下来你将学到,在ios平台一共有5种后台运行的模式。本教程你将创建的应用是一个简单的有选项卡的应用,每一个选项卡都证明了一种后台模式的运行-----从连续的音频播放到voip网络电话。让我们开始教程吧!

开启后台模式

在开始研究该项目之前,让我们快速浏览一下ios平台5种后台运行的基本模式。
  • 播放音频-----应用程序可以在后台持续的播音或者录音。
  • 接收位置更新-----随着设备位置的变化应用程序要持续获得地理位置更新。
  • 执行有限长度的任务----在有限的时间内,无论任何情况下应用程序都可以执行任何代码。
  • 正在下载杂志-----杂志类应用特例,应用程序能够在后台下载
  • 提供voip(voice-over-ip)服务---应用程序能够在后台执行任意的代码,当然苹果会限制它的用途,你的应用必须提供voip服务。
本教程将按照上面的顺序告诉你如何使用上面5种模式。如果你只对其中的某种感兴趣,你就跳过其他的吧。
开始本项目开启你的ios后台运行的学习吧。首先下载样例工程,这里有一些小贴士,用户接口已经配置好了。把工程运行起来,5个tab如下图所示:

这些tab就是你接下来学习的路线图。
注意:为了达到完美的效果,你应该在真机上运行该项目,一些后台任务在模拟器上面运行的不太好。

播放音频

在ios上播放音频有好几种方式,为了提供更多的音频数据来播放,这些方法大多需要实现一些回调方法。所谓回调就是ios什么时候让你的应用来做这些事情比如delegate,在回调里填充音频数据。
如果你想从一些流数据里面来播放音频,你先开启一个网络连接,该连接的回调方法会提供持续的音频数据。
当你激活了音频后台模式,即使你的应用不在前台ios也会继续运行你的callback方法。没错------在本教程的5种后台模式里面,音频后台模式是自动化最高的一个。你只需要激活它,并且提供合适的操作设施就可以了。
当你的应用真的需要音频后台播放,你再使用它,如果你耍小聪明,用这种模式干其他事情,而播放器不发声,苹果将拒绝你的app上线。
在这部分你将添加一个音频播放器到你的应用里面。打开后台模式,向你自己证明它真的起作用吧。
为了播放音频,你需要学习AV Foundation,打开TBFirstViewController.m 在顶部添加如下代码:
[objc] view plaincopy
  1. #import <AVFoundation/AVFoundation.h>  
现在找到viewDidLoad ,在方法的底部添加如下代码:
[objc] view plaincopy
  1. // Set AVAudioSession  
  2.  NSError *sessionError = nil;  
  3.  [[AVAudioSession sharedInstance] setDelegate:self];  
  4.  [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&sessionError];  
  5.   
  6.  // Change the default output audio route  
  7.  UInt32 doChangeDefaultRoute = 1;  
  8.  AudioSessionSetProperty(kAudioSessionProperty_OverrideCategoryDefaultToSpeaker,  
  9.    sizeof(doChangeDefaultRoute), &doChangeDefaultRoute);  
这段代码初始化了音频session,并且确保声音播放是通过扩音器来进行的,而不是手机听筒。你还需要一对成员变量来记录播放的轨迹,插入下面两个声明:
[objc] view plaincopy
  1. @property (nonatomicstrongAVQueuePlayer *player;  
  2. @property (nonatomicstrongid timeObserver;  
把它们放在这段代码中间,在文件的头部:
[objc] view plaincopy
  1. @interface TBFirstViewController ()  
  2.    
  3. // Insert code here  
  4.    
  5. @end  
开始工程包含了从点击打开链接的音频文件,被寄予了信誉,你可以免费使用这些音乐。
其中播放音乐最简单的一种方式就是使用 AV Foundation的 AVPlayer. 被称为AVQueuePlayer. ,AVQueuePlayer. 让你建立一个 AVPlayerItems 的队列,然后他们会被依次的播放。
再一次,在viewDidLoad 方法的最后加上下列代码:
[objc] view plaincopy
  1. NSArray *queue = @[  
  2.  [AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"IronBacon" withExtension:@"mp3"]],  
  3.  [AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"FeelinGood" withExtension:@"mp3"]],  
  4.  [AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"WhatYouWant" withExtension:@"mp3"]]];  
  5.   
  6.  self.player = [[AVQueuePlayer alloc] initWithItems:queue];  
  7.  self.player.actionAtItemEnd = AVPlayerActionAtItemEndAdvance;  
这段代码创建了一个AVPlayerItem 对象的数组。接着用该数组创建了AVQueuePlayer 对象。另外的,该队列将播放下一段音频当这一段播放完成时。
随着播放队列的进行,为了更改歌曲名称,你需要观察currentItem 对象的属性改变,现在你可以添加观察方法,在viewDidLoad:的下面:
[objc] view plaincopy
  1. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(voidvoid *)context  
  2. {  
  3.     if ([keyPath isEqualToString:@"currentItem"])  
  4.     {  
  5.         AVPlayerItem *item = ((AVPlayer *)object).currentItem;  
  6.         self.lblMusicName.text = ((AVURLAsset*)item.asset).URL.pathComponents.lastObject;  
  7.         NSLog(@"New music name: %@"self.lblMusicName.text);  
  8.     }  
  9. }  
当这个方法被调用的时候,你首先要确定被更新的属性是你感兴趣的那个。在这种情形下,实际上是没必要的,因为只有一个属性被观察。但是确是一个好的实践来检查,当你 以后添加更多的观察者。如果是currentItem key,你可以使用文件名来更新lblMusicName 标签。
你还需要一种方式来更新显示正在播放音频流逝时间的标签,做这件事情最好的方式就是使用 addPeriodicTimeObserverForInterval:queue:usingBlock: 方法,该方法将在给定的队列里面被调用。
把这段代码添加到 viewDidLoad:方法的最后面:
[objc] view plaincopy
  1. void (^observerBlock)(CMTime time) = ^(CMTime time) {  
  2.      NSString *timeString = [NSString stringWithFormat:@"%02.2f", (float)time.value / (float)time.timescale];  
  3.      if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive) {  
  4.          self.lblMusicTime.text = timeString;  
  5.      } else {  
  6.          NSLog(@"App is backgrounded. Time is: %@", timeString);  
  7.      }  
  8.  };  
  9.   
  10.  self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(101000)  
  11.                                                                queue:dispatch_get_main_queue()  
  12.                                                           usingBlock:observerBlock];  
你首先创建了一个块,无论什么时候时间流逝了,这块代码就会执行。如果你不知道怎么使用代码块(你应该会,这个棒极了),请阅读点击打开链接,这个块基于应用程序的状态创建了一个更新歌曲时间的字符串。在这之后调用 - (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block 来开启更新。
让我们暂停一下,来讨论应用程序的状态:
应用程序一般有下列五种状态,简单来说,他们是:
  • 未运行----你的应用还未启动
  • 活动----应用被启动了,在前台
  • 未激活-----你的应用已经启动了,但是发生了一些事情被打断了。比如来电话了,他就变成未激活了,未激活意味着应用还在前台运行,但是他不能接收任何的事件
  • 后台---在这种状态下你的应用不在前台,但是它依旧可以运行代码
  • 挂起---这个状态应用将不能运行代码
如果你想进一步研究这些状态之间的不同点,这个议题在苹果官网有很好的文档说明点击打开链接
你可以通过调用[[UIApplication sharedApplication] applicationState]来查看应用程序的状态。请记住我们一共可以得到3种状态UIApplicationStateActiveUIApplicationStateInactive, 和UIApplicationStateBackground. 当你的app正在运行时suspend和not running很明显永远也不会发生。因此并没有定义他们的枚举值。
让我们回到代码,如果应用程序处于激活状态,你需要更新音乐标签,如果不这样做的话,你只是在控制台打印了信息。当应用处于后台时,你依旧可以更新标签,只是为了证明应用在后台时还可以接收回调信息。
现在你还没做的事情是:完成didTapPlayPause 方法来使play/pause按钮起作用。把下面的代码添加到TBFirstViewController.m,文件的末尾@end的上面:
[objc] view plaincopy
  1. - (IBAction)didTapPlayPause:(id)sender  
  2. {  
  3.     self.btnPlayPause.selected = !self.btnPlayPause.selected;  
  4.     if (self.btnPlayPause.selected)  
  5.     {  
  6.         [self.player play];  
  7.     }  
  8.     else  
  9.     {  
  10.         [self.player pause];  
  11.     }  
  12. }  

好的,这就是所有的代码了,编译并运行代码,你将看到下面的效果:

现在点击play按钮,音乐就开始了。
让我们来测试一下,后台是否运行了,点击home键,糟糕,为什么音乐停止了,还有一段神奇的事情没做呢。
对大多数后台模式来说(whatever 模式除外),你需要在info。plist里面增加一个键值对,来声明应用程序需要在后台运行。
回到xcode,做如下事情:
  1. 在文件导航栏里面选中工程。
  2. 点击 info
  3. 当鼠标悬浮在列表上面时点击+
  4. 在出现的列表里面选择 Required Background Modes 

再次编译并运行,播放音乐并按home键,这次你依旧能听到音乐,即使是在后台运行。
如果还不起作用的 话,可能是因为你是用的模拟器,换个真机试一下,在你xcode的控制台你就可以看见打印的信息。这就证明即使在后台,代码依旧在运行。
一种模式讲完了,如果你想继续学习整个教程---还有4个。

接收后台更新

当处于定位后台模式的时候,即使处于后台,你的应用也将接收到用户位置更新的位置代理信息。你可以控制位置更新的精准度,也可以在后台改变精准度。
与上面一样---当你的应用真的需要为用户提供位置信息时,你再使用这种模式。当你使用这个模式,苹果如果发现用户并没有从你这里取得信息,你的应用将会被拒绝。有时候苹果也需要你来添加一些警告,你的应用将导致电池使用率的增加。
第二个tab栏是位置更新,打开TBSecondViewController.m 添加一些声明属性:
[objc] view plaincopy
  1. @property (nonatomicstrongCLLocationManager *locationManager;  
  2. @property (nonatomicstrongNSMutableArray *locations;  

这些属性应该被添加到这两行代码之间:
[objc] view plaincopy
  1. @interface TBSecondViewController ()  
  2.    
  3. // add code here  
  4.    
  5. @end  

CLLocationManager 是你用来得到位置更新的类文件,你将使用locations数组来存储位置信息,用来在地图上画出来。
viewDidLoad:方法的最后添加如下:
[objc] view plaincopy
  1. self.locations = [[NSMutableArray alloc] init];  
  2. self.locationManager = [[CLLocationManager alloc] init];  
  3. self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;  
  4. self.locationManager.delegate = self;  

这段代码创建了一个空的数组来存储位置信息,接着创建了 CLLocationManager对象,这段代码设置位置的精确度到最高,在这里你可以根据你的需要来调节。一会你将学到更多的定位精确度信息。
现在你可以完成accuracyChanged: 方法:
[objc] view plaincopy
  1. - (IBAction)accuracyChanged:(id)sender  
  2. {  
  3.     const CLLocationAccuracy accuracyValues[] = {  
  4.         kCLLocationAccuracyBestForNavigation,  
  5.         kCLLocationAccuracyBest,  
  6.         kCLLocationAccuracyNearestTenMeters,  
  7.         kCLLocationAccuracyHundredMeters,  
  8.         kCLLocationAccuracyKilometer,  
  9.         kCLLocationAccuracyThreeKilometers};  
  10.    
  11.     self.locationManager.desiredAccuracy = accuracyValues[self.segmentAccuracy.selectedSegmentIndex];  
  12. }  
accuracyValues 数组包含了CLLocationManager. 所有可能用到的值。这个属性确定了定位的精度。
你可能认为这样做很愚蠢,为什么不让location manager总是提供最精确的位置信息?因为低的精确度更省电。
除了desiredAccuracydistanceFilter. 之外,还有另外的一个属性来控制你的app多久接收一次位置更新,这个属性将告诉 location manager 当设备呗移动了某个特定的距离时,你将获得位置更新。
同样为了省电,这个数值应该设置的尽可能高些。
现在你可以在 enabledStateChanged: 方法里面添加如下代码来实现定位。
[objc] view plaincopy
  1. - (IBAction)enabledStateChanged:(id)sender  
  2. {  
  3.     if (self.switchEnabled.on)  
  4.     {  
  5.         [self.locationManager startUpdatingLocation];  
  6.     }  
  7.     else  
  8.     {  
  9.         [self.locationManager stopUpdatingLocation];  
  10.     }  
  11. }  
xib文件里面有一个UISwitch 来开启和关闭定位,下面你需要添加CLLocationManagerDelegate 方法来获取位置更新,在@end的上面加入:
[objc] view plaincopy
  1. #pragma mark - CLLocationManagerDelegate  
  2.    
  3. /* 
  4.  *  locationManager:didUpdateToLocation:fromLocation: 
  5.  * 
  6.  *  Discussion: 
  7.  *    Invoked when a new location is available. oldLocation may be nil if there is no previous location 
  8.  *    available. 
  9.  * 
  10.  *    This method is deprecated. If locationManager:didUpdateLocations: is 
  11.  *    implemented, this method will not be called. 
  12.  */  
  13. - (void)locationManager:(CLLocationManager *)manager  
  14.     didUpdateToLocation:(CLLocation *)newLocation  
  15.            fromLocation:(CLLocation *)oldLocation  
  16. {  
  17.     // Add another annotation to the map.  
  18.     MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];  
  19.     annotation.coordinate = newLocation.coordinate;  
  20.     [self.map addAnnotation:annotation];  
  21.    
  22.     // Also add to our map so we can remove old values later  
  23.     [self.locations addObject:annotation];  
  24.    
  25.     // Remove values if the array is too big  
  26.     while (self.locations.count > 100)  
  27.     {  
  28.         annotation = [self.locations objectAtIndex:0];  
  29.         [self.locations removeObjectAtIndex:0];  
  30.    
  31.         // Also remove from the map  
  32.         [self.map removeAnnotation:annotation];  
  33.     }  
  34.    
  35.     if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)  
  36.     {  
  37.         // determine the region the points span so we can update our map's zoom.  
  38.         double maxLat = -91;  
  39.         double minLat =  91;  
  40.         double maxLon = -181;  
  41.         double minLon =  181;  
  42.    
  43.         for (MKPointAnnotation *annotation in self.locations)  
  44.         {  
  45.             CLLocationCoordinate2D coordinate = annotation.coordinate;  
  46.    
  47.             if (coordinate.latitude > maxLat)  
  48.                 maxLat = coordinate.latitude;  
  49.             if (coordinate.latitude < minLat)  
  50.                 minLat = coordinate.latitude;  
  51.    
  52.             if (coordinate.longitude > maxLon)  
  53.                 maxLon = coordinate.longitude;  
  54.             if (coordinate.longitude < minLon)  
  55.                 minLon = coordinate.longitude;  
  56.         }  
  57.    
  58.         MKCoordinateRegion region;  
  59.         region.span.latitudeDelta  = (maxLat +  90) - (minLat +  90);  
  60.         region.span.longitudeDelta = (maxLon + 180) - (minLon + 180);  
  61.    
  62.         // the center point is the average of the max and mins  
  63.         region.center.latitude  = minLat + region.span.latitudeDelta / 2;  
  64.         region.center.longitude = minLon + region.span.longitudeDelta / 2;  
  65.    
  66.         // Set the region of the map.  
  67.         [self.map setRegion:region animated:YES];  
  68.     }  
  69.     else  
  70.     {  
  71.         NSLog(@"App is backgrounded. New location is %@", newLocation);  
  72.     }  
  73. }  

好长的一段代码,这里我们不讲解这些代码,代码的注释你应该看得懂。接下来在plist里面添加“App registers for location updates”一列。让ios知道你需要后台获取位置信息:


现在编译并运行代码,滑动switch到on


第一次运行时你将看到一个弹出框提示应用要获取你的位置信息,点击ok你就可以获取位置信息了。在模拟器上面也可以。
拿着真机到处走走,你将看到:

如果你是在使用模拟器,点击debug菜单,选择location-freeway driver


你将在控制台看到如下信息:
[objc] view plaincopy
  1. 2013-03-07 22:31:11.667 TheBackgrounder[52611:c07] App is backgrounded. New location is <+37.33500926,-122.03272188> +/- 5.00m (speed 7.74 mps / course 246.09) @ 3/7/13, 10:31:11 PM Eastern Daylight Time  
  2. 2013-03-07 22:31:12.670 TheBackgrounder[52611:c07] App is backgrounded. New location is <+37.33497737,-122.03281282> +/- 5.00m (speed 9.18 mps / course 251.37) @ 3/7/13, 10:31:12 PM Eastern Daylight Time  
  3. 2013-03-07 22:31:13.669 TheBackgrounder[52611:c07] App is backgrounded. New location is <+37.33494812,-122.03292120> +/- 5.00m (speed 10.78 mps / course 251.72) @ 3/7/13, 10:31:13 PM Eastern Daylight Time  
  4. 2013-03-07 22:31:14.658 TheBackgrounder[52611:c07] App is backgrounded. New location is <+37.33492222,-122.03304215> +/- 5.00m (speed 12.11 mps / course 254.18) @ 3/7/13, 10:31:14 PM Eastern Daylight Time  

执行限时任务-whatever

这种模式的官方叫法时Executing a Finite-Length Task in the Background“. 但是我还是认为whatever好记。
这个任务里面你可以执行任意的代码,比如下载,上传,渲染等 ,只不过时间是呗限制的,至于你能申请多少cpu时间,这个是由ios系统决定的,并不确定。但是你可以查看uiapplication‘的 backgroundTimeRemaining属性,来查看你还有多少剩余时间。
通常情况下,据以往的观察,你将有10分钟的时间,依旧这个没有api来保证,甚至也没有一个大概的时间范围保证。因此不要依赖这个数据。你将有可能只得到了5min或者5秒,你的app要做好各种打算。
这有一个计数的例子,我们将在后计数。

打开TBThirdViewController.m 文件,添加如下属性:
[objc] view plaincopy
  1. @property (nonatomicstrongNSDecimalNumber *previous;  
  2. @property (nonatomicstrongNSDecimalNumber *current;  
  3. @property (nonatomic) NSUInteger position;  
  4. @property (nonatomicstrongNSTimer *updateTimer;  
  5. @property (nonatomic) UIBackgroundTaskIdentifier backgroundTask;  
代码应该被添加到:
[objc] view plaincopy
  1. @interface TBThirdViewController ()  
  2.    
  3. // add code here  
  4.    
  5. @end  
NSDecimalNumbers 顺序的保存了两个之前的值,NSDecimalNumbers 类可以容纳很大的数据,因此特别适合做这个。position 是一个告诉现在数字位置的计时器。你将使用updateTimer 来证明使用这个api时计时器会继续进行。也可以减慢计数,你可以观察这些。
viewDidLoad 里面的最后加入:
[objc] view plaincopy
  1. self.backgroundTask = UIBackgroundTaskInvalid;  
最重要的部分,在didTapPlayPause:里面添加 :
[objc] view plaincopy
  1. - (IBAction)didTapPlayPause:(id)sender  
  2. {  
  3.     self.btnPlayPause.selected = !self.btnPlayPause.selected;  
  4.     if (self.btnPlayPause.selected)  
  5.     {  
  6.         self.previous = [NSDecimalNumber one];  
  7.         self.current  = [NSDecimalNumber one];  
  8.         self.position = 1;  
  9.    
  10.         self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.5  
  11.                                                             target:self  
  12.                                                           selector:@selector(calculateNextNumber)  
  13.                                                           userInfo:nil  
  14.                                                            repeats:YES];  
  15.    
  16.         self.backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{  
  17.             NSLog(@"Background handler called. Not running background tasks anymore.");  
  18.             [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];  
  19.             self.backgroundTask = UIBackgroundTaskInvalid;  
  20.         }];  
  21.     }  
  22.     else  
  23.     {  
  24.         [self.updateTimer invalidate];  
  25.         self.updateTimer = nil;  
  26.         if (self.backgroundTask != UIBackgroundTaskInvalid)  
  27.         {  
  28.             [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];  
  29.             self.backgroundTask = UIBackgroundTaskInvalid;  
  30.         }  
  31.     }  
  32. }  

让我门看一下代码确定如何使用该api。

按钮会依据选中状态,计数如果是停止状态,则应该启动;如果是启动状态,则应该停止。
首先,你应该初始化一列斐波那契变量。接着创建一个 NSTimer 对象,timer每秒钟会执行两次,调用calculateNextNumber 方法。
现在进入最重要的一步,调用 beginBackgroundTaskWithExpirationHandler:. 方法。这个方法告诉ios在后台时你需要更久的时间来
执行代码,调用该方法之后你的应用就会在后台获取cpu时间,调用endBackgroundTask:.方法来停止。
如果应用在后台一段时间之后,你仍然没有调用 endBackgroundTask: 方法,ios将调用你定义的
beginBackgroundTaskWithExpirationHandler: 方法里面的代码块,来给你一次停止你代码的机会,因此调用endBackgroundTask: 方法
来告诉ios你已经执行完了是一个好的做法。如果你不这么做,而且在代码块被调用后继续执行代码 ,你的应用就会被强行终止。
代码中第二个if语句比较简单,它只是让timer失效,并且调用 - (void)endBackgroundTask:(UIBackgroundTaskIdentifier)identifier 方法,
来告诉系统我不再需要cpu时间了。
当你每调用一次beginBackgroundTaskWithExpirationHandler:. 方法时,都要调用endBackgroundTask:方法。如果你调用
beginBackgroundTaskWithExpirationHandler: 方法两次而调用endBackgroundTask方法一次,直到你再次调用endBackgroundTask方法
之前你都会获得cpu时间。这也就是你为什么需要 backgroundTask 变量的原因。

现在你可以继续完成这个项目了,在 @end:之前加入下面代码:
[objc] view plaincopy
  1. - (void)calculateNextNumber  
  2. {  
  3.     NSDecimalNumber *result = [self.current decimalNumberByAdding:self.previous];  
  4.    
  5.     if ([result compare:[NSDecimalNumber decimalNumberWithMantissa:1 exponent:40 isNegative:NO]] == NSOrderedAscending)  
  6.     {  
  7.         self.previous = self.current;  
  8.         self.current  = result;  
  9.         self.position++;  
  10.     }  
  11.     else  
  12.     {  
  13.         // This is just too much.... Let's start over.  
  14.         self.previous = [NSDecimalNumber one];  
  15.         self.current  = [NSDecimalNumber one];  
  16.         self.position = 1;  
  17.     }  
  18.    
  19.     NSString *currentResultLabel = [NSString stringWithFormat:@"Position %d = %@"self.positionself.current];  
  20.     if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)  
  21.     {  
  22.         self.txtResult.text = currentResultLabel;  
  23.     }  
  24.     else  
  25.     {  
  26.         NSLog(@"App is backgrounded. Next number = %@", currentResultLabel);  
  27.         NSLog(@"Background time remaining = %.1f seconds", [UIApplication sharedApplication].backgroundTimeRemaining);  
  28.     }  
  29. }  

在这种情况下有更多的你感兴趣的地方,比如backgroundTimeRemaining 变量的值,当系统调用 beginBackgroundTaskWithExpirationHandler:.代码块的时候,计数就停止了。
编译并运行代码,切换到第三个tab:

点击Start 你就会看到应用正在计数,现在点击home键观察xcode控制台输出。你就会看到随着时间的流逝,数据在更新。
大多数情况下时间是以600s(10min)开始的,然后一直下降到5秒钟。如果你一直等待到5秒钟(根据你的情况,有可能是其他值),过期的代码块将会被执行,控制台也会停止输出。如果你再次回到app'计时器就会继续执行。
这段代码有一个bug,它给了我一个解释后台通知的一个机会。设想一下,你的应用在后台运行,直到等待代码执行时间过期,在这种情况下你的代码将执行过期的handler,并调用endBackgroundTask:, 方法来结束cpu时间的申请。
如果你再次回到app,计时器将会再次执行,但是当你再次退出应用时,你就不会再次得到后台时间了,为什么呢?因为在时间过期和app切换到后台这段时间里面没有地方来执行beginBackgroundTaskWithExpirationHandler:.方法。
这个问题怎么解决呢?有好几个方法来解决,其中一种就是利用应用状态改变的通知。
一共有两种方法你可以接受到应用状态改变的通知。一种时通过app delegate’的方法,另一种时监听ios发给你应用的一些通知。
  • UIApplicationWillResignActiveNotification 和 applicationWillResignActive: 当你的应用即将进入非活动(inactive)状态时这两个方法就会被调用。在这个时间点上,你的应用还没处于后台--它还处于前台--但是它不能接受到触摸事件了。
  • UIApplicationDidEnterBackgroundNotification and applicationDidEnterBackground: 这两个方法会在应用程序进入后台时调用,在这个时候你的应用就不再处于活动状态了,这将是你运行代码的最后机会了,如果你想获得更多的cpu时间,这将是最好的时候来调用beginBackgroundTaskWithExpirationHandler: 方法。
  • UIApplicationWillEnterForegroundNotification and applicationWillEnterForeground: 当应用程序返回活动状态时,这两个方法将被调用,这会应用仍在后台,但是你可以重新开始任何你想做的事情。如果再开始的时候你调用了beginBackgroundTaskWithExpirationHandler: ,现在就是调用endBackgroundTask: 的好时机。
  • UIApplicationDidBecomeActiveNotification and applicationDidBecomeActive: 这两个方法在上面的调用之后紧接着调用,这时你的应用已经从后台回到前台了。如果你的app刚从一个临时打断中恢复过来--一个电话,这两个方法就会被调用--你的应用并没有真正进入后台,UIApplicationWillResignActiveNotification 方法会被调用。

你可以在苹果的开发文档里面看到这些所有的信息。点击打开链接
下一部分将会包含如何使用这些通知,修改beginBackgroundTaskWithExpirationHandler bug留给读者吧!

处理报刊下载

在ios5里面苹果引进了杂志api,将允许你创建杂志和新闻类的app,有一些新的特性。使用报刊杂志类api的应用,没有通常的应用图标,因为他们是在杂志应用里面安装的。
杂志模式对杂志类应用非常特殊,它提供了一系列api使得你的应用很容易下载大文件。即使你的应用在后台,也可以持续下载整个文件。
如果你的应用不是报刊杂志类的请不要使用该模式,否则代码将不会起作用。
样例工程的xib文件里面有个UITextField 控件是在uiwebview里面加载的,通常这个url是:https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/ProgrammingWithObjectiveC.pdf 这是一个大文件,用来证明应用在后台时仍旧可以下载该pdf文件。如果你那里有其他的大型pdf文件你也可以用它来代替。
TBFourthViewController.m:顶部添加两个属性:
[objc] view plaincopy
  1. @property (nonatomicstrongNKIssue *currentIssue;  
  2. @property (nonatomicstrongNSString *issueFilename;  
首先,完成 UITextFieldDelegate 里面的方法,在编辑文本区域时点击return键,这个方法就会执行。这段代码可以放在该文件的任何地方。
[objc] view plaincopy
  1. #pragma mark - UITextFieldDelegate  
  2.    
  3. - (BOOL)textFieldShouldReturn:(UITextField *)textField  
  4. {  
  5.     self.webView.hidden = YES;  
  6.     self.progress.progress = 0.0f;  
  7.     self.progress.hidden = NO;  
  8.    
  9.     NKLibrary *library = [NKLibrary sharedLibrary];  
  10.     for (NKIssue *issue in [library.issues copy])  
  11.     {  
  12.         [library removeIssue:issue];  
  13.     }  
  14.     self.currentIssue = [library addIssueWithName:@"test" date:[NSDate date]];  
  15.    
  16.     NSURL *downloadURL = [[NSURL alloc] initWithString:self.txtURL.text];  
  17.     NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];  
  18.     NKAssetDownload *assetDownload = [self.currentIssue addAssetWithRequest:request];  
  19.     [assetDownload downloadWithDelegate:self];  
  20.     [textField resignFirstResponder];  
  21.     return YES;  
  22. }  

代码首先隐藏了web view显示了进度条,NKLibrary在app里面提供了一个单例对象来管理杂志。你需要使用它来创建NKIssue 对象。
首先移除所有的刊物,来避免添加同名称的刊物引发的错误。下一步,创建一个刊物,随便命名,在真实情况下应该是刊物名称或者一个数字,每个刊物都应该有不同的名称。
接下来,在文本框里面创建 NSURL 对象,再创建NSURLRequest 对象,最后把请求添加到 NKIssue 对象里面。使用NKAssetDownload 对象你就可以开始下载文件了,设置viewController为一个代理。
你可以为NSURLConnection 建立一个可选的代理方法,该方法将会放松下载进度的更新,把下面的代码添加到类文件的任意位置。
[objc] view plaincopy
  1. #pragma mark - NSURLConnectionDownloadDelegate  
  2.    
  3. - (void)connection:(NSURLConnection *)connection  
  4.       didWriteData:(long long)bytesWritten  
  5.  totalBytesWritten:(long long)totalBytesWritten  
  6. expectedTotalBytes:(long long)expectedTotalBytes  
  7. {  
  8.     float progress = (float)totalBytesWritten / (float)expectedTotalBytes;  
  9.     if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)  
  10.     {  
  11.         self.progress.progress = progress;  
  12.     }  
  13.     else  
  14.     {  
  15.         NSLog(@"App is backgrounded. Progress = %.1f", progress);  
  16.     }  
  17. }  
  18.    
  19. - (void)connectionDidFinishDownloading:(NSURLConnection *)connection  
  20.                         destinationURL:(NSURL *)destinationURL  
  21. {  
  22.     self.issueFilename = destinationURL.pathComponents.lastObject;  
  23.     NSURL *fileURL = [self.currentIssue.contentURL URLByAppendingPathComponent:self.issueFilename];  
  24.    
  25.     [[NSFileManager defaultManager] moveItemAtURL:destinationURL  
  26.                                             toURL:fileURL  
  27.                                             error:nil];  
  28.    
  29.     if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)  
  30.     {  
  31.         self.webView.hidden = NO;  
  32.         self.progress.hidden = YES;  
  33.         NSURL *fileURL = [self.currentIssue.contentURL URLByAppendingPathComponent:self.issueFilename];  
  34.         [self.webView loadRequest:[NSURLRequest requestWithURL:fileURL]];  
  35.     }  
  36.     else  
  37.     {  
  38.         NSLog(@"App is backgrounded. Download finished");  
  39.     }  
  40. }  
如果在下载列表里面有新的数据,第一个方法就会被调用。你不需要做太多的事情,操作系统会照顾好所有的数据,当应用在前台时 你可以更新ui,在后台时,你可以在控制台打印log。
当下载完成时,第二个方法会被调用。文件必须得移动到Newsstand API 规定的地方,如果空间满了,可以移除旧的报刊版本。
当移动完成后,你可以更新ui或者在后台打印信息。
如果在后台下载完成会发生什么?webview控件并不会被pdf内容更新,你可以用上面讲过的几种通知方式来修改这个问题。
首先从上面的方法中提取更新的代码,可以重复使用,而不用再次敲代码。在类文件的任意位置添加下面方法:
[objc] view plaincopy
  1. - (void)updateWebView  
  2. {  
  3.     self.webView.hidden = NO;  
  4.     self.progress.hidden = YES;  
  5.     NSURL *fileURL = [self.currentIssue.contentURL URLByAppendingPathComponent:self.issueFilename];  
  6.     [self.webView loadRequest:[NSURLRequest requestWithURL:fileURL]];  
  7. }  
connectionDidFinishDownloading:destinationURL: 里面移除多余的代码,之后if语句变成下面样子:
[objc] view plaincopy
  1. if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)  
  2. {  
  3.     [self updateWebView];  
  4. }  
  5. else  
  6. {  
  7.     NSLog(@"App is backgrounded. Download finished");  
  8. }  
当app变成活动状态时,添加下列你想调用的代码:
[objc] view plaincopy
  1. - (void)appBecameActive  
  2. {  
  3.     if (self.currentIssue && self.currentIssue.downloadingAssets.count == 0 && self.webView.hidden)  
  4.     {  
  5.         [self updateWebView];  
  6.     }  
  7. }  

最后,把下列代码添加到viewDidLoad:的末尾:
[objc] view plaincopy
  1. [[NSNotificationCenter defaultCenter] addObserver:self  
  2.                                          selector:@selector(appBecameActive)  
  3.                                              name:UIApplicationDidBecomeActiveNotification  
  4.                                            object:nil];  

当你添加了一个观察之后,不要忘记在对象被回收时取消它。因此让我们来完成dealloc:方法:
[objc] view plaincopy
  1. - (void)dealloc  
  2. {  
  3.    [[NSNotificationCenter defaultCenter] removeObserver:self];  
  4. }  
现在,你基本上做完了,猜一下,还有什么没做?。。。Info.plist 。这次你需要添加第二个键直队,来让app成为一个杂志类应用:
首先在 Required background modes 里面添加一个新的键值:


接着在plist的主列表里面添加一个新的键值,并且选择Application presents content in Newsstand:



当你选择这个项目之后,值就变成一个boolean类型,记住选择yes。
现在,编译并运行代码,切换到第四个tab栏,你将看到下面结果:

选择文本区域,点击return键盘的键,文件就会开始下载,你也会看到进度条从0开始一直到100;当下载完成以后你就会在webview里面看到文件:


好了,应用在前台时,显示正常。但它会通过真实的测试吗?
停止运行app,再次启动。这次点击return之后点击home键,你将会看到控制台的log输出,返回应用,你将看到webview更新了pdf的内容。耶!
如果你的网速很快,下载将会完成的很快,你可能都没能看到控制台的信息。在这种情况下,找一个大的pdf文件。
你可能注意到,你的应用不再有一个正常的图标了,它已经在杂志类应用里面了,并且有一个默认的报刊图标。


提供网络电话(voip)服务

最后一种模式是一种非常强大的后台模式,因为它允许你的应用在后台执行任意的代码。这种模式要比“Whatever”好,因为你可以执行任意的代码而没有时间限制。更给力的是,当app异常关闭了或者用户重启手机了,应用会在后台重新启动。非常棒!
需要注意的是,你的应用必须给用户提供VoIP的功能,否则苹果会拒绝你和你的应用。
创建一个voip应用已经超出了本教程的范围,但是我至少会构建一些基本原则。voip(Voice-over-IP)也就是网络电话,在本教程里面你将创建一个简单的应用来连接服务器。在后台时保持连接开启,当应用从链接接收到信息的时候回调。
开始,打开TBFifthViewController.m 文件,在顶部添加下面属性声明:
[objc] view plaincopy
  1. @property (nonatomicstrongNSInputStream *inputStream;  
  2. @property (nonatomicstrongNSOutputStream *outputStream;  
  3. @property (nonatomicstrongNSMutableString *communicationLog;  
  4. @property (nonatomicBOOL sentPing;  
接着定义一些常量,这个你在后来的链接中可能用到。在@implementation TBFifthViewController:前面添加下列代码:
[objc] view plaincopy
  1. const uint8_t pingString[] = "ping\n";  
  2. const uint8_t pongString[] = "pong\n";  
开始,有一个简便的方法给textview添加事件,当链接完成时、链接断开、你将得到提示。在类文件的任意位置添加如下代码:
[objc] view plaincopy
  1. - (void)addEvent:(NSString *)event  
  2. {  
  3.     [self.communicationLog appendFormat:@"%@\n", event];  
  4.     if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)  
  5.     {  
  6.         self.txtReceivedData.text = self.communicationLog;  
  7.     }  
  8.     else  
  9.     {  
  10.         NSLog(@"App is backgrounded. New event: %@", event);  
  11.     }  
  12. }  
特别简单,如果app是活动状态的,就添加字符,更新ui,在后台的话,就只是控制台打印。
现在添加didTapConnect:,方法的实现,当用户点击链接按钮时,方法就会被调用:
[objc] view plaincopy
  1. - (IBAction)didTapConnect:(id)sender  
  2. {  
  3.     if (!self.inputStream)  
  4.     {  
  5.         // 1  
  6.         CFReadStreamRef readStream;  
  7.         CFWriteStreamRef writeStream;  
  8.         CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)(self.txtIP.text), [self.txtPort.text intValue], &readStream, &writeStream);  
  9.    
  10.         // 2  
  11.         self.sentPing = NO;  
  12.         self.communicationLog = [[NSMutableString alloc] init];  
  13.         self.inputStream = (__bridge_transfer NSInputStream *)readStream;  
  14.         self.outputStream = (__bridge_transfer NSOutputStream *)writeStream;  
  15.         [self.inputStream setProperty:NSStreamNetworkServiceTypeVoIP forKey:NSStreamNetworkServiceType];  
  16.    
  17.         // 3  
  18.         [self.inputStream setDelegate:self];  
  19.         [self.outputStream setDelegate:self];  
  20.         [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];  
  21.         [self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];  
  22.    
  23.         // 4  
  24.         [self.inputStream open];  
  25.         [self.outputStream open];  
  26.    
  27.         // 5  
  28.         [[UIApplication sharedApplication] setKeepAliveTimeout:600 handler:^{  
  29.             if (self.outputStream)  
  30.             {  
  31.                 [self.outputStream write:pingString maxLength:strlen((char*)pingString)];  
  32.                 [self addEvent:@"Ping sent"];  
  33.             }  
  34.         }];  
  35.     }  
  36. }  
看起来有点复杂,因为你正在创建一个双向流。
  1. 最简单的方法就是使用CFStreamCreatePairWithSocketToHost 函数,这将意味着稍后需要建立很多NSInputStream 和outputStream的连接。
  2. 当你使用 CFStreamCreatePairWithSocketToHost, 创建了输入输出流后,你把它们连接到相应的Objective-C类里面。在这里调用setProperty:forKey: 方法很重要,因为它会提示ios系统即使应用在后台,也要维持住连接。你只需要为输入流做这些。
  3. 下一步,设置控制器对象为代理,设置两个流的loop到应用的主loop里面。ios操作系统需要知道调用那个运行loop里面的代理方法。在这种情况下最好的运行loop就是和应用的主loop关联起来。当你得到了新消息时,你需要在主线程里面来更新ui。
  4. 从这之后,流文件就已经被计划好了,你只需要调用open 方法。
  5. 最后需要在xcode里面做的事情只是针对voip应用的。调用setKeepAliveTimeout:handler: 方法之后,你可以设置一个handler,当应用在后台时,该handler会周期的调用。它允许你的应用做任何事情,为了保持长连接,它需要发送“ping”到你的服务器。你把它设置成10分钟调用一次---依据文档这是这个方法允许的最小值。在这里你需要做的事情就是给服务器发送一个ping,然后打印log信息就可以了。
现在你需要添加流代理,他将会接收来自连接的更新。把下面代码添加到你的类文件的任意位置:
[objc] view plaincopy
  1. #pragma mark - NSStreamDelegate  
  2.    
  3. - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode  
  4. {  
  5.     switch (eventCode) {  
  6.         case NSStreamEventNone:  
  7.             // do nothing.  
  8.             break;  
  9.    
  10.         case NSStreamEventEndEncountered:  
  11.             [self addEvent:@"Connection Closed"];  
  12.             break;  
  13.    
  14.         case NSStreamEventErrorOccurred:  
  15.             [self addEvent:[NSString stringWithFormat:@"Had error: %@", aStream.streamError]];  
  16.             break;  
  17.    
  18.         case NSStreamEventHasBytesAvailable:  
  19.             if (aStream == self.inputStream)  
  20.             {  
  21.                 uint8_t buffer[1024];  
  22.                 NSInteger bytesRead = [self.inputStream read:buffer maxLength:1024];  
  23.                 NSString *stringRead = [[NSString alloc] initWithBytes:buffer length:bytesRead encoding:NSUTF8StringEncoding];  
  24.                 stringRead = [stringRead stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];  
  25.    
  26.                 [self addEvent:[NSString stringWithFormat:@"Received: %@", stringRead]];  
  27.    
  28.                 if ([stringRead isEqualToString:@"notify"])  
  29.                 {  
  30.                     UILocalNotification *notification = [[UILocalNotification alloc] init];  
  31.                     notification.alertBody = @"New VOIP call";  
  32.                     notification.alertAction = @"Answer";  
  33.                     [[UIApplication sharedApplication] presentLocalNotificationNow:notification];  
  34.                 }  
  35.                 else if ([stringRead isEqualToString:@"ping"])  
  36.                 {  
  37.                     [self.outputStream write:pongString maxLength:strlen((char*)pongString)];  
  38.                 }  
  39.             }  
  40.             break;  
  41.    
  42.         case NSStreamEventHasSpaceAvailable:  
  43.             if (aStream == self.outputStream && !self.sentPing)  
  44.             {  
  45.                 self.sentPing = YES;  
  46.                 if (aStream == self.outputStream)  
  47.                 {  
  48.                     [self.outputStream write:pingString maxLength:strlen((char*)pingString)];  
  49.                     [self addEvent:@"Ping sent"];  
  50.                 }  
  51.             }  
  52.             break;  
  53.    
  54.         case NSStreamEventOpenCompleted:  
  55.             if (aStream == self.inputStream)  
  56.             {  
  57.                 [self addEvent:@"Connection Opened"];  
  58.             }  
  59.             break;  
  60.    
  61.         default:  
  62.             break;  
  63.     }  
  64. }  

该方法处理了连接有可能接收到的所有事件,它们中的大多数都很简单,并且很容易看明白。
NSStreamEventHasBytesAvailable 情况只会在输入流里面出现,但是它应该在任何情况下都被检查。你把buffer转为string,在新行执行trim方法,然后打印出信息。
接下来的部分很有趣,如果事件是notify”,你设计了一个本地的通知。在一个真正的VoIP应用里面,它将对应是有电话打进来。因此无论应用是在前后台,这个都会执行。
如果命令是“ping”,你需要发送一个“pong”给服务器。
当输出字节有空间发送数据时,你就调用NSStreamEventHasSpaceAvailable 方法。当且仅当你第一次接收到这个时,你发送ping给服务器。
为了能够使应用正常运行,你当然需要添加 App provides Voice over IP 到后台的plist:


在你运行应用之前你需要一个服务器来测试它,你可以使用一个叫做 netcat 的小工具,它允许你 创建一个简单的服务器,打开终端(Applications/Utilities/Terminal.app app)会话,输入如下命令:
[objc] view plaincopy
  1. nc -l 10000  
这行命令在10000端口上开了一个应用,来监听连接,现在回到xcode,运行应用:


如果你在使用模拟器,你就设置ip为127.0.0.1,如果你是在设备上测试,你就需要找到你的mac电脑在网络中的ip,并且把你的ip设置成它。
现在点击连接,当连接建立起来后你就可以在控制台看到ping信息。
在你的终端控制台打ping然后点击回车,你将看到丛应用里面发送来的pong,如果你不是发送ping,而是其他的东西,你就接收不到任何回馈。


接下来,试一下notify 命令,你需要在另一台设备上面运行,而不是在模拟器上面,因为通知在模拟器里面并不起作用。
如果你点击了home键,并且在终端发送了ping命令,你将仍然会接收到pong的返回,如果是在设备上运行,你将收到推送消息:



这是第五种后台模式,号称最强大的,明智的使用它吧!

接下来学习什么

你可以下载本教程的所有源代码:
如果你想阅读我们上面讲到的更多的官方文档,请点击Background Execution and Multitasking,这歌文档解释了所有的后台模式,并且每一种后台模式都有文档链接。
本文讲到的一个特别感兴趣的东西就是being a responsible background app,如果你想发布一个后台应用到appstore,这里或多或少有一些东西你会感兴趣。
我希望你能喜欢本教程,现在你可以根据你的需求,选择某种后台模式来完成自己的应用了。
记住工程的源代码地址:http://download.csdn.net/detail/dongtaochen2039/8274921下载下来吧。
0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:23130次
    • 积分:551
    • 等级:
    • 排名:千里之外
    • 原创:28篇
    • 转载:15篇
    • 译文:2篇
    • 评论:1条
    文章分类
    最新评论