【译】iOS中的程序后台模式

英文原版链接:点击跳转


从iOS4开始,当用户按下Home键时,app会在内存中被挂起暂停住,即使app在内存里,它的所有操作也会被暂停,只有等到用户再次使用它时才会被重新恢复运行。那么app在进入后台时可以进行其它操作吗?

当然可以!在某些条件下,app仍然可以在后台执行额外操作。这篇教程将会完整的教你怎样使用以及何时使用这些后台操作。

iOS对后台模式的操作有非常严格的限制,在iOS上实现多任务运行还没有没有完美的解决方案。当用户切换到其它app时,大多数当前的app仍然会被完全挂起暂停。你的app只能在特定的情况下可以在后台保持运行状态:播放声音、获取实时定位更新、下载新闻报刊、VOIP网络电话。

如果上面这些特定情况你的app里都不是需要的,那只能说你不幸运了。但还有一个例外:所有的app在真正被挂起之前,可以有10分钟的时间去完成它们要做的。

所以后台模式可能不适合你,但是如果适合的话,继续读下去吧!

你很快会学到,iOS中有五种基本的后台模式可以让你使用。在本篇教程中,你会创建一个简单的Tab风格的应用程序项目,每个子tab都会演示一种后台模式的特性,从持续后台播放声音到网络电话(VOIP)的来电连接监听。

让我们开始吧!

开始动手

在深入研究项目之前,让我们快速浏览下,iOS中的五种基本后台模式都有哪些:

本教程会分段讲述上面五种后台模式,如果你只是对其中的一个模式感兴趣,那就直接快速跳过去看你感兴趣的模式吧!

在开始学习后台模式之前,先下载项目样例。有一个好消息告诉你:用户交互界面已经为你配置好了。

pic

你也可以GitHub上访问这个项目。项目仓库包含了创建UI的步骤,尽管这篇教程的着重点是在后台模式上。

运行项目并查看你的五个Tab运行情况。

pic

这些tab是你学习本教程剩余部分的地图指南。首先从后台播放声音开始。

温馨提示:为了完整的演示效果,最好在真机上进行测试。有些后台模式在模拟器上运行可能会不正常。

一、播放/记录声音

在iOS里有很多种播放声音的方式,其中大多数方式都需要实现回调去提供播放的声音数据。所谓的回调就是iOS需要你App所做的事(类似delegate方法),在这个例子里,是将声音波形数据填充到内存缓冲区里。

如果你想从数据流中播放声音,你可以开启一个网络连接,在连接回调函数中提供持续的声音数据。

当你激活了声音后台播放模式,iOS会在你app不是当前活动app的情况下,继续调用声音的回调函数。在这五种后台播放模式中,有四种都是跟这个类似的。声音的后台模式是自动的。你只需要激活并提供简单的方法去合适的处理它。

对上面的播放声音情况,你可能有一个坏坏的想法。记住你只能在你app确实需要播放声音的时候去使用后台声音模式。如果你想要试着通过后台去播放无声音乐来获取CPU时间。苹果会拒绝你的app的!

pic

在本节,你会在app里添加audio player,开启后台模式,自己动手演示论证它是可行有效的。

为了获取声音回调,你需要导入AVFoundation库。打开TBFirstViewController.m文件,并且在文件顶部加入头文件依赖。

#import <AVFoundation/AVFoundation.h>

现在,找到viewDidiLoad方法并且增加下面的代码在该方法尾部:

// Set AVAudioSession
NSError *sessionError = nil;
[[AVAudioSession sharedInstance] setDelegate:self];
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&sessionError];

// Change the default output audio route
UInt32 doChangeDefaultRoute = 1;
AudioSessionSetProperty(kAudioSessionProperty_OverrideCategoryDefaultToSpeaker,
sizeof(doChangeDefaultRoute), &doChangeDefaultRoute);

初始化audio session确保声音的播放时从扬声器出来的,而不是手机听筒里。

你还需要一些成员变量去保存播放记录,这里插入2个属性声明。

@property (nonatomic, strong) AVQueuePlayer *player;
@property (nonatomic, strong) id timeObserver;

将它们放到下面代码的中间:

@interface TBFirstViewController ()
 
// Insert code here
 
@end

第一个项目包含的声音文件是来自我最喜欢的免版税音乐网站:incompetech.com。你只要信用授权,就可以在incompetech.com上免费使用所有kevin MacLeod的歌曲。谢谢你,Kevin!

