iOS MultiTasking再认识(中篇)

Background Fetch能力测试与验证

首先我们来看看使用Background Fetch与不使用其效果对比图:

左图是直接前台加载的效果,如果是弱网环境下,转菊花的时间就会越久。右图则是经过后台获取后,直接进入应用的效果的。下面通过不同的case,验证和总结Background Fetch的能力。

测试Case

但是由于这个测试通过Xcode调试模式处理,结果与理论出入有些大。这里,我提供理论性的结论,大家有兴趣可以自测一下。

case1 Fetch完后,调用CompletionHandler。模拟一次后台获取。

理论上,30s后,应用被挂起。如果下载未完成,下载任务会中止。

case2 Fetch完后,不调用CompletionHandler。模拟一次后台获取。

理论上,30s,应用应该被kill掉

case5 任何一种情况下,在后台获取的过程中,系统进入休眠。

理论上,在这30s内进入休眠,对于后台获取无影响

case6 任何一种情况下,在后台获取的过程中,应用被中断而退出运行。

理论上,在这30s内进入应用被中断而退出运行,对于后台获取无影响。

但是仍需提醒的是,频繁后台获取会过多消耗手机电量和流量,所以,后台获取虽好,可要慎重使用哦!

Remote Notification

作为iOS7之后的另外一大新特性,Remote Notification的出现,大大改善了iOS应用的用户体验。类似于Background Fetch,Remote Notification也多用于新闻、天气、社交应用等应用。Remote Notification相比于Background Fetch,更适合用于那些偶尔出现,但却很重要的内容,如果使用后台获取模式中在两次获取间需要等待的时间是不可接受的话,远程通知会是一个不错的选择。远程通知会比后台获取更有效率,因为应用程序只有在需要的时候才会启动。

在了解Remote Notification之前,我们先来了解一下它的原理,这样即使以后遇到什么问题,我们出不会一脸蒙逼的样子~~哈哈

远程推送原理

  1. 传输路径

    Remote Notification(即PUSH系统)主要由4部分构成:

    • Provider (生成notification的server)
    • APNS
    • Device
    • Client App

      其中,APNS是Remote Notification的核心部分,它是苹果所提供的一套服务,用于传递信息给ios(watchOS, tvOS, OS X)等设备。推送消息的传输路径如下图:

      我们的设备联网时(无论是蜂窝网络或是Wi-Fi)都会与苹果的APNS服务器建立一个长连接,当Provider推送一条通知的时候,它是先推送到苹果的APNS服务器上面,而苹果的APNS服务器再通过与设备建立的长连接进而把通知推送到我们的设备上。当设备处于非联网状态的时候,APNS服务器会保留Provider所推送的最后一条通知,设备转换为联网状态时,APNS会把其保留的最后一条通知推送给我们的设备;如果设备长时间处于非联网的状态下,那么APNS服务器为其保存的最后一条通知也会丢失。

  2. Device Token的生成

    Device Token简单理解就是包含了设备信息和应用信息(设备的UUID和APP的Bundle Identifier)的一串编码。deviceToken对于一台设备上的一个应用来说是唯一的,但是它并非固定不变的,比如我们重新安装应用时,对应的token就会改变。Provider在推送消息的时候必须带有此deviceToken,然后APNS会根据此deviceToken找到对应的设备以及设备上对应的应用,从而把此推送消息推送给此应用。当一个App注册接收远程通知的时候,系统会发送请求到APNS服务器,APNS服务器收到此请求后会根据请求所带的key值生成一个独一无二的value值也就是所谓的deviceToken,APNS服务器会把此deviceToken包装成一个NSData对象发送到对应请求的App上。此deviceToken是我们的Provide(server)在给我们的设备推送通知的时候,必须包含的内容。所以App在收到deviceToken后,需要把它发送给我们的服务器。

  3. Provider对Notification的生成Provider生成notification,对于每一个notification都会生成对应的payload(后面会详细介绍这一部分),并将其附加到http request中,发送给APNS server。APNS server会将些消息转发给对应的device,最后通过os将些notification转发到对应的App。自2015年7月份后,Apple更新了APNS的协议,基于HTTP2.0协议以及JSON数据传输,而在此之前,APNS是基于TLS socket连接完成通信过程的。简单地给出APNS新老通信协议的对比:戳->APNS推送协议

Remote Notification介绍

  1. IOS7之前

    • 推送类型单一,如显示标题、指定声音等
    • 仅支持以下代理方法处理notification
    • 用户可以收到Notification,但是只能进入应用之后才能响应push,获取数据,更新UI

      如下图所示: 

      iOS7之前的Remote Notification与没有Background Fetch时获取数据刷新的问题是一样,用户进入应用后,必须等待数据加载的过程。

  2. IOS7及之后

    • 在接收到Remote Notification后,系统会唤醒设备和我们的后台应用,并先执行一段代码来准备数据和UI(时间有限),然后再提示用户有推送(唤醒App在后台运行。但是系统分配的后台运行时间不超过30s,经过测试也大概30s的样子。

    • 同时支持以下代理方法处理notification:

        - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo 
        - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))handler
      

      下图是iOS7之后,Remote Notification组件关系图。

    • Remote Notification也可以做到完全的Silent模式,在没有提醒消息或任何声音的情况下,只去更新应用界面或者触发后台工作。然后可以在完成下载或处理完更新内容后,发送一条本地通知。通过用户进入App。这个主要通过Payload去设置就可以实现,后面使用方法里会提到。
  3. 使用场景

    类似于Background Fetch,Remote Notification也多用于新闻、天气、社交应用等应用。Remote Notification相比于Background Fetch,更适合用于那些偶尔出现,但却很重要的内容,如果使用后台获取模式中在两次获取间需要等待的时间是不可接受的话,远程通知会是一个不错的选择。远程通知会比后台获取更有效率,因为应用程序只有在需要的时候才会启动。

    同时,它也通常会结合Background Transfer Service去完成一些耗时较长的网络传输工作。

