iOS-Strategies for Handling App State Transitions

iOS App的生命周期参考iOS-App Life Cycle。本文介绍了,App的生命周期的各种状态下的处理策略。

Launch Time

状态变化

当App开始运行时(无论是进入Foreground或者background状态),都需要通过以下两个回调:

1
2
-application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
-application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

来完成以下工作:

  • 通过application.applicationState来判断是进入什么状态,如果是Foregound,则为UIApplicationStateInactive,如果是Background,则为UIApplicationStateBackground;
  • 通过launchOptions获取App开始运行的原因以及做出相应的对策;
  • 初始化App一些关键的数据结构;
  • 不要在此处进行OpenGL ES渲染,在applicationDidBecomeActive:中进行;
  • 设置keyWindow。

在开始运行时,系统会自动加载storyboard文件,以及对应的初始化ViewController,对于一些支持状态还原的APP,应该在willFinishLaunchingWithOptionsdidFinishLaunchingWithOptions两个方法之间进行状态还原。在willFinishLaunchingWithOptions方法中展示keyWindow,并判断是否进行状态还原。在didFinishLaunchingWithOptions方法中,对用户界面进行最终的修改。

下面是Not Running状态进入到Foreground状态的流程图:

App-Launch

下面是Not Running状态进入到Background状态的流程图:

App-Launch

横屏模式

App默认是竖屏启动的。只需要横屏显示的模式时,可以显示通知系统,以保证App横屏地启动。设置方式如下:

  • 在Info.plist文件中把UIInterfaceOrientation设置为UIInterfaceOrientationLandscapeLeft或者UIInterfaceOrientationLandscapeRight;
  • 布局好横屏模式下的Views;
  • 重写ViewController的shouldAutorotateToInterfaceOrientation方法,在横屏下返回YES,竖屏返回NO;
  • 对于StatusBar,系统也会根据UIInterfaceOrientation进行自动设置,效果等同于在applicationDidFinishLaunching:方法中调用[application setStatusBarOrientation:animated:]方法。

初次启动时创建数据文件

在初次启动时,可能需要进行创建一些App需要的数据和配置文件,应该在以下目录中创建:

1
2
3
Library/Application Support/<bundleID>/
/Documents/
iCloud的包含目录

如果App的Bundle中包含了需要进行修改的文件,需要复制这些文件到上述目录下,再进行修改。不能直接修改Bundle中的文件,否则会造成签名不匹配,可能导致启动失败。

Interrupted Temporarily

App在运行时,可能遇到一些中断(Alert-based Interruptions),这种情况下,App依然在前台,但是收不到任何触摸事件(依然可以收到推送以及其他一些事件,例如加速计事件)。

中断处理

当中断发生时,例如来电时,App会切换到Inactive状态,直到用户取消这一中断,此时,App切换回Active状态或者Background状态。

流程图:

App-Interruptions

如果是推送的Banner,不会造成中断。但是,如果用户往下拉Banner,出现通知中心时,App将会切换到Inactive状态,与中断一样,直到用户关闭通知中心或者启动其他App。

在处理这种情况时,App应该在以下回调中:

1
applicationWillResignActive:

处理:

  • 保存数据和任何相关的状态信息;
  • 停止Timer和其他的定时任务;
  • 停止任何在运行的数据库查询;
  • 不要新建新的任务;
  • 暂停电影的播放(特别是通过AirPlay的播放);
  • 如果是游戏App,进入暂停状态;
  • 降低OpenGL ES的帧率;
  • 挂起dispatch queues或者operation queues的运行代码(可以在inactive状态下在background tasks中继续处理一些网络请求或者其他时间敏感的后台任务)。

当App重新变成Active状态时,App应该在以下回调中:

1
applicationDidBecomeActive:

反向处理上面的操作,游戏除外,游戏应该依然处于暂停状态,让用户自己选择。

电源键(Sleep/Wake Button)

如果用户按下电源键,系统会disables触摸事件,将App切换到后台,并将application.applicationState设置为UIApplicationStateBackground,并且锁屏。

如果用户按下电源键,app必须关闭与NSFileProtectionComplete保护选项下的文件的引用。当电源键被按下时,系统将锁屏,并丢弃这些保护文件的引用,任何与这些文件相关的访问将会失败。所以,应该在applicationWillResignActive:方法中关闭,而在applicationDidBecomeActive:方法中重新打开。

Enter the Foreground

前台任务

App切换回前台时,需要恢复那些切换到后台时,停止的任务。即applicationWillEnterForeground:回调应该与applicationDidEnterBackground:回调的任务相反。而applicationDidBecomeActive:回调应该执行一些与App启动时一致的任务。