在iOS平台上播放音乐最简单的方式就是使用AVFoundation库里的AVPlayer。在本教程中,你会使用AVPlayer的子类AVQueuePlayer。在AVQueuePlayer中,你可以设置AVPlayerItems的队列,这样就会一首接一首的自动被播放。

在viewDidLoad方法后面继续加上代码:

NSArray *queue = @[
    [AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"IronBacon" withExtension:@"mp3"]],
    [AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"FeelinGood" withExtension:@"mp3"]],
    [AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"WhatYouWant" withExtension:@"mp3"]]
    ];
 
self.player = [[AVQueuePlayer alloc] initWithItems:queue];
self.player.actionAtItemEnd = AVPlayerActionAtItemEndAdvance;

这个创建了一个AVPlayerItem的对象数组,然后用这个数组去创建AVQueuePlayer。此外,将这个数组设置为一首接着一首的播放。

为了在歌曲队列切换时及时更新歌曲名称,你需要观察player的currentItem属性。为了做这个,需要在ViewDidLoad方法后面加上这些:

[self.player addObserver:self
              forKeyPath:@"currentItem"
                 options:NSKeyValueObservingOptionNew
                 context:nil];

这样无论何时,只要player的currentItem属性发生改变,这个类的观察回调方法就会被调用。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"currentItem"])
    {
        AVPlayerItem *item = ((AVPlayer *)object).currentItem;
        self.lblMusicName.text = ((AVURLAsset*)item.asset).URL.pathComponents.lastObject;
        NSLog(@"New music name: %@", self.lblMusicName.text);
    }
}

当观察方法得到调用后,你首先需要确定,发生改变的属性是你感兴趣的。在这里,这一步并不是必须的,因为这里只有一个属性被观察了,但是这是个好的练习如果你稍后添加更多的观察属性。如果它是currentItem属性,那么你就可以更新lblMusicName Label的文件名称。

你还需要给当前正在播放的声音增加一个表示时间进度的Label。最简单的方式是使用addPeriodicTimeObserverForInterval:queue:usingBlock:方法,该方法会在给定的queue里面,调用上面方法里提供的block。

将下面代码加入viewDidLoad方法:

void (^observerBlock)(CMTime time) = ^(CMTime time) {
        NSString *timeString = [NSString stringWithFormat:@"%02.2f", (float)time.value / (float)time.timescale];
        if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive) {
            self.lblMusicTime.text = timeString;
        } else {
            NSLog(@"App is backgrounded. Time is: %@", timeString);
        }
    };
 
self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(10, 1000)
                                                              queue:dispatch_get_main_queue()
                                                         usingBlock:observerBlock];

你首先需要创建根据时间改变而调用的block。如果你不知道如何使用blocks(你应该要会用,因为它非常的酷!),就阅读iOS5教程里面的《如何使用Blocks》文章,这里的block基于你的应用程序当前状态,然后创建字符串来根据歌曲时间的改变而发生改变。

当调用- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block方法后,就可以启动更新操作了。

让我们在这里暂停一下,讨论一些关于程序状态的东西。

你的程序可以有5种状态,简要列举:

  1. 未运行状态:程序没有启动前的状态。

  2. 活动状态:当你的程序被启动,它就处于活跃状态。

  3. 不活动状态:当你的程序正在运行时被某些事务中断运行,比如电话来电时它就会被转换成不活动状态。不活动状态是指程序仍然在前台运行,不过它不能接收响应事件。

  4. 后台状态:这种状态下,你的程序已经不再是处于前台了,但是它仍然可以运行代码。

  5. 暂停挂起状态:程序进入这个状态后,它就不能再执行代码了。

如果你想彻底这些状态之间的差别,在苹果网站上有非常详细的关于程序状态和多线程的文档解释。

你可以通过调用[[UIApplication sharedApplication] applicationState]来查看你的程序状态。请注意,你只能获取三种状态的返回:UIApplicationStateActiveUIApplicationStateInactiveUIApplicationStateBackground。暂停挂起状态和未运行状态显然不会发生在你的程序运行期间,所以这里没有对它们做状态值定义。

让我们回到代码中。如果程序是处在活动状态,你需要更新音乐的标题label。如果不是,你只需要在控制台输入更新信息。你仍然可以在程序后台模式时更新label的文字,但是这里有一点需要指出,你的程序在进入后台模式后,仍然可以接收上面的回调callback。