Remote Notification应用

  1. 推送环境的配置

    这是一个比较复杂的过程,不是本篇文章的重点,Push配置一文比较清楚地介绍了Push环境的配置,大家可以作为参考。

  2. 启用推送唤醒

    如下图直接使用Capabilities配置

  3. 更改推送的payload

    Payload用来定义系统通知信息的Json数据结构,它由以下几部分组成:

    • alter: 消息的内容
    • badge: 标记notification的数目
    • sound: 提示音
    • content-available: 仅在iOS7之后支持,通过此属性,设置其为"Silent" remote notification,app在后台被唤醒,通过调用application:didReceiveRemoteNotification:fetchCompletionHandler:处理notification。

      切记,content-available是决定应用能否被在后台唤醒并执行上述回调的关键。

      aps{
         alert:"My first push notification",
         badge: 1,
         sound: "default",
         content-available: 1
      }
      

      另外,上面介绍APNS的时候,有提到过,APNS对于Payload的大小是有限制的,大小也取决于通过协议。基于HTTP2协议,provider API能支持最大字节数据为4096bytes。

  4. 注册Remote Notification(获取deviceToken)

    一般都是在App启动完成时去注册。远程通知注册方法的调用一般都在didFinishLaunchingWithOptions:方法中完成。

     - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
         // Override point for customization after application launch.
         UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert;
         UIUserNotificationSettings *mySettings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
         [[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];
         [[UIApplication sharedApplication] registerForRemoteNotifications];
         return YES;
     }
    
     //注册成功后,会返回deviceToken,需要将deviceToken告知push server
     -(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(nonnull NSData *)deviceToken{
    
         NSLog(@"Did Register for Remote Notifications with Device Token (%@)", deviceToken);
     }
    
     -(void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(nonnull NSError *)error{
    
         NSLog(@"Did Fail to Register for Remote Notifications");
         NSLog(@"%@, %@", error, error.localizedDescription);
     }
    
  5. 实现推送唤醒代码并通知系统

    iOS7添加了一个新的AppDelegate方法来处理Remote Notification,当接收到一条带content-available的推送通知时,此代理方法会被调用。iOS7之后的系统,都建议使用该方法来处理notification。

     - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))handler
    
  1. 同样地,在使用此API时,也有一些需要注意到的地方:

    • 系统分配最多30s的时间,处理notification。超过30s,系统会再次将应用挂起。
    • API被调用场景:其一、App在前台、后台运行或挂起的状态下notification到来;其二、用户通过通知栏使App进入前台。所以,需要避免同一条notification被重复处理。在通过通知栏进入应用,且push回调被调用时,App的状态为UIApplicationStateInactive;而第一种情况下,App的状态为UIApplicationStateBackground;因此可以通过状态的不同,判断是哪种情况下触发的push回调。
    • 通常建议,使用Remote Notification,尽量使用Silent mode。App在后台处理完notification后后,再发送一条LocalNotification。这样用户通过通知栏进入应用后,便可直接展示最新的数据。也避免了上面提到的重复调用上述回调的问题。
    • fetchCompletionHandler需要在处理完notification后,马上调用。

      这里是这部分的实现代码。

      -(void)application:(UIApplication *)application
      didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo
      fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler
      {
        [self dataTaskResumeWithCompletionHandler:completionHandler];
        NSLog(@"%@", userInfo);
      }
      
      -(void)dataTaskResumeWithCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler
      {
        NSURL * url = [NSURL URLWithString:@"http://xmind-dl.oss-cn-qingdao.aliyuncs.com/xmind-7-update1-macosx.dmg"];
      
        NSURLSessionDataTask * dataTask = [[self defaultURLSession] dataTaskWithURL:url
                                                                  completionHandler:^(NSData *data, NSURLResponse * response, NSError * error)
                                           {
      
                                               if (error == nil) {
                                                   NSString * text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
      
                                                   NSLog(@"Data = %@", text);
                                                   completionHandler(UIBackgroundFetchResultNewData);
                                               }
                                               else{
                                                   completionHandler(UIBackgroundFetchResultFailed);
                                               }
      
                                           }];
        [dataTask resume];
      
      }
      
      -(NSURLSession *)defaultURLSession
      {
        static NSURLSession * defaultSession = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            NSURLSessionConfiguration * configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
      
            defaultSession = [NSURLSession sessionWithConfiguration:configuration
                                                                          delegate:nil
                                                                     delegateQueue:[NSOperationQueue mainQueue]];
        });
        return defaultSession;
      }
      


需要完全的PushDemo,包含Push脚本,请猛戳->PushDemo源码下载


网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者何慧授权发布。