第21小时:构建支持后台运行的应用程序






4.87/5 (9投票s)
在本章摘录自《Sams Teach Yourself iPhone Application Development in 24 Hours》中,我们将探讨:iOS 4如何支持后台任务、支持哪些类型的后台任务、如何禁用后台运行、如何暂停应用程序以及如何在后台执行代码。
约翰·雷 由Sams出版 ISBN-10:0-672-33220-5 ISBN-13:978-0-672-33220-3 |
本小时您将学到什么
- iOS 4如何支持后台任务
- 支持哪些类型的后台任务
- 如何禁用后台运行
- 如何暂停应用程序
- 如何在后台执行代码
“在后台运行多个应用程序的能力”嘲讽了 Verizon 的广告。“为什么一个现代操作系统不能同时运行多个程序?”讨论组里有人问道。作为一名开发者和 iPhone 的粉丝,我发现这些帖子因其天真而有趣,但也有些令人困惑。iPhone 一直都在后台同时运行多个应用程序,但它们仅限于 Apple 的应用程序。此限制是为了保留设备作为手机的用户体验。Apple 并没有采取“任意行事”的方法,而是采取措施确保手机始终保持响应。
随着 iOS 4.x 的发布,Apple 通过向第三方应用程序开放后台处理来回应竞争对手的呼声。然而,与竞争对手不同,Apple 在处理后台运行方面一直很谨慎——将其开放给用户经常遇到的特定任务集。在本小时的课程中,您将学习可以在 iOS 4 中实现的几种多任务处理技术。
了解 iOS 4 后台运行
如果您在构建本书中的教程时一直在使用 iOS 4.x 或更高版本,您可能已经注意到,当您在手机或 iPhone 模拟器中退出应用程序时,它们仍然会显示在 iOS 任务管理器中,除非您手动停止它们,否则它们往往会从上次离开的地方继续运行。这是因为在 iOS 4.x 中创建的项目在您点击“构建并运行”按钮后就已具备后台运行能力。这并不意味着它们会在后台运行,只是它们知道新的 iOS 4 后台功能,并且会在稍加帮助下加以利用。
在我们探讨如何在项目中启用后台运行(也称为多任务处理)之前,我们首先要准确地了解什么是后台感知应用程序,从支持的后台运行类型开始,然后是应用程序生命周期方法。
后台运行的类型
我们将在 iOS 4.x 中探讨四种主要的后台运行类型:应用程序暂停、本地通知、任务特定后台处理和任务完成。
暂停
当一个应用程序被暂停时,它将停止执行代码,但会完全保留用户离开时的状态。当用户返回应用程序时,它看起来好像一直在运行。实际上,所有任务都将停止,从而防止应用程序占用您的 iPhone 资源。任何针对 iOS 4.x 编译的应用程序都将默认支持后台暂停。如果应用程序即将暂停,您仍然应该在其中处理清理工作(请参阅本章后面的“后台感知应用程序生命周期”部分),但除此之外,它“即插即用”。
除了在应用程序暂停时执行清理工作之外,您还有责任从后台暂停状态中恢复,并更新应用程序中在暂停期间应该更改的任何内容(例如时间/日期更改)。
本地通知
第二种后台处理类型是本地通知的调度 (UILocalNotification
)。如果您曾经体验过推送通知,那么本地通知与此相同,但它们是由您编写的应用程序生成的。应用程序在运行时可以调度通知,使其在未来的某个时间点显示在屏幕上。例如,以下代码初始化一个通知 (UILocalNotification
),将其配置为在五分钟后显示,然后使用应用程序的 scheduleLocalNotification
方法完成调度
UILocalNotification *futureAlert;
futureAlert = [[[UILocalNotification alloc] init] autorelease];
futureAlert.fireDate = [NSDatedateWithTimeIntervalSinceNow:300];
futureAlert.timeZone = [NSTimeZonedefaultTimeZone];
[[UIApplication sharedApplication] scheduleLocalNotification:futureAlert];
这些通知在由 iOS 调用时,可以显示消息、播放声音,甚至更新应用程序的通知徽章。但是,它们无法执行任意应用程序代码。实际上,您很可能会在注册本地通知后简单地允许 iOS 暂停您的应用程序。收到通知的用户可以点击通知窗口中的“查看”按钮返回您的应用程序。
任务特定后台处理
在 Apple 决定实现后台处理之前,他们对用户如何使用手持设备进行了一些研究。他们发现,用户需要特定类型的后台处理。首先,他们需要音频在后台继续播放;这对于 Pandora 等应用程序是必需的。其次,支持位置的软件需要在后台更新自身,以便用户继续接收导航反馈。最后,Skype 等 VoIP 应用程序需要在后台运行以处理来电。
这三种类型的任务在 iOS 4.x 中以独特而优雅的方式处理。通过声明您的应用程序需要其中一种后台处理类型,您可以在许多情况下,在很少改动的情况下使您的应用程序继续运行。要声明您的应用程序能够支持任何(或所有)这些任务,您需要将“Required Background Modes”(UIBackgroundModes
) 键添加到项目的 plist 文件中,然后添加“App Plays Audio”(音频)、“App Registers for Location Updates”(位置) 或“App Provides Voice over IP Services”(VoIP) 的值。
长时间运行任务的任务完成
我们将在 iOS 4.x 中使用的第四种后台处理类型是任务完成。使用任务完成方法,您可以“标记”应用程序中需要完成才能安全暂停的任务(文件上传/下载、大量计算等)。
例如,要标记一个长时间运行任务的开始,首先声明该特定任务的标识符
UIBackgroundTaskIdentifier myLongTask;
然后使用应用程序的 `beginBackgroundTaskWithExpirationHandler` 方法告诉 iOS 您正在启动一段可以在后台继续运行的代码
myLongTask = [[UIApplicationsharedApplication] beginBackgroundTaskWithExpirationHandler:^{ // If you’re worried about exceeding 10 minutes, handle it here }];
最后,使用应用程序的 `endBackgroundTask` 方法标记长时间运行任务的结束
[[UIApplication sharedApplication] endBackgroundTask:myLongTask];
您标记的每个任务总共将有大约 10 分钟的时间来完成其操作,这对于大多数常见用途来说是足够的时间。时间结束后,应用程序将被暂停,并像其他暂停的应用程序一样处理。
后台感知应用程序生命周期方法
在第 4 小时“深入 Cocoa Touch”中,您开始学习应用程序生命周期,如图 21.1 所示。您了解到,在 iOS 4.x 中,应用程序应在 `applicationDidEnterBackground` 委托方法中自行清理。这取代了早期 OS 版本中的 `applicationWillTerminate`,或者正如您稍后将了解到的,在您明确标记为无法(或不需要)在后台运行的应用程序中。
除了 `applicationDidEnterBackground` 之外,您还应该实现其他几个方法,以便成为一个合格的后台感知 iOS 应用程序。对于许多小型应用程序,您无需对它们做任何事情,只需将它们保留在应用程序委托中即可。但是,随着您的项目复杂性增加,您将需要确保您的应用程序在前台和后台之间干净地切换(反之亦然),避免潜在的数据损坏并创建无缝的用户体验。
注意 - 了解这一点很重要,如果 iOS 认为设备资源不足,即使您的应用程序处于后台,它也可能终止您的应用程序。您可以预期您的应用程序会正常运行,但要为它们可能意外强制退出的情况做好计划。
Apple 要求在您的后台感知应用程序中包含以下方法
application:didFinishLaunchingWithOptions
:当您的应用程序首次启动时调用。如果您的应用程序在暂停时终止或从内存中清除,则需要手动恢复其先前的状态。(您确实已将其保存到用户的偏好设置中,对吧?)applicationDidBecomeActive
:当应用程序启动或从后台返回前台时调用。此方法可用于重新启动进程并在需要时更新用户界面。applicationWillResignActive
:当应用程序被请求移至后台或退出时调用。如果需要,此方法应用于准备应用程序进入后台状态。applicationDidEnterBackground
:当应用程序成为后台应用程序时调用。这取代了 iOS 4.x 中的applicationWillTerminate
。您应该在此方法中处理所有最终清理工作。您也可以使用它来启动长时间运行的任务,并使用任务完成后台处理来完成它们。applicationWillEnterForeground
:当应用程序在后台后返回活动状态时调用。applicationWillTerminate
:当非多任务版本的 iOS 上的应用程序被要求退出,或者当 iOS 确定需要关闭正在运行的后台应用程序时调用。
所有这些方法的存根都存在于您的 iOS 4.x 应用程序委托实现文件中。如果您的应用程序需要额外的设置或拆卸工作,只需将代码添加到现有方法中即可。正如您很快就会看到的那样,许多应用程序,例如本书中的大多数应用程序,几乎不需要更改。
注意 - 本小时课程的假设是您正在使用 iOS 4.x 或更高版本。如果您不是,则在 OS 的早期版本上使用与后台相关的 方法和属性将导致错误。为了成功地同时针对 iOS 4.x 和早期设备,请检查后台运行是否可用,然后在您的应用程序中做出相应的反应。
Apple 在 iPhone 应用程序编程指南中提供了以下代码片段,用于检查(无论 OS 版本如何)多任务处理支持是否可用
UIDevice* device = [UIDevice currentDevice];
BOOL backgroundSupported = NO;
if ([device respondsToSelector:@selector(isMultitaskingSupported)])
backgroundSupported = device.multitaskingSupported;
如果生成的 backgroundSupported
布尔值为 YES
,则您可以安全地使用与后台相关的代码。
现在您已经了解了可用的后台相关方法和后台处理类型,让我们看看如何实现它们。为此,我们将重用我们贯穿本书构建的教程(只有一个例外)。我们不会介绍这些教程是如何构建的,因此如果您对应用程序的核心功能有疑问,请务必参考前面的课程。
禁用后台运行
我们从启用后台运行的完全相反开始:禁用它。如果您仔细考虑,有许多不同的“消遣”应用程序不需要支持后台暂停或处理。这些应用程序是您使用后就退出的应用程序。它们不需要在任务管理器中徘徊。
例如,考虑第 6 小时“模型-视图-控制器应用程序设计”中的 HelloNoun 应用程序。用户体验不会因为每次运行应用程序都从头开始而受到负面影响。要实现项目中的此更改,请按照以下步骤操作
- 打开要禁用后台运行的项目(例如 HelloNoun)。
- 在资源组中打开项目的 plist 文件 (HelloNoun-Info.plist)。
- 向属性列表添加一个附加键,从“键”弹出菜单中选择“应用程序不在后台运行”(
UIApplicationExitsOnSuspend
)。 - 点击键旁边的复选框,如图 21.2 所示。
- 保存对 plist 文件的更改。
在您的 iPhone 上或 iPhone 模拟器中构建并运行该应用程序。当您使用 Home 按钮退出应用程序时,它不会被暂停,也不会显示在任务管理器中,并且下次启动时它会重新开始。
处理后台暂停
在第二个教程中,我们处理后台暂停。如前所述,您无需执行任何操作即可支持此功能,只需使用 iOS 4.x 开发工具构建您的项目即可。也就是说,我们以此示例为例,在用户从后台返回应用程序时提示他们。
在本例中,我们将更新第 8 小时“处理图像、动画和滑块”中的 ImageHop 应用程序。可以想象(请大家配合我!),用户可能想要启动兔子跳,退出应用程序,然后在将来的某个时间点返回到它离开时的确切位置。
为了在应用程序从暂停状态返回时提醒用户,我们将编辑应用程序委托方法 `applicationWillEnterForeground`。请记住,此方法仅在应用程序从后台状态返回时调用。打开 `ImageHopAppDelegate.m` 并实现此方法,如清单 21.1 所示。
清单 21.1
- (void)applicationWillEnterForeground:(UIApplication *)application { UIAlertView *alertDialog; alertDialog = [[UIAlertView alloc] initWithTitle: @"Yawn!" message:@"Was I asleep?" delegate: nil cancelButtonTitle: @"Welcome Back" otherButtonTitles: nil]; [alertDialog show]; [alertDialog release]; }
在该方法中,我们声明、初始化、显示和释放一个警报视图,与第 10 小时“引起用户注意”教程中完全相同。更新代码后,构建并运行应用程序。启动 ImageHop 动画,然后使用 Home 按钮将应用程序置于后台。
等待几秒钟(以防万一),然后通过任务管理器或应用程序图标(而不是“构建并运行”!)再次打开 ImageHop。当应用程序返回前台时,它应该从上次离开的地方继续运行,并显示如图 21.3 所示的警报。
实现本地通知
在本课的早些时候,您看到了生成本地通知 (UILocalNotification
) 所需的一小段代码片段。事实证明,除了那几行代码之外,您不需要做太多其他事情!为了演示本地通知的使用,我们将更新第 10 小时“引起用户注意”的 doAlert
方法。它不仅会显示一个警报,还会在此后 5 分钟显示一个通知,然后调度本地通知每天发生。
常见通知属性
创建通知时,您需要配置几个属性。其中一些比较有趣的包括以下内容
applicationIconBadgeNumber
:当通知触发时显示在应用程序图标上的整数fireDate
:一个NSDate
对象,提供未来触发通知的时间timeZone
:用于调度通知的时区repeatInterval
:通知重复的频率,如果重复的话soundName
:一个字符串 (NSString
),包含当通知触发时要播放的声音资源的名称alertBody
:一个字符串 (NSString
),包含要显示给用户的信息
创建和调度通知
打开 GettingAttention 应用程序并编辑 doAlert
方法,使其类似于清单 21.2。(粗体行是现有方法的补充。)代码到位后,我们将一起逐步讲解。
清单 21.2
1: -(IBAction)doAlert:(id)sender { 2: UIAlertView *alertDialog; 3: UILocalNotification *scheduledAlert; 4: 5: alertDialog = [[UIAlertView alloc] 6: initWithTitle: @"Alert Button Selected" 7: message:@"I need your attention NOW (and in alittle bit)!" 8: delegate: nil 9: cancelButtonTitle: @"Ok" 10: otherButtonTitles: nil]; 11: 12: [alertDialog show]; 13: [alertDialog release]; 14: 15: 16: [[UIApplication sharedApplication] cancelAllLocalNotifications]; 17: scheduledAlert = [[[UILocalNotification alloc] init] autorelease]; 18: scheduledAlert.applicationIconBadgeNumber=1; 19: scheduledAlert.fireDate = [NSDate dateWithTimeIntervalSinceNow:300]; 20: scheduledAlert.timeZone = [NSTimeZone defaultTimeZone]; 21: scheduledAlert.repeatInterval = NSDayCalendarUnit; 22: scheduledAlert.soundName=@"soundeffect.wav"; 23: scheduledAlert.alertBody = @"I'd like to get your attention again!"; 24: 25: [[UIApplication sharedApplication] ÂscheduleLocalNotification:scheduledAlert]; 26: 27: }
首先,在第 3 行,我们将 scheduledAlert
声明为 UILocalNotification
类型的对象。这个本地通知对象就是我们用所需的消息、声音等设置好的,然后传递给应用程序,以便将来某个时间显示。
在第 16 行,我们使用 [UIApplication sharedApplication]
获取我们的应用程序对象,然后调用 UIApplication
方法 cancelAllLocalNotifications
。这将取消此应用程序可能已创建的任何先前调度的通知,为我们提供一个全新的开始。
第 17 行分配并初始化本地通知对象 scheduledAlert
。由于通知将由 iOS 而非我们的 GettingAttention 应用程序处理,我们可以使用 autorelease
来释放它。
在第 18 行,我们配置通知的 `applicationIconBadgeNumber` 属性,以便当通知被触发时,应用程序的徽章号设置为 `1`,以表示已发生通知。
第 19 行使用 `fireDate` 属性以及 `NSDate` 类方法 `dateWithTimeIntervalSinceNow` 将通知设置为在 300 秒后触发。
第 20 行设置通知的 timeZone
。这几乎总是应该设置为 [NSTimeZone defaultTimeZone]
返回的本地时区。
第 21 行设置通知的 repeatInterval
属性。这可以从各种常量中选择,例如 NSDayCalendarUnit
(每天)、NSHourCalendarUnit
(每小时)和 NSMinuteCalendarUnit
(每分钟)。完整列表可在 Xcode 开发者文档的 NSCalendar
类参考中找到。
在第 22 行,我们设置了一个声音,用于与通知一起播放。soundName
属性配置了一个字符串 (NSString
),其中包含声音资源的名称。由于我们项目中已经有 soundeffect.wav 可用,我们可以直接使用它,无需进一步添加。
第 23 行通过将通知的 `alertBody` 设置为我们希望用户看到的消息来完成通知配置。
当通知对象完全配置好后,我们使用 UIApplication
方法 scheduleLocalNotification
(第 25 行)来调度它。这样就完成了实现!
选择“构建并运行”以在您的 iPhone 或 iPhone 模拟器上编译并启动应用程序。当 GettingAttention 启动并运行后,点击“提醒我!”按钮。在显示初始警报后,点击 Home 按钮退出应用程序。去喝点东西,然后在大约 4 分 59 秒后回来。正好 5 分钟后,您将收到一个本地通知,如图 21.4 所示。
使用任务特定后台处理
到目前为止,我们还没有真正进行任何后台处理!我们暂停了一个应用程序并生成了本地通知,但在这些情况下,应用程序都没有进行任何处理。让我们改变这一点!在最后的两个示例中,我们将在应用程序处于后台时在后台执行真实代码。尽管生成一个 VoIP 应用程序超出了本书的范围,但我们可以使用上一小时课程中的 Cupertino 应用程序,稍作修改,以展示位置和音频的后台处理!
准备 Cupertino 应用程序以实现音频功能
在上一小时完成 Cupertino 应用程序时,它告诉我们 Cupertino 的距离有多远,并在屏幕上显示了直线、向左和向右的箭头,以指示用户应该朝哪个方向行驶才能到达母舰。我们可以使用 `SystemSoundServices` 更新应用程序以实现音频功能,就像我们在第 10 小时 GettingAttention 应用程序中所做的那样。
我们修改的唯一棘手之处在于,我们不希望听到与我们上次听到的声音相同的声音重复播放。为了满足这个要求,我们将为每种声音使用一个常量:1 表示直线,2 表示向右,3 表示向左,并在每次播放声音时将其存储在一个名为 `lastSound` 的变量中。然后,我们可以将其作为比较点,以确保我们即将播放的不是刚刚播放过的声音!
添加 AudioToolbox 框架
要使用系统声音服务,我们首先需要添加 AudioToolbox 框架。在 Xcode 中打开 Cupertino(带指南针实现)项目。右键单击“Frameworks”组,然后选择“Add”、“Existing Frameworks”。从出现的列表中选择 AudioToolbox.framework,然后单击“Add”,如图 21.5 所示。
添加音频文件
在本小时课程附带的 Cupertino 音频指南针 - 导航和音频文件夹中,您会找到一个 Audio 文件夹。将音频文件夹中的文件(straight.wav、right.wav 和 left.wav)拖到 Xcode 项目中的 Resources 组。在提示时选择将文件复制到应用程序中,如图 21.6 所示。
更新 CupertinoViewController.h 接口文件
现在已将必要的文件添加到项目中,我们需要更新 CupertinoViewController 接口文件。添加 `#import` 指令以导入 AudioToolbox 接口文件,然后声明三个 SystemSoundID
(soundStraight
、soundLeft
和 soundRight
)的实例变量以及一个整数 lastSound
,用于保存我们播放的最后一个声音。请记住,这些不是对象,因此无需将变量声明为指向对象的指针,为它们添加属性或释放它们!
更新后的 CupertinoViewController.h 文件应类似于清单 21.3。
清单 21.3
#import <UIKit/UIKit.h> #import <CoreLocation/CoreLocation.h> #import <AudioToolbox/AudioToolbox.h> @interface CupertinoViewController : UIViewController <CLLocationManagerDelegate> { CLLocationManager *locMan; CLLocation *recentLocation; IBOutlet UILabel *distanceLabel; IBOutlet UIView *distanceView; IBOutlet UIView *waitView; IBOutlet UIImageView *directionArrow; SystemSoundID soundStraight; SystemSoundID soundRight; SystemSoundID soundLeft; int lastSound; } @property (assign, nonatomic) CLLocationManager *locMan; @property (retain, nonatomic) CLLocation *recentLocation; @property (retain, nonatomic) UILabel *distanceLabel; @property (retain, nonatomic) UIView *distanceView; @property (retain, nonatomic) UIView *waitView; @property (retain, nonatomic) UIView *directionArrow; -(double)headingToLocation:(CLLocationCoordinate2D)desired current:(CLLocationCoordinate2D)current; @end
添加声音常量
为了帮助跟踪我们上次播放的是哪种声音,我们声明了 `lastSound` 实例变量。我们的意图是将其用于保存表示我们三种可能声音中的每一种的整数。与其记住 2 = 右、3 = 左等等,不如在 `CupertinoViewController.m` 实现文件中添加一些常量来使其保持一致。
在为项目定义的现有常量之后插入这三行
#define straight 1 #define right 2 #define left 3
设置完成后,我们就可以实现为应用程序生成音频方向的代码了。
实现库比蒂诺音频方向
要向 Cupertino 应用程序添加声音播放功能,我们需要修改两个现有的 CupertinoViewController
方法。viewDidLoad
方法将为我们提供一个很好的位置来加载所有三个声音文件,并适当地设置 soundStraight
、soundRight
、soundLeft
引用。我们还将使用它将 lastSound
变量初始化为 0
,这与我们的任何声音常量都不匹配。这确保了无论第一个声音是什么,它都会播放。
编辑 CupertinoViewController.m 并将 viewDidLoad
更新为与清单 21.4 匹配。
清单 21.4
- (void)viewDidLoad { [super viewDidLoad]; NSString *soundFile; soundFile = [[NSBundle mainBundle] pathForResource:@"straight" ofType:@"wav"]; AudioServicesCreateSystemSoundID((CFURLRef) [NSURL fileURLWithPath:soundFile] ,&soundStraight); [soundFile release]; soundFile = [[NSBundle mainBundle] pathForResource:@"right" ofType:@"wav"]; AudioServicesCreateSystemSoundID((CFURLRef) [NSURL fileURLWithPath:soundFile] ,&soundRight); [soundFile release]; soundFile = [[NSBundle mainBundle] pathForResource:@"left" ofType:@"wav"]; AudioServicesCreateSystemSoundID((CFURLRef) [NSURL fileURLWithPath:soundFile] ,&soundLeft); [soundFile release]; lastSound=0; locMan = [[CLLocationManager alloc] init]; locMan.delegate = self; locMan.desiredAccuracy = kCLLocationAccuracyThreeKilometers; locMan.distanceFilter = 1609; // a mile [locMan startUpdatingLocation]; if ([CLLocationManager headingAvailable]) { locMan.headingFilter = 15; [locMan startUpdatingHeading]; } }
提示 - 请记住,这都是我们以前用过的代码!如果您在理解声音播放过程时遇到困难,请参考第 10 小时教程。
我们需要实现的最终逻辑是在有航向更新时播放每个声音。实现此功能的 CupertinoViewController.m 方法是 locationManager:didUpdateHeading
。每次此方法中更新箭头图形时,我们都会准备使用 AudioServicesPlay SystemSound
函数播放相应的声音。但在我们这样做之前,我们将检查以确保它与 lastSound
不是同一个声音;这将有助于防止由于一个声音文件重复播放而导致的 Max Headroom 口吃效应。如果 lastSound
与当前声音不匹配,我们将播放它并使用新值更新 lastSound
。
按描述编辑 locationManager:didUpdateHeading
方法。最终结果应类似于清单 21.5。
清单 21.5
- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading { if (self.recentLocation != nil && newHeading.headingAccuracy >= 0) { CLLocation *cupertino = [[[CLLocation alloc] initWithLatitude:kCupertinoLatitude longitude:kCupertinoLongitude] autorelease]; double course = [self headingToLocation:cupertino.coordinate current:recentLocation.coordinate]; double delta = newHeading.trueHeading - course; if (abs(delta) <= 10) { directionArrow.image = [UIImage imageNamed:@"up_arrow.png"]; if (lastSound!=straight) AudioServicesPlaySystemSound(soundStraight); lastSound=straight; } else { if (delta > 180) { directionArrow.image = [UIImage imageNamed:@"right_arrow.png"]; if (lastSound!=right) AudioServicesPlaySystemSound(soundRight); lastSound=right; } else if (delta > 0) { directionArrow.image = [UIImage imageNamed:@"left_arrow.png"]; if (lastSound!=left) AudioServicesPlaySystemSound(soundLeft); lastSound=left; } else if (delta > -180) { directionArrow.image = [UIImage imageNamed:@"right_arrow.png"]; if (lastSound!=right) AudioServicesPlaySystemSound(soundRight); lastSound=right; } else { directionArrow.image = [UIImage imageNamed:@"left_arrow.png"]; if (lastSound!=left) AudioServicesPlaySystemSound(soundLeft); lastSound=left; } } directionArrow.hidden = NO; } else { directionArrow.hidden = YES; } }
应用程序现已准备好进行测试。使用“构建并运行”将更新的 Cupertino 应用程序安装到您的 iPhone 上,然后尝试移动。当您移动时,它会说“右”、“左”和“直行”以对应屏幕上的箭头。尝试退出应用程序,看看会发生什么。惊喜!它不会工作!那是因为我们尚未更新项目的 plist 文件以包含“Required Background Modes”(UIBackgroundModes
) 键。
提示 - 如果在测试应用程序时,它仍然显得有点“话痨”(声音播放过于频繁),您可能希望在 viewDidLoad
方法中将 locMan.headingFilter
更新为更大的值(如 15 或 20)。这将有助于减少航向更新的次数。
添加后台模式键
我们的应用程序执行两个任务,这些任务在后台状态下应保持活动。首先,它跟踪我们的位置。其次,它播放音频以提供我们大致的航向。我们需要为应用程序添加音频和位置后台模式指定,才能使其正常工作。通过以下步骤更新 Cupertino 项目 plist
- 点击打开资源组中的项目 plist 文件 (Cupertino-Info.plist)。
- 向属性列表添加一个附加键,从“键”弹出菜单中选择“必需的后台模式”(
UIBackgroundModes
)。 - 展开该键,并在其中添加两个值:“App Plays Audio”(音频) 和“App Registers for Location Updates”(位置),如图 21.7 所示。这两个值都可以从“值”字段的弹出菜单中选择。
- 保存对 plist 文件的更改。
更新 plist 后,在您的 iPhone 上安装更新的应用程序,然后再次尝试。这次,当您退出应用程序时,它将继续运行!当您四处移动时,您会听到语音指示,因为 Cupertino 会在后台继续跟踪您的位置。
注意 - 通过声明位置和音频后台模式,您的应用程序能够在后台时使用位置管理器和 iOS 的许多音频播放机制的全部服务。
完成长时间运行的后台任务
在本小时的最后一个教程中,我们需要从头开始创建一个项目。我们的书并非旨在构建需要大量后台处理的应用程序。当然,我们可以演示如何将代码添加到现有项目中并允许方法在后台运行,但我们没有任何长时间运行的方法可以利用它。
为了演示我们如何告诉 iOS 允许某项任务在后台运行,我们将创建一个新应用程序 SlowCount,它只做一件事:缓慢地数到 1,000。我们将使用后台任务完成方法确保即使应用程序在后台,它也会继续计数直到达到 1,000——如图 21.8 所示。
准备项目
创建一个名为 SlowCount 的基于视图的 iPhone 应用程序。我们将快速完成开发,因为您可以想象,这个应用程序非常简单。
该应用程序将有一个单一的 outlet,一个名为 theCount
的 UILabel
,我们将用它在屏幕上显示计数器。此外,它还需要一个用作计数器(count
)的整数,一个将以固定间隔触发计数(theTimer
)的 NSTimer
对象,以及一个 UIBackgroundTaskIdentifier
变量(不是对象!),我们将用它来引用在后台运行的任务(counterTask
)。
注意 - 您希望启用后台任务完成的每个任务都需要自己的 UIBackgroundTaskIdentifier
。这与 UIApplication
方法 endBackgroundTask
一起使用,以识别刚刚结束的后台任务。
打开 SlowCountViewController.h 文件并按清单 21.6 所示实现。
清单 21.6
#import <UIKit/UIKit.h> @interface SlowCountViewController : UIViewController { int count; NSTimer *theTimer; UIBackgroundTaskIdentifier counterTask; IBOutlet UILabel *theCount; } @property (nonatomic,retain) UILabel *theCount; @end
注意 - UILabel
theCount
是我们应用程序中唯一会访问和修改属性的对象;因此,它是唯一需要 @property
声明的东西。
接下来,在 SlowCountViewController.m 的 dealloc
方法中清理 UILabel
对象。其他实例变量要么不是对象(count
、counterTask
),要么将在应用程序的其他地方分配和释放(NSTimer
)。
- (void)dealloc {
[theCount release];
[super dealloc];
}
创建用户界面
声称这个应用程序拥有“用户界面”有点牵强,但我们仍然需要准备 SlowCountViewController.xib 来在屏幕上显示 theCount
标签。
在 Interface Builder 中打开 XIB 文件,并将一个 UILabel
拖到视图中心。将标签的文本设置为 0。选中标签后,使用属性检查器 (Command+1) 将标签对齐方式设置为居中,并将字体大小设置为 36。最后,将标签的左右两侧与左右尺寸指南对齐。您刚刚创建了一个 UI 杰作,如图 21.9 所示。
通过从“Document”窗口中的“File's Owner”图标拖动到视图中的 UILabel
来完成视图。在提示时选择 theCount
以建立连接。
实现计数器逻辑
为了完成我们应用程序的核心功能(计数!),我们需要做两件事。首先,我们需要将计数器(`count`)设置为 0,并分配和初始化一个将以固定间隔触发的 `NSTimer`。其次,当计时器触发时,我们将要求它调用第二个方法 `countUp`。在 `countUp` 方法中,我们将检查 `count` 是否为 `1000`。如果是,我们将关闭计时器并完成;如果不是,我们将更新 `count` 并将其显示在我们的 `UILabel` `theCount` 中。
初始化计时器和计数器
让我们从初始化计数器和计时器开始。在 SlowCount.m 的 `viewDidLoad` 方法中执行此操作再好不过了。按照清单 21.7 所示实现 `viewDidLoad`。
清单 21.7
1: - (void)viewDidLoad { 2: [super viewDidLoad]; 3: count=0; 4: theTimer=[NSTimer scheduledTimerWithTimeInterval:0.1 5: target:self 6: selector:@selector(countUp) 7: userInfo:nil 8: repeats:YES]; 9: }
第 3 行将我们的整数计数器 count
初始化为 0。
第 4-8 行初始化并分配 theTimer
NSTimer
对象,间隔为 0.1
秒。selector
设置为使用方法 countUp
,我们接下来将编写该方法。计时器设置为使用 repeats:YES
继续重复。
剩下的就是实现 countUp
以便它增加计数器并显示结果。
更新计数器和显示
在 SlowCountViewController.m 中的 viewDidLoad
方法之前添加 countUp
方法,如清单 21.8 所示。这应该非常简单——如果计数等于 1000
,我们就完成了,是时候进行清理了——否则,我们继续计数!
清单 21.8
1: - (void)countUp { 2: if (count==1000) { 3: [theTimer invalidate]; 4: [theTimer release]; 5: } else { 6: count++; 7: NSString *currentCount; 8: currentCount=[[NSString alloc] initWithFormat:@"%d",count]; 9: theCount.text=currentCount; 10: [currentCount release]; 11: } 12: }
第 2-4 行处理我们已达到计数限制(`count==1000`)的情况。如果达到了,我们使用计时器的 `invalidate` 方法停止它,然后 `release` 它。
第 5-11 行处理实际计数和显示。第 5 行更新 count
变量。第 7 行声明 currentCount
字符串,然后在第 8 行分配并填充。第 9 行使用 currentCount
字符串更新我们的 theCount
标签。第 10 行释放字符串对象。
构建并运行应用程序——它应该会像您预期的那样——缓慢计数直到达到 1,000。不幸的是,如果您将应用程序置于后台,它将暂停。计数将停止,直到应用程序返回前台。
启用后台任务处理
要使计数器在后台运行,我们需要将其标记为后台任务。我们将使用此代码片段来标记我们要在后台执行的代码的开始
counterTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ // If you're worried about exceeding 10 minutes, handle it here }];
我们将使用此代码片段来标记结束
[[UIApplication sharedApplication] endBackgroundTask:counterTask];
注意 - 如果我们担心应用程序在被强制结束之前(大约 10 分钟)未能完成后台任务,我们可以在 beginBackgroundTaskWithExpirationHandler
块中实现可选代码。您始终可以通过检查 UIApplication
属性 backgroundTimeRemaining
来查看剩余时间。
让我们更新 viewDidLoad
和 countUp
方法,以包含这些代码添加。在 viewDidLoad
中,我们将在初始化计数器之前启动后台任务。在 countUp
中,我们在 count==1000
且计时器失效并释放后结束后台任务。
更新 viewDidLoad
,如清单 21.9 所示。
清单 21.9
- (void)viewDidLoad { [super viewDidLoad]; counterTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ // If you're worried about exceeding 10 minutes, handle it here }]; count=0; theTimer=[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(countUp) userInfo:nil repeats:YES]; }
然后对 countUp
进行相应的添加,如清单 21.10 所示。
清单 21.10
- (void)countUp { if (count==1000) { [theTimer invalidate]; [theTimer release]; [[UIApplication sharedApplication] endBackgroundTask:counterTask]; } else { count++; NSString *currentCount; currentCount=[[NSString alloc] initWithFormat:@"%d",count]; theCount.text=currentCount; [currentCount release]; } }
保存您的项目文件,然后在您的 iPhone 或模拟器中“构建并运行”该应用程序。计数器开始计数后,按下 Home 按钮将应用程序移至后台。等待大约一分钟,然后通过任务管理器或应用程序图标重新打开应用程序。计数器将在后台继续运行!
显然,这本身并不是一个引人注目的项目,但对于现实世界应用程序中可以实现的功能,其影响无疑令人兴奋!
进一步探索
当我坐下来写这节课时,我左右为难。后台任务/多任务处理无疑是 iOS 4.0 的“必备”功能,但要在十几或二十页的篇幅中演示任何有意义的东西都是一个挑战。我希望我们所实现的是更好地理解 iOS 多任务处理的工作原理以及如何在您自己的应用程序中实现它。请记住,这并非后台处理的综合指南——还有更多功能可用,以及许多优化支持后台的应用程序以最大限度延长 iPhone 电池续航时间和速度的方法。
下一步,您应该阅读 Apple 的《iPhone 应用程序编程指南》(可通过 Xcode 文档获取)中的以下部分:“在后台执行代码”、“准备您的应用程序在后台执行”和“启动后台任务”。
当您审阅 Apple 的文档时,请密切关注您的应用程序在进入后台时应完成的任务。对于游戏和图形密集型应用程序,存在一些超出我们在此讨论范围的影响。您遵守这些准则的程度将决定 Apple 是否接受您的应用程序,或将其退回给您进行优化。
摘要
iOS 设备上的后台应用程序与您的 Macintosh 上的后台应用程序不同。iOS 后台应用程序必须遵循一套明确定义的规则,才能被视为 iOS 4.x 的“好公民”。在本小时的课程中,您了解了 iOS 中可用的不同类型的后台运行以及支持后台任务的方法。通过五个教程应用程序,您测试了这些技术,创建了从应用程序未运行时触发的通知到提供后台语音提示的简单导航应用程序。
您现在应该已准备好创建自己的后台感知应用程序,并充分利用 iOS 4.0 的最新和最强大的功能。
问答
- 为什么我不能在后台运行任何我想要的代码?
- 总有一天,我猜你会,但目前平台仅限于我们讨论的特定类型的后台处理。在始终连接互联网的设备上运行任何东西都涉及巨大的安全和性能影响。请记住,iPhone 是一款手机。Apple 旨在确保当您的 iPhone 需要用作手机时,它能够正常运行!
- 那么基于时间线的后台处理,比如即时通讯客户端呢?
- 基于时间线处理(对随时间发生的事件做出反应)目前在 iOS 中是不允许的。这令人失望,但它确保了您的手机上没有几十个应用程序占用资源,等待事情发生。
工作坊
测验
- 后台任务在 iOS 4.0 中可以是您想要的任何内容。对还是错?
- 任何您为 iOS 4.0 编译的应用程序在用户退出后都会继续运行。对还是错?
- 只有一个长时间运行的后台任务可以被标记为后台完成。对还是错?
答案
- 错误。Apple 对 iOS 4.0 中实现后台处理有一套明确的规则。
- 错误。应用程序默认会在后台暂停。要继续处理,您必须按照本小时课程中的描述实现后台任务。
- 错误。您可以标记任意多个长时间运行的任务,但它们都必须在设定的时间内(大约 10 分钟)完成。
活动
- 返回到之前某个小时的项目,并为其正确启用后台处理。