流程图:

App-Enter-Foreground

注意,App切换回前台时,会收到UIApplicationWillEnterForegroundNotification广播,可以注册监听这一广播事件。

处理队列通知(Queued Notifications)

处于Suspended状态的App,必须在切回Foreground或者Background状态时,准备好处理队列通知。因为处于Suspended状态的App无法执行代码,也就无法处理与横竖屏装换、时间变化、语言变化和其他一些影响App界面和状态的通知。为了确保这些通知不会丢失,系统将这些相关的通知队列化,当App切回Foreground或者Background状态时,系统进行分发这些通知。为了确保App不会被通知淹没,系统将这些事件合并,对于相关的类型,只分发一条通知。

下面是通知的类型:

事件 通知
当一个外部设备连接或者断开时 EAAccessoryDidConnectNotification EAAccessoryDidDisconnectNotification
设备朝向切换时 UIDeviceOrientationDidChangeNotification
时间变化时(进入新的一天、运营商时间更新等) UIApplicationSignificantTimeChangeNotification
电池变化时 UIDeviceBatteryLevelDidChangeNotification UIDeviceBatteryStateDidChangeNotification
距离(Proximity)传感器状态变化时 UIDeviceProximityStateDidChangeNotification
被保护的文件状态变化时 UIApplicationProtectedDataWillBecomeUnavailable UIApplicationProtectedDataDidBecomeAvailable
外部显示器连接或者断开时 UIScreenDidConnectNotification UIScreenDidDisconnectNotification
显示器的屏幕模式切换时 UIScreenModeDidChangeNotification
App的设置项变化时(在设置App中被用户手动变化,例如关闭推送通知) NSUserDefaultsDidChangeNotification
语言或者地区变化时 NSCurrentLocaleDidChangeNotification
用户的iCloud账号状态变化时 NSUbiquityIdentityDidChangeNotification

队列化通知将会被分发到App主线程的RunLoop中,一般会在触摸事件以及其他用户输入之前被分发。大多数App都足够快能够处理这些事件,如果App的交互和界面出现延迟,那么可能需要检查处理事件的代码逻辑。

App在后台时,依然可以调用setNeedsDisplaysetNeedsDisplayInRect:方法,当时此时系统会先搁置这些请求,并在App切换回Foreground状态时,分发这些尚未完成的View-Update通知。

处理iCloud账号状态变化

当iCloud账号状态变化时,例如,用户登录或退出,或者用户开启或者关闭iCloud的文件和数据同步,系统会分发NSUbiquityIdentityDidChangeNotification通知,App应该做出相应的变化。例如,当用户退出iCloud时,App对iCloud的文件应用应该关闭。

处理地区变化

当用户切换地区时,App会收到NSCurrentLocaleDidChangeNotification通知,此时,应该对一些地区敏感的信息进行处理,例如日期,时间和一些数字。可以利用以下方法来获取一个自动切换的NSLocale对象:

1
[NSLocale autoupdatingCurrentLocale]

处理App的设置变化

当App的设置项变化时(在设置App中被用户手动变化,例如关闭推送通知),App会收到NSUserDefaultsDidChangeNotification通知,App应该做出相应的变化。例如,邮件App应该对用户的账户信息敏感,当用户在设置App中关闭这一权限时,邮件App应该不能再操作用户的邮件,否则可能会造成安全问题。

Enter the Background

后台任务

当App切换到后台时,应该在applicationDidEnterBackground:回调中,处理:

  • 保存相关的状态信息;
  • 尽可能释放内存:占用内存过大的后台App,将会被系统直接终止,所以,尽可能释放图片,数据,多媒体等资源的强引用,系统本身会移除所有系统对缓存图片的引用,系统管理的数据存储,以及所有Core Animation Layers的储备(Backing Store),这并不会移除layers对象,也不会改变其属性,只是不让其显示在屏幕上;
  • 预备好App即将被截图:系统会在applicationDidEnterBackground回调后进行App的截图,用于多任务处理,如果有敏感信息,应该在applicationWillResignActive回调中进行隐藏或者修改,因为applicationDidEnterBackground在双击Home键的时候不会触发。

回调applicationDidEnterBackground,大概有5秒左右的处理时间,如果超出,App将会被系统直接终止并从内存移除,如果需要进行更多的时间,调用方法:

1
beginBackgroundTaskWithExpirationHandler:

并在子线程中进行耗时任务。

流程图:

App-Bg-Life-Cycle

注意,App切换到后台时,会收到UIApplicationDidEnterBackgroundNotification广播,可以注册监听这一广播事件。