iOS推送通知背景下载神秘化

在实施推送通知时,我发现许多有关后台下载如何工作的不完整和不准确的信息。 我必须通过实验弄清楚其中的一些内容,以防万一它可以帮助其他人,这就是我所学到的。

(请注意,我将不介绍设置Apple Push Notifications的详细信息-这是一个完全不同的主题)。

期望的行为

至少对于我的应用程序而言,期望的行为是到达推送通知以触发后台下载,以便在用户下次打开该应用程序时(通过点击通知或正常打开该应用程序),该数据已经在那里。

此外,如果用户点击通知本身,则该应用应打开并显示该通知引用的特定视图/数据。

为了更具体一点,在紧急电子邮件和任务管理应用程序中,有关新电子邮件的通知应触发该应用程序从服务器下载该消息的内容,然后点击通知应打开该应用程序并显示该特定线程。

一个明显的问题是为什么您不能仅随通知一起直接发送所需的内容。 问题在于,Apple将通知有效载荷限制为2 KB,不足以包含大多数电子邮件的内容和元数据。

配置项目以进行通知背景下载

首先,您需要为您的应用设置适当的功能。 我假设您已经在目标设置的“功能”部分中启用了“推送通知”。 对于后台获取,您还需要在“后台模式”下启用“后台获取”和“远程通知”。

所有重要内容可用标志

其次,不要忘记在服务器发出的实际通知中将content-available设置为1,以触发后台下载。 如果您未能在通知中包含此标志,则通知到达时,您的应用将不会收到任何协议方法调用。

了解相关的App Delegate协议方法

乍一看,这似乎很简单。 UIApplicationDelegateProtocol提供了application:didReceiveRemoteNotification:fetchCompletionHandler:方法,该方法在通知到达电话时被调用。 并发症有两个方面:

  1. 当用户点击通知以启动应用程序时,也会使用完全相同的参数调用完全相同的didReceiveRemoteNotification方法。
  2. 如果通知到达时(或用户点击通知时)应用尚未运行,则通知信息将作为启动选项传递给didFinishLaunchingWithOptions,然后调用didReceiveRemoteNotification *。 同样,没有区分通知到达与用户点击通知。

因此,您需要处理四种情况:

  1. 通知到达,应用程序在后台运行。
  2. 通知到达,应用未在后台运行。
  3. 用户点击通知,应用程序在后台运行。
  4. 用户点击通知,应用未在后台运行。

案例1和3仅在案例2和4在didFinishLaunchingWithOptions的启动选项中提供通知详细信息时才调用didReceiveRemoteNotification,然后调用didReceiveRemoteNotification。

更为复杂的是,如果用户通过将其从内存中刷出而手动杀死了您的应用程序,则不会发生情况2。 也就是说,您的应用程序永远不会在后台启动以获取数据,直到用户选择再次启动它。

从用户点击通知中区分通知到达

除了一个例外,您可以通过检查应用程序状态来区分通知到达和用户在didReceiveRemoteNotification(或didFinishLaunchingWithOptions)中点击通知的情况。 如果应用程序在后台运行(UIApplicationStateBackground),则应执行后台下载。 相反,如果应用程序是“非活动的”(UIApplicationStateInactive),则表示它是响应于用户点击通知而从后台移动到活动的。 如果该应用程序已经处于活动状态(UIApplicationStateActive),则通常不需要执行任何操作。

但是,有一个重要的例子。 如果应用程序已暂停(在快速应用程序切换模式下,双击主页按钮后),并且通知到达,则应用程序的状态将为UIApplicationStateInactive而不是UIApplicationStateBackground。 但是,这并不意味着用户点击了通知。 因此,您需要其他逻辑来检测这种情况。 您可以将applicationWillEnterForeground用作其他信息。 当应用程序从后台转换为非活动状态时,始终会调用该方法,因此您可以依靠它在用户点击通知且应用程序实际上正在启动时(而不是通知到达时)被调用,而用户尚未交互。 您可以保留一个标志来跟踪该应用程序是否真正启动。

这是AppDelegate中的基本代码:

  -(void)applicationWillResignActive:(UIApplication *)application { 
self.appIsStarting =否;
}
  -(void)applicationDidEnterBackground:(UIApplication *)application { 
self.appIsStarting =否;
}
  -(void)applicationWillEnterForeground:(UIApplication *)application { 
self.appIsStarting = YES;
}
  -(void)applicationDidBecomeActive:(UIApplication *)application { 
self.appIsStarting =否;
}
  -(void)应用程序:(UIApplication *)应用程序didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void(^)(UIBackgroundFetchResult))completionHandler { 
  UIApplicationState状态= [应用程序applicationState]; 
 如果(状态== UIApplicationStateBackground || 
(状态== UIApplicationStateInactive &&
!self.appIsStarting)){
  NSDictionary * aps = userInfo [@” aps”]; 
如果(aps){
//执行后台抓取并
//呼叫完成处理程序
}
}
  } if if(state == UIApplicationStateInactive && 
self.appIsStarting){
  //用户点击通知 
completeHandler(UIBackgroundFetchResultNewData);
}其他{
  //应用处于活动状态 
completeHandler(UIBackgroundFetchResultNoData);
}
}

此代码处理上述情况1和3。

处理应用程序先前未运行的情况

如前所述,如果在通知到达时或用户点击通知时应用尚未运行,则在调用didReceiveRemoteNotification之前,将调用didFinishLaunchingWithOptions和launchOptions中的通知。 解决此问题的最简单方法是忽略didFinishLaunchingWithOptions中的通知选项。 但是,您确实需要记住在didFinishLaunchingWithOptions中设置appIsStarting标志,以便如果应用程序状态为UIApplicationStateInactive,则didReceiveRemoteNotification知道将其视为通知分流:

  NSDictionary *数据= [launchOptions 
objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
 如果(数据){ 
self.appIsStarting = YES;
}

使用完成处理程序

别忘了,您只有30秒的时间来执行后台提取,完成后必须调用completionHandler。

测试中

可悲的是,您无法在模拟器上测试推送通知-您必须使用物理设备。

为了测试通知到达时以后台模式启动(而不是已经在后台运行)的应用程序的场景,您可以使用Xcode在手机上启动该应用程序,然后通过在Xcode中停止该应用程序来终止该应用程序。 下一个到达的通知将使应用程序重新启动到后台,调用(如上所述)didFinishLaunchingWithOptions而不是didReceiveRemoteNotification。 请注意,您无法通过自己将应用程序滑出内存来杀死它来测试此情况,因为那样一来,iOS不会响应到达的通知而将其启动到后台。

(2016年4月30日添加)或者,如评论员所指出的,您可以在方案“信息”选项卡中将应用设置为“等待可执行文件启动”,然后再在设备上运行它。

*已于2016年4月30日更新,以正确描述通知到达或轻按通知且应用当前未运行时的行为。