现在剩下来需要做的就是增加didTapPlayPause的接口,让play/pause按钮正常工作。在TBFirstViewController.m的底部,也就是@end的上方增加一个新方法。

- (IBAction)didTapPlayPause:(id)sender
{
    self.btnPlayPause.selected = !self.btnPlayPause.selected;
    if (self.btnPlayPause.selected)
    {
        [self.player play];
    }
    else
    {
        [self.player pause];
    }
}

好极了,所有代码已经准备完毕,构建Build和运行代码,你将会看到这个:

pic

现在按下播放按钮让音乐播放起来。棒极了!

让我们测试下后台模式是否正常工作。按下home按钮(如果你使用的是模拟器,按下组合键(Cmd-Shift-H),呃!为什么音乐停下来了?好吧,这里还有一个非常重要的步骤没有设置!)

对大多数后台模式来说(Whatever模式是一个例外),你需要在Info.plist里添加键值去声明你的程序在进入后台后仍然可以执行代码。

回到Xcode中,按以下方式设置:

  1. 在File Navigator面板上点击项目。

  2. 点击Info标签。

  3. 在列表的行中点击(+)按钮。

  4. 在出现的列表中选择Required Background Modes

pic

当你选了这项,XCode会在这项下面创建一个数组。在右边展开列表并选择App plays audio.在这个列表中,你可以看到本篇教程介绍的所有的后台模式 - 也有小部分的模式需要使用特殊的硬件去支持(蓝牙)。

pic

再次构建编译运行。启动音乐播放,点击home按钮,而这一次,你应该会继续听到音乐的播放,尽管这个程序已经进入后台了。

如果这对你仍然不管用,可能是因为你使用的是模拟器。在真机设备上测试,应该是可以正常工作的。你也可以看到XCode控制台上时间的更新输出。证明了尽管程序进入了后台,你的代码仍然在运行着。

第一种后台模式结束,如果你准备学习完整个教程 - 还有四种去学习!

GitHub仓库对这个教程的标记是BackgroundMusic


二、实时位置更新

在位置后台模式中,你的程序即使进入了后台,也可以更新用户位置,并接收位置的delegate的消息。你可以控制定位的精度,甚至可以在程序在后台状态下改变定位精度。

再次说明,你只能在你的app确实需要使用后台模式来给用户提供数据服务时使用。如果你使用了后台模式,而苹果发现用户并没有从中获得任何有用的帮助,你的app会被拒绝。有时苹果会要求你给用户增加一个警告描述,来说明你的app将会导致耗电量增加。

第二个tab是关于位置更新的,打开TBSecondViewController.m并且增加一些属性生命:

@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) NSMutableArray *locations;

这些属性添加在文件顶部的下列行之间:

@interface TBSecondViewController ()
 
// add code here
 
@end

CLLocationManager是你用来获取设备位置更新的类。你会使用locations数组去存储位置信息,并将它们在地图上标记出来。

现在在viewDidLoad函数底部加上下面代码:

self.locations = [[NSMutableArray alloc] init];
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.locationManager.delegate = self;

这里创建了一个空的可变数组去存储位置更新数据,然后创建了CLLocationManager对象。代码设置了定位精度为最高精度,你可以在APP里根据需要调整这个定位精度。马上你会学到更多其它的精度设置和它们的重要性。

现在你可以实现accuracyChanged:方法:

- (IBAction)accuracyChanged:(id)sender
{
    const CLLocationAccuracy accuracyValues[] = {
        kCLLocationAccuracyBestForNavigation,
        kCLLocationAccuracyBest,
        kCLLocationAccuracyNearestTenMeters,
        kCLLocationAccuracyHundredMeters,
        kCLLocationAccuracyKilometer,
        kCLLocationAccuracyThreeKilometers};
 
    self.locationManager.desiredAccuracy = accuracyValues[self.segmentAccuracy.selectedSegmentIndex];
}

accuracyValues数组包含所有CLLocationManager的精度属性值。这个属性决定了你想要的定位精度。

你也许认为这样做很愚蠢。为什么定位不一直提供给你尽可能最高的精确度呢?理由在这里:降低耗电量。较低的精确度意味着更少的耗电量。

所以,如果你的app不需要太高的精确度,你应该选择最低的精确度来满足你的需求,你可以在任何时候改变这个值。

