理解iOS的用户通知(上篇)

达芬奇密码2018-06-22 13:35


用户通知简介

用户通知是什么

iOS中存在三种常见的事件通知方式:NSNofiticationCenter、KVO Notification 和 User Notifications,其中 User Notifications,就是本文将要探讨的用户通知。

我们都知道 iOS 系统经常的有一些与 App 相关的通知栏消息,这些消息往往伴随着提示音以及 App 的桌面图标右上角的未读消息提示,这些通知就是 iOS 的用户通知。

用户通知的分类

用户通知分为两类:本地通知和远程通知,其中远程通知又称为推送通知。

两者最主要的区别是:本地通知是由 App 发送到当前设备上,不需要网络支持;而远程通知是由 App 的服务器发送到苹果的 APNs 服务器,并由 APNs 服务器转发到相应设备(由 App 服务器指定接收通知的设备)。

两者最主要的共同点是:本地通知和远程通知对用户的表现形式是相同的,两者均可以采用通知栏消息、App 桌面图标右上角角标和提示音的方式通知用户。

用户通知有什么用处

及时有效的(无论是在前台还是后台)向用户发送消息(聊天信息、新闻、待办事项、天气变化等)是用户通知最大的优势。

此外,有效合理的使用用户通知,可以让我们的 App 有更好的体验,如:

  • 当待办事项将要过期时可以及时提醒用户;
  • 当用户执行下载大文件任务时进入后台,当下载完成后可以通知用户;
  • 当用户环球旅行时,可以根据用户的地理位置推送天气变化等信息;
  • 当用户订阅的某杂志或新闻主题有更新时,通知用户;
  • ……

本文后续内容将以应用开发者的角度对用户通知进行深入的探讨,本文讨论内容针对iOS7/8/9,有关 iOS10 系统的用户通知会另做讲解。

本文中的远程通知使用了 Simplepush.php ,内部代码很简单,可使用该脚本自定义远程通知的内容, 建议花几分钟查看以下 Simplepush.php 的用法。此外,demo 不提供证书,如有远程通知需求,请自行申请证书,否则无法正常使用 Simplepush。

本文主要参考了苹果官方的 Local and Remote Notification Programming Guide 以及本文用到的接口的官方文档。

本地通知的使用

开启本地通知功能

  • 对于 iOS7,如果用户没有在系统设置里关闭该 App 的通知功能,那么开发者无需做任何操作即可使用本地通知功能。

  • 对于 iOS8 及以后的系统,若需要使用本地通知功能,则需要注册通知类型。 通知类型有四种:角标(UIUserNotificationTypeBadge)、提示音(UIUserNotificationTypeSound)、提示信息(UIUserNotificationTypeAlert)和无任何通知(UIUserNotificationTypeNone)。 你可以注册上诉四种通知类型的任意组合,但最终可用的通知形式需要根据用户对此 App 通知的设置确定。比如:App 内部注册了角标、提示音和提示信息,但是用户关闭了声音通知,那么收到本地通知时是不会有提示音的。 对于 iOS8 及以后的系统,注册本地通知的代码示例如下:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
            // 只有 iOS8 and later 才需要
            if ([[UIApplication sharedApplication]     respondsToSelector:@selector(registerForRemoteNotifications)]) {
            // 这里 types 可以自定义,如果 types 为 0,那么所有的用户通知均会静默的接收,系统不会给用户任何提示(当然,App 可以自己处理并给出提示)
            UIUserNotificationType types = (UIUserNotificationType) (UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert);
            // 这里 categories 可暂不深入,本文后面会详细讲解。
            UIUserNotificationSettings *mySettings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
            // 当应用安装后第一次调用该方法时,系统会弹窗提示用户是否允许接收通知
            [[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];
        }
    
        // Your own other codes.
        return YES;
    }
    