另外还有一个属性可以用来控制你的app接收位置更新的频率,不论desiredAccuracy: distanceFilter的返回值是多少。这个属性的设置会让定位类对象知道,你仅仅是希望在设备移动一定范围后才接收新的位置更新。

再次申明,这个值应该竟可能的高,因为这样可以节省电量。

现在,你可以在enabledStateChanged:方法里添加代码去启动定位更新:

- (IBAction)enabledStateChanged:(id)sender
{
    if (self.switchEnabled.on)
    {
        [self.locationManager startUpdatingLocation];
    }
    else
    {
        [self.locationManager stopUpdatingLocation];
    }
}

Xib文件有一个UISwitch控件,并绑定了switchEnabled接口方法,用来控制定位的开关。接下来你需要添加CLLocationManagerDelegate方法去接收位置的更新。在文件的尾部,@end关键字的前面增加下面的代码:

#pragma mark - CLLocationManagerDelegate
 
/*
 *  locationManager:didUpdateToLocation:fromLocation:
 *
 *  Discussion:
 *    Invoked when a new location is available. oldLocation may be nil if there is no previous location
 *    available.
 *
 *    This method is deprecated. If locationManager:didUpdateLocations: is
 *    implemented, this method will not be called.
 */
- (void)locationManager:(CLLocationManager *)manager
    didUpdateToLocation:(CLLocation *)newLocation
           fromLocation:(CLLocation *)oldLocation
{
    // Add another annotation to the map.
    MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];
    annotation.coordinate = newLocation.coordinate;
    [self.map addAnnotation:annotation];
 
    // Also add to our map so we can remove old values later
    [self.locations addObject:annotation];
 
    // Remove values if the array is too big
    while (self.locations.count > 100)
    {
        annotation = [self.locations objectAtIndex:0];
        [self.locations removeObjectAtIndex:0];
 
        // Also remove from the map
        [self.map removeAnnotation:annotation];
    }
 
    if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
    {
        // determine the region the points span so we can update our map's zoom.
        double maxLat = -91;
        double minLat =  91;
        double maxLon = -181;
        double minLon =  181;
 
        for (MKPointAnnotation *annotation in self.locations)
        {
            CLLocationCoordinate2D coordinate = annotation.coordinate;
 
            if (coordinate.latitude > maxLat)
                maxLat = coordinate.latitude;
            if (coordinate.latitude < minLat)
                minLat = coordinate.latitude;
 
            if (coordinate.longitude > maxLon)
                maxLon = coordinate.longitude;
            if (coordinate.longitude < minLon)
                minLon = coordinate.longitude;
        }
 
        MKCoordinateRegion region;
        region.span.latitudeDelta  = (maxLat +  90) - (minLat +  90);
        region.span.longitudeDelta = (maxLon + 180) - (minLon + 180);
 
        // the center point is the average of the max and mins
        region.center.latitude  = minLat + region.span.latitudeDelta / 2;
        region.center.longitude = minLon + region.span.longitudeDelta / 2;
 
        // Set the region of the map.
        [self.map setRegion:region animated:YES];
    }
    else
    {
        NSLog(@"App is backgrounded. New location is %@", newLocation);
    }
}

上面的定位方法代码块可真庞大。本教程的重点不是Core Location。所以我们不必太关注上面的代码 - 其实上面代码的注释也是非常多的。如果app是active活跃状态,那么代码会更新地图。如果app是在后台,你应该查看XCode控制台的位置更新日志的输出。

现在你已经知道info.plist设置后台模式的方法,你不必再犯之前的错误了。向数组中再添加一行App registers for location updates,告知iOS你的app会在后台持续定位更新。

pic

编译运行!切换到第二个Tab页面,将switch按钮打开到on状态。

pic

当你首次打开应用,你应该会看到一个标准弹窗,询问你是否允许app访问你的位置。点击OK并在室外或者建筑周围走走。你应该会看到位置的更新,甚至在模拟器上也是可以的。

过一会,你应该看到类似下面这样的效果:

pic

如果你把app切换到后台,你在控制台上应该会看到位置更新的日志。再次从后台打开app,你会看到地图上所有的位置标记大头针都被更新了。

如果你使用模拟器,你可以使用它的模拟移动功能,在Debug菜单中找到它:

pic

设置定位的方式为Freeway Drive选项,然后按下home按钮。你会看到控制台在不停的打印出模拟驾驶在加尼福尼亚高速上的位置更新日志。

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
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
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
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

GitHub仓库中关于本节教程的代码:BackgroundLocation

接下来让我们继续看第三个Tab和第三种后台模式。


三、执行有限时长的任务(或whatever情况)

下一个后台模式,官方称呼为:Executing a Finite-Length Task in the Background。太口语话的表达,我觉得用Whatever来表达更贴切。

准确的说,这完全不是后台模式,你不必在info.plist文件中去申明你的app使用后台模式。它是一个可以让你app在后台时,获取有限的时间去执行任何代码的API。嗯…Whatever的代码都可以。

你可以使用这个模式在后台完成上传或者下载。假设我们正在编写一个图片分享类app。如果用户选择好照片并上传,但是立刻把app切入了后台,这样就可能没有足够的时间去上传这些图片到你的服务器上去。使用这个API,你可以获取额外的CPU时间去完成上传,让你的用户可以确信有一张或多张图片安全的上传到了服务器,这样会让用户体验更好。

pic

这里只是举了个简单的例子。实际上你可以在API里执行任意的代码:执行耗时的计算(本节的示例),图片增加滤镜,渲染复杂的3D模型…whatever!你的想象力是有限的,但是有点需要注意,你获取的后台时间是有限的,不是无限制时长的后台。

你的app进入后台可以拥有多长时间是iOS系统决定的。这里没有对你获取多长后台时间的保证,但是你可以不断查看UIApplication类的backgroundTimeRemaining属性,这个属性会告诉你,你还剩多少时间可以执行。

通过大数据观察得知,通常情况下你会有十分钟的后台执行时间。再次申明,这里没有任何的保证,并且API文档都没有给出一个大概的时间,所以不能依赖这十分钟的数字,你也许可以有五分钟或者五秒,所以你的app需要为此做好一切准备。

对每一个计算机专业的学生来说,这里有一个普遍类似的类似的任务:计算斐波拉契数列

这里你将会在后台去计算这些数字!

打开TBThirdViewController.m文件,增加下来属性:

@property (nonatomic, strong) NSDecimalNumber *previous;
@property (nonatomic, strong) NSDecimalNumber *current;
@property (nonatomic) NSUInteger position;
@property (nonatomic, strong) NSTimer *updateTimer;
@property (nonatomic) UIBackgroundTaskIdentifier backgroundTask;

上面的代码需要加在该文件的顶部,也就是下面的代码中间:

@interface TBThirdViewController ()
 
// add code here
 
@end

NSDecimalNumbers的属性会记录数列中前面的两个值。NSDecimalNumber是一个可以存储非常大数字的类。这个刚好满足你的需求。position是用来记录当前数字在数列中的位置。

你会使用updateTimer去证明在使用这个API时,定时器依然工作着,同样也可以一定程度的减慢计算方便你观察。

找到viewDidLoad方法,并把下面的代码添加在最后:

self.backgroundTask = UIBackgroundTaskInvalid;

现在重要的部分来了,先看didTapPlayPause:方法的实现:

- (IBAction)didTapPlayPause:(id)sender
{
    self.btnPlayPause.selected = !self.btnPlayPause.selected;
    if (self.btnPlayPause.selected)
    {
        self.previous = [NSDecimalNumber one];
        self.current  = [NSDecimalNumber one];
        self.position = 1;
 
        self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.5
                                                            target:self
                                                          selector:@selector(calculateNextNumber)
                                                          userInfo:nil
                                                           repeats:YES];
 
        self.backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
            NSLog(@"Background handler called. Not running background tasks anymore.");
            [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
            self.backgroundTask = UIBackgroundTaskInvalid;
        }];
    }
    else
    {
        [self.updateTimer invalidate];
        self.updateTimer = nil;
        if (self.backgroundTask != UIBackgroundTaskInvalid)
        {
            [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
            self.backgroundTask = UIBackgroundTaskInvalid;
        }
    }
}

让我们通过代码来看看如何使用这个API。

这按钮会切换它的选中状态来控制标识计算器的启动与停止。

首先,你需要设置斐波拉契数列的变量。然后创建NSTimer对象去每秒两次的调用calculateNextNumber方法。

现在重点来了:beginBackgroundTaskWithExpirationHandler:。这个方法告诉iOS你需要更多的时间去完成某项任务,无论是什么任务(whatever)。调用这个方法过后,如果你的app进入后台后,它仍然会占有CPU时间直到你调用endBackgroundTask:方法。