当系统弹窗提示用户是否允许接收通知后,用户可能会拒绝;我们可以在 AppDelegate 的 application:didRegisterUserNotificationSettings: 方法中用来查看注册成功的通知类型,我们可以在拿到注册结果后做自定义操作(比如失败时弹个窗提示用户当前无法使用用户通知)。 苹果推荐在之后发送的本地通知时,要避免使用没有注册成功的通知类型(并不是强制要求)。

- (void)application: (UIApplication*)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings {
    if (notificationSettings.types & UIUserNotificationTypeBadge) {
        NSLog(@"Badge Nofitication type is allowed");
    }
    if (notificationSettings.types & UIUserNotificationTypeAlert) {
        NSLog(@"Alert Notfication type is allowed");
    }
    if (notificationSettings.types & UIUserNotificationTypeSound) {
        NSLog(@"Sound Notfication type is allowed");
    }
}

发送本地通知

发送一个本地通知主要有如下步骤:

  1. 首先要按照上述 "开启本地通知功能" 步骤注册通知类型;
  2. 创建一个 UILocalNotification 对象;
  3. 设置 UILocalNotification 对象的 fireDate 属性,该属性表示什么时间点发送这条本地通知; 同时可以设置 timeZone 属性表示时区,设置 timeZone 后,当用户跨越时区时,fireDate 会按照时区被调整(类似于钟表调整); 此外,可以使用 repeatInterval 和 repeatCalendar 来设置周期性的通知。
  4. 设置通知的提示信息:
    • 设置 alertTitle 作为通知的概要,设置 alertBody 作为通知的具体信息;注意这里强烈建议使用本地化的字符串,即 NSLocalizedString(@"This is alert body", nil); 。 注意 alertTitle 属性只适用于 iOS8.2 及以后的系统
    • 设置 applicationIconBadgeNumber 用于展示 App 桌面图标的右上角角标;
    • 设置 soundName, 我们一般设置为 UILocalNotificationDefaultSoundName;使用自定义 sound 在后面会进一步讲解;
    • 在设置提醒方式的值时,对于 iOS8 及以后的系统,可以检查下当前提醒方式是否已经注册成功(可以用 [[UIApplication sharedApplication] currentUserNotificationSettings] 获取注册成功的通知类型)。
  5. 可以选择设置 userInfo 属性,该属性一般可以存放业务有关的信息(如 ID 等),这样收到通知后可以方便处理业务相关逻辑;
  6. 将上面创建的 UILocalnotification 放入通知队列中:使用方法 scheduleLocalNotification: 会按照 UILocalnotification 中的 fireDate 进行通知的发送, 而使用 presentLocalNotificationNow: 会立即发送该本地通知。

下面给出一段示例代码:

- (void)scheduleLocalNotification {
    NSDate *itemDate = [NSDate date];

    UILocalNotification *localNotif = [[UILocalNotification alloc] init];
    if (localNotif == nil)
        return;
    localNotif.fireDate = [itemDate dateByAddingTimeInterval:10];
    localNotif.timeZone = [NSTimeZone defaultTimeZone];

    localNotif.alertBody = [NSString stringWithFormat:NSLocalizedString(@"%@ after %i seconds scheduled.", nil), @"本地通知", 10];

    localNotif.alertTitle = NSLocalizedString(@"Local Notification Title", nil);

    localNotif.soundName = UILocalNotificationDefaultSoundName;
    localNotif.applicationIconBadgeNumber = 1;

    NSDictionary *infoDict = [NSDictionary dictionaryWithObject:@"ID:10" forKey:@"LocalNotification"];
    localNotif.userInfo = infoDict;

    [[UIApplication sharedApplication] scheduleLocalNotification:localNotif];
}

处理收到的本地通知

这里分三种情况讨论如何处理本地通知:

应用处于前台

应用处于前台时,本地通知到达时,不会有提示音、通知栏横幅提示,但是 App 桌面图标的右上角角标是有数值显示的,所以即使在前台,我们也应该对角标数量做处理 此时,我们可以在 application:didReceiveLocalNotification: 方法中获取到本地通知,示例代码如下:

- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
    NSString *itemName = [notification.userInfo objectForKey:@"LocalNotification"];
    [self.windowRootController displayNotification:[NSString stringWithFormat:@"%@ receive from didReceiveLocalNotificaition", itemName]];
    // 这里将角标数量减一,注意系统不会帮助我们处理角标数量
    application.applicationIconBadgeNumber -= 1;
}

应用处于后台

当应用处于后台时,本地通知到达时,会根据本地通知设置的通知类型以及用户设置的通知类型进行提示,例如锁屏界面通知、通知栏通知、声音、角标。 此时如果滑动锁屏界面通知或点击通知栏通知,则会切换应用到前台,我们可以使用与应用处于前台时相同的获取通知的方式。 但是如果我们点击 App 桌面图标,则无法获取到用户通知,此时通知栏消息仍然会存在。此外,角标也不会变化,如果希望修改角标,则需要 App 进入前台后将其修改。

应用没有运行

如果应用没有运行,当本地通知到达时,会根据本地通知设置的通知类型以及用户设置的通知类型进行提示,例如锁屏界面通知、通知栏通知、声音、角标。 此时如果滑动锁屏界面通知或点击通知栏通知,则会打开应用,但这时我们获取通知的方式与前面有所不同,通过 application:didReceiveLocalNotification: 是无法获取通知的。 这种情况我们需要通过 application:didFinishLaunchingWithOptions: 中的 LaunchOptions 获取通知,示例代码如下:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    UILocalNotification *localNotif = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
    if (localNotif) {
        NSString *itemName = [localNotif.userInfo objectForKey:@"LocalNotification"];
        [self.windowRootController displayNotification:[NSString stringWithFormat:@"%@ receive from didFinishLaunch", itemName]];  // custom method
        [UIApplication sharedApplication].applicationIconBadgeNumber = localNotif.applicationIconBadgeNumber-1;
    }

    // Your own other codes.
    return YES;
}

同样的,但是如果我们点击 App 桌面图标,则无法获取到用户通知,此时通知栏消息仍然会存在。此外,角标也不会变化,如果希望修改角标,则需要 App 进入前台后将其修改。

地理位置相关的本地通知

在 iOS8 及以后系统中,我们可以定义一个与地理位置有关的本地通知,这样当我们跨过设定的地理区域时,系统会发送本地通知。

注册位置相关的本地通知

  1. 需要创建一个 CLLocationManager 对象,并为其设置一个 delegate;
  2. 请求用户允许使用定位服务:调用 CLLocationManager 的 =requestWhenInUseAuthorization=, 注意工程的 plist 中需要配置 NSLocationWhenInUseUsageDescription 选项,否则定位服务无法正常启用;示例代码如下:

    - (void)registerLocationBasedNotification {
        CLLocationManager *locationManager = [[CLLocationManager alloc] init];
        locationManager.delegate = self;
        // 申请定位权限
        [locationManager requestWhenInUseAuthorization];
    }
    
  3. 通过 CLLocationManagerDelegate 回调检查用户是否允许使用定位服务,如果允许了服务,那么可以发送一个位置相关的本地通知。

    - (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
        // 因为上面我们是使用了 requestWhenInUseAuthorization,所以这里检查的是 kCLAuthorizationStatusAuthorizedWhenInUse
        if (status == kCLAuthorizationStatusAuthorizedWhenInUse) {
        [self scheduleLocalBasedNotification];
        }
    }
    
  4. 创建一个位置相关的本地通知,并将其交由系统处理。

    - (void)scheduleLocalBasedNotification {
        UILocalNotification *locationNotification = [[UILocalNotification alloc] init];
        locationNotification.alertBody = @"到达xxx";
        locationNotification.regionTriggersOnce = NO; // 表示每次跨越指定区域就会发送通知
        locationNotification.region = [[CLCircularRegion alloc] initWithCenter:LOC_COORDINATE radius:LOC_RADIUS identifier:LOC_IDENTIFIER];
    
        [[UIApplication sharedApplication] scheduleLocalNotification:locNotification];
    }
    