其实,大多数情况下,如果app进入后台一段时间而你未调用endBackgroundTask:方法,iOS会调用beginBackgroundTaskWithExpirationHandler:方法的block块,这将给你一次停下正在执行的代码的机会。所以当你任务完成后,最好要调用endBackgroundTask:方法去告知iOS。如果你不这样做,并且继续执行你的代码,你的app将会被强制关闭。

if语句的else部分很简单。它只是释放了定时器并调用了- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)identifier方法去告知iOS你不需要额外的CPU时间了。

这是非常重要的,就是在每次调用beginBackgroundTaskWithExpirationHandler:方法后,必须要调用endBackgroundTask:方法。如果你对两个任务调用了两次beginBackgroundTaskWithExpirationHandler:方法,但是只对第一个任务调用了一次endBackgroundTask:方法,你仍然会占用CPU时间直到你第二次调用endBackgroundTask:方法去结束第二个任务。这就是为什么你需要backgroundTask变量的原因。

现在你可以实现具体的计算逻辑,将下面代码添加到文件尾部,但是在@end的前面:

- (void)calculateNextNumber
{
    NSDecimalNumber *result = [self.current decimalNumberByAdding:self.previous];
 
    if ([result compare:[NSDecimalNumber decimalNumberWithMantissa:1 exponent:40 isNegative:NO]] == NSOrderedAscending)
    {
        self.previous = self.current;
        self.current  = result;
        self.position++;
    }
    else
    {
        // This is just too much.... Let's start over.
        self.previous = [NSDecimalNumber one];
        self.current  = [NSDecimalNumber one];
        self.position = 1;
    }
 
    NSString *currentResultLabel = [NSString stringWithFormat:@"Position %d = %@", self.position, self.current];
    if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
    {
        self.txtResult.text = currentResultLabel;
    }
    else
    {
        NSLog(@"App is backgrounded. Next number = %@", currentResultLabel);
        NSLog(@"Background time remaining = %.1f seconds", [UIApplication sharedApplication].backgroundTimeRemaining);
    }
}

再一次重申,这里使用了应用程序状态的技巧来表明即使app进入后台依然可以显示结果。在这个例子里,还有一个有趣的信息:属性backgroundTimeRemaining的值。iOS调用beginBackgroundTaskWithExpirationHandler:方法的block才会把这个计算器给停下来。

编译运行,切换到第三个tab上。

pic

点击Start按钮,你将会看到app正在计算结果。现在按下home键,查看Xcode控制台的输出,你会看到随着可执行时间的减少,你的程序仍然在更新着数字结果。

在大多数的例子中,这个时间会从600开始(600秒 = 10分钟),并且减少到的5秒。如果你在等待着时间的减少到5秒时(也可能是其它的时间值,取决于你的特定情况),这个超时block块会被调用,并且你的app停止输出任何信息。这时,如果你回到app中,这个timer定时器会再一次启动执行,然后程序进入彻底疯狂状态。

在这个代码里只有一个Bug,而这也给我机会去解释后台的消息通知。假设你让app进入后台,并一直等待着让分配的后台时间结束。在本例中,你的app会调用ExpirationHandler的block块,并执行endBackgroundTask:方法,因此结束了后台的时间需求。

如果你再次回到app中,timer定时器会继续启动执行。但是如果你再次将app退到后台,你再也无法获得后台的可执行时间了。为什么?因为没有任何地方再次调用了beginBackgroundTaskWithExpirationHandler:方法。

如何解决这个问题?这里有很多的方法去解决,其中一种是使用程序状态改变通知来解决。

当你的app改变了状态,这里有两种方式去获取通知。第一种就是通过你的app delegate方法。第二种是监听iOS发送给你app中的通知:

你可以在苹果官方文档App States and Multitasking中详细的查看(有很多精美的流程图)。

下一段会涉及这些通知。在真正的计算机教育中,解决beginBackgroundTaskWithExpirationHandler的Bug就留给读者去解决,作为一个练习。

GitHub仓库里关于本教程的代码:BackgroundWhatever


四、处理报刊杂志的下载

五、提供网络电话的服务(VoIP)

最后四、五两种后台模式方式已经在当下的开发中已经不是特别的通用与实用,故决定暂时不翻译。时间应该用在眼下迫切的事上。如有需要,可以阅读原文,原文链接在本文最顶端已经给出。

文章来自 http://skymonkey.cn/

高能广告区

暂无广告哦=^^=。继续看看其它文章吧!