处理位置相关的本地通知

与上面讲过的 “处理收到的本地通知” 比较,这里可以在通知里获取到 region,然后可以做自定义操作,其余所有操作均与 “处理收到的本地通知” 一致。 注意如果用户没有允许使用定位权限,则无法收到位置相关的本地通知。

远程通知的使用

APNs 简介

APNs 是苹果提供的远程通知的服务器,当 App 处于后台或者没有运行时,如果 App 的服务器(之后我们称为 Provider)需要发送通知信息给客户端,则需要借助于 APNs 服务器。

使用 APNs 服务时,远程通知的路由路径为: Provider –> 苹果的 APNs 服务器 –> 手机设备 –> App。 在这个路径中,Provider 与 APNs 服务器之间有一个 TLS 连接,Provider 通过这个连接将远程通知推送到苹果的 APNs 服务器; 手机设备与 APNs 服务器之间也会有一个 TLS 连接,所有发往手机设备的 APNs 远程通知都是使用这一个 TLS连接,然后由设备区分远程通知所属的 App,进而通知给用户某应用有远程通知。

下面简单介绍下这个流程:

设备 与 APNs

设备与 APNs 建立连接的过程如图:

需要明确的要点:

  1. 此连接由系统建立并维持,无需开发人员管理;
  2. 上图中的证书是苹果设备本身的证书,与开发者账号中申请的证书无关;
  3. 每个设备与 APNs 服务器只需维持一条连接。

Provider 与 APNs

Provider 与 APNs 建立连接的过程如图:

需要明确的要点:

  1. 此连接由 App 的 bundle ID 唯一确定;
  2. 上图中 Provider certificate 需要通过开发者账号申请生成,其中包含 App 的 bundle ID。

APNs 工作的流程


  1. 首先客户端需要向 APNs 服务器注册当前 App,APNs 会返回一个 Token(注意这个过程要求 App 有合法的证书,有关证书这里不做详细描述);注意不同应用在同一设备上获取的 Token 不同,同一应用在不同设备上获取的 Token也不同,所以 Token 是跟设备与 App 唯一绑定的;
  2. App 拿到 Token 后需要将其发送给 Provider;
  3. Provider 发送推送通知时,指定 Token 和通知内容,并发送给 APNs 服务器;
  4. APNs 服务器会将通知发送给 Token 对应的设备上;
  5. 设备收到通知后,根据 APNs 发过来的通知中带有的 bundleID 信息区分是哪个App的远程通知(这里应该是根据 Token 来获取 bundleID)。

Feedback 机制

Feedback 是 APNs 服务器提供的用于减少服务器压力以及优化网络的服务,基本的工作流程如下图:

  1. Provider 发送一个远程通知给 APNs 服务器,APNs 服务器会检测目的设备是否在线,如果不在线,那么 APNs 服务器会暂存该消息;
  2. 当目的设备上线后,APNs 会发送暂存的消息给目的设备(按照苹果官方说法暂存消息只会暂存最后一条消息,之前的消息会被丢弃);
  3. 如果目的设备很久都没有上线,那么 APNs 消息会把该设备加入 feedback 名单。Provider 可以定期去 APNs 拉新 feedback 名单;
  4. 当 Provider 再次给之前的设备发送远程通知时,需要检查一下 feedback 名单,如果设备在这个名单,则不再发送给 APNs 了;
  5. 当设备重新上线后,Provider 可以再将此设备移除 feedback 名单,当 Provider 更新 feedback list 后,就可以重新给该设备发送远程通知了。当然,feedback list 的更新可能会有周期,如果需要及时有效的更新 feedback list,那么需要 App 打开后,及时通知 Provider;
  6. 这种机制的好处就是防止发送多余无用的远程通知消息,一方面可以减缓 APNs 服务器的压力,另一方面也可以减少网络流量;
本文来自网易实践者社区,经作者白天宇授权发布。