Client technology: This article will take you to understand the iOS message push mechanism

I. Overview



Message push is an important way for apps to deliver information to users. Regardless of whether the app is running or not, users can receive push messages as long as the user has turned on the notification permission. Developers can initiate local message pushes by calling the iOS system methods. For example, our most common alarm clock application, the App can directly initiate local notifications based on the locally stored alarm clock information, so even if there is no network, you can receive alarm reminders. For remote message push, the business server sends the message content in a fixed format to the Apple Push Notitfication service (APNs), and then pushes it to the user’s device via Apple’s APNs server. For example, Tencent News can push current affairs hot news to users. The QQ mailbox can push users to receive new email reminders, and the game App can notify players of new game benefits in this way. It can not only notify users of important information in time, but also prompt users to open or wake up the App through push messages, thereby increasing the utilization rate of the App. In addition to fixed push parameters such as title, content, prompt sound, and corner number, developers can also add custom parameters to the push message, so that users can go directly to the relevant news, email or welfare page when they click on the push message, providing better User experience and page exposure.





Two, XCode configuration



Before using message push related functions, we first need to prepare a certificate that supports the push function. Individual developers can refer to Tencent Cloud's TPNS document  [1] to configure and export the push certificate in the Apple Developer Center. In addition, you need to increase the message push permission in the Signing & Capabilities configuration of the XCode project configuration. After the operation is completed, Xcode will automatically generate or update the project’s entitlements file and add the APS Environment field as shown in the figure.


Three, apply for message push permission




Regardless of whether it is a local push or a remote push, you must first apply for push permission from the user before pushing, and the push message can only be received after the user is authorized. Apple introduced the UserNotifications framework in iOS10, encapsulated and upgraded push related functions. In addition to some basic local and remote message push functions that UIApplication could do before, it also added withdrawing or modifying push messages and custom notification UI. , Push message front-end display and other functions. In iOS10 and above, Apple recommends that developers use: requestAuthorizationWithOptions:completionHandler: method to apply for message push permission from users. This method needs to specify a UNAuthorizationOptions type parameter used to describe the push permission, including alert (the title, text, etc. of the message), sound (the message prompt), badge (the corner label displayed in the upper right corner of the App); it can also be used in this method The completionHandler callback method uses the granted parameter to determine whether the user has allowed authorization. The relevant code is as follows:




#import <UserNotifications/UserNotifications.h>……[[UNUserNotificationCenter currentNotificationCenter]requestAuthorizationWithOptions:UNAuthorizationOptionSound|UNAuthorizationOptionAlert|UNAuthorizationOptionBadgecompletionHandler:^(BOOL granted, NSError * _Nullable error) {if(granted){ //User allowed push permission application {//User rejected the push permission request}}];


In iOS9, you can directly use the registerUserNotificationSettings method of UIApplication. This method also needs to configure parameters such as sound, alert, and badge, but it does not provide a callback method for judging whether the user clicked on authorization or rejected. The relevant code is as follows:


[[UIApplication sharedApplication] registerUserNotificationSettings: [UIUserNotificationSettings settingsForTypes:  (UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge)                                   categories:nil]];

It should be noted that whether it is UserNotifications or UIApplication's method of applying for push permissions, the system pop-up window for applying for user authorization above will only be displayed once. iOS will record the user's authorization status for the App, and will not repeatedly apply for authorization from the user. Message push is an important function of the app, and it is also a good operation method. Therefore, many apps will check the authorization status of message push after launch. If the user rejects the message push permission, it will still remind the user with a certain frequency. , And then turn on the push permission of the App in the iOS settings center. The relevant code is as follows:



if(@available(iOS 10.0,*)){ [[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {if (UNAuthorizationStatusDenied == settings.authorizationStatus) {//The user refuses to push the message and the pop-up prompts the user Authorize in the system settings UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Not open the push function" message:@"Please allow notification in the \"Settings-App-Notification\" option of the device" preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction* cancel = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction* action){ [alert dismissViewControllerAnimated: YES completion: nil]; }]; UIAlertAction* ok = [UIAlertAction actionWithTitle:@"Go to set" style: UIAlertActionStyleDefault handler:^(UIAlertAction* action){                [alert dismissViewControllerAnimated: YES completion: nil];                NSURL * url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];                if([[UIApplication sharedApplication] canOpenURL:url])                {                    NSURL*url =[NSURL URLWithString:UIApplicationOpenSettingsURLString];                    [[UIApplication sharedApplication] openURL:url];                }            }];            [alert addAction: cancel];            [alert addAction: ok];            [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated: YES completion: nil];        }    }];}else{    UIUserNotificationSettings *setting = [[UIApplication sharedApplication] currentUserNotificationSettings];    if (UIUserNotificationTypeNone == setting.types) {//The user refuses to push the message, the processing method is the same as above }}

四、本地推送




在 iOS10 中,UserNotifications 框架为我们提供了 UNMutableNotificationContent 对象描述消息推送的标题、内容、提示音、角标等内容。UNNotificationTrigger 对象描述消息推送的推送时间策略,UNNotificationRequest 对象整合推送内容和时间。
每个 Request 对象都需要配置一个 id 来标识该条推送内容,UNUserNotificationCenter 通过该 id 来管理(包括增加、删除、查询和修改)所有的 Request。
UNNotificationTrigger 有四个子类,分别是 UNTimeIntervalNotificationTrigger 用于通过时间间隔控制消息推送;UNCalendarNotificationTrigger 通过日期控制消息推送;UNLocationNotificationTrigger 通过地理位置控制消息推送;UNPushNotificationTrigger 远程消息推送对象。相关代码如下:


//推送内容UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];content.title = @"推送标题";content.body = @"推送内容";content.sound = [UNNotificationSound defaultSound];//默认提示音//日期推送,今日15:53:00推送本地消息NSDateComponents* date = [[NSDateComponents alloc] init];date.hour = 15;date.minute = 53;UNCalendarNotificationTrigger* calendarTrigger = [UNCalendarNotificationTrigger       triggerWithDateMatchingComponents:date repeats:NO];//倒计时推送,2s后推送本地消息UNTimeIntervalNotificationTrigger *intervalTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:2 repeats:NO];UNNotificationRequest* request = [UNNotificationRequest       requestWithIdentifier:@"testId" content:content trigger:calendarTrigger];//将推送请求添加到管理中心才会生效UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {   if (error != nil) {       NSLog(@"%@", error.localizedDescription);   }}];

在 iOS9 中,UIApplication 提供了 presentLocalNotificationNow 和 scheduleLocalNotification 两个本地消息推送的方法。分别表示立即推送和按照固定日期推送,UILocalNotification 同时描述了消息内容和推送的时机。
示例代码是一个 2s 后推送的本地消息,soundName 属性用于描述消息的提示音,用户可以自定义提示音(需要将音频文件打包到安装包中)或者使用默认提示音乐,repeatInterval 和 repeatCalendar 属性分别用于根据时间差和日期进行重复提示的操作。相关代码如下:


UILocalNotification *notification = [[UILocalNotification alloc] init];notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:2];notification.alertTitle = @"推送标题";notification.alertBody = @"推送内容";//notification.soundName = UILocalNotificationDefaultSoundName;notification.soundName = @"mysound.wav";[[UIApplication sharedApplication] scheduleLocalNotification:notification];

五、远程推送




不同于本地消息推送不依赖网络请求,可以直接调用 iOS 系统方法,远程消息推送的实现涉及到用户设备、我们自己的业务方服务器和苹果的 APNs 服务的交互。
不同于 Android 系统中远程消息推送的实现,需要 App 自身通过后台服务与业务服务器维持长链接通信,iOS 中的消息推送是操作系统与苹果的 APNs 服务器直接交互实现的,App 自身并不需要维持与服务器的连接。
只要用户开启了推送权限,我们的业务服务器就可以随时通过调用 APNs 服务向用户推送通知,这样既能够为开发者和用户提供安全稳定的推送服务,也够节省系统资源消耗,提高系统流畅度和电池续航能力。

iOS 客户端远程消息推送的实现可以分为以下几个流程:

  • 用户的 iphone 通过 iOS 的系统方法调用与苹果的 APNs 服务器通信,获取设备的 deviceToken,它是由 APNs 服务分配的用于唯一标识不同设备上的不同 App,可以认为是由 deviceID、bundleId 和安装时的相关信息生成的,App 的升级操作 deviceToken 不变,卸载重装 App、恢复和重装操作系统后的 deviceToken 会发生变化。
  • 苹果的 APNs 服务是基于 deviceToken 实现的,因此需要将设备的 deviceToken 发送到我们的业务服务器中,用于后续的消息推送。一个设备可能登录过多个用户,一个用户也可能在多个设备中登录过,当我们需要给不同用户推送不同的消息时,除了 deviceToken 之外,我们还需要保存用户的 openid 与 deviceToken 的映射关系。我们可以在用户登录成功后的时机更新 openid 和 deviceToken 的映射关系,用户退出后取消映射关系,只保存用户最后登录设备的 deviceToken,避免一个设备收到多个重复通知和一个用户在不同设备收到多个通知等情况。
  • 在新闻类 App 出现事实热点新闻时,后台服务就可以携带消息内容和 deviceToken 等内容,向苹果的 APNs 服务发起消息推送请求,推送消息的实现是异步的,只要请求格式和 deviceToken 检查通过APNs服务就不会报错,但是用户还是可能因为网络异常或者关闭了推送权限等原因收不到推送消息。
  • APNs 服务向用户设备推送消息这一步也是异步的,在用户关机或网络异常收不到推送的情况下,APNs 会为每个 deviceToken 保留最后一条推送消息,待网络恢复后再次推送。


1. 获取设备deviceToken


在 App 启动时,我们可以通过 UIApplication的registerForRemoteNotifications 方法向苹果的 APNS 服务器请求 deviceToken。
如果请求成功,则 didRegisterForRemoteNotificationsWithDeviceToken 回调方法会被执行,为了便于业务服务器的调用,我们一般会将二进制的 deviceToken 转换为 16 进制的字符串后再进行存储。
如果请求失败,则 didFailToRegisterForRemoteNotificationsWithError 方法也会被调用,并附带具体的错误信息。相关代码如下:


//调用系统方法请求deviceToken- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    [[UIApplication sharedApplication] registerForRemoteNotifications];}//deviceToken获取成功的回调- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{    NSString *deviceTokenStr;    NSUInteger length = deviceToken.length;    if (![deviceToken isKindOfClass:[NSData class]] || length == 0) {        return;    }    const unsigned char *bytes = (const unsigned char *)deviceToken.bytes;    NSMutableString *hex = [NSMutableString new];    for (NSInteger i = 0; i < deviceToken.length; i++) {        [hex appendFormat:@"%02x", bytes[i]];    }    deviceTokenStr = [hex copy];    NSLog(@"%@", deviceTokenStr);}//deviceToken获取失败的回调- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{    NSLog(@"error,%@",error);}

2. 后台调用APNs推送


业务方服务器调用 APNs 服务时首先要建立安全连接,进行开发者身份的认证,分为基于证书(Certificate-Based)和基于Token(Token-Based)的认证两种方式,比较常用的是基于证书的认证方式。
推送证书分为开发环境和生产环境的证书,分别对应不同的 APNs 推送接口,我们从苹果开发者平台或者第三方平台导出的推送证书一般有 p12 和 pem 两种格式的文件,为了便于接口调用我们可以通过以下命令将 p12 格式的文件转换为 pem 证书。


openssl pkcs12 -in push_dev.p12 -out push_dev.pem -nodes

 基于证书建立 TLS 连接的流程如下图所示:

  • 业务方服务器(Provider)向APNs服务器发起建立TLS连接的请求。
  • APNs服务器返回的它的证书,供业务方服务器校验。
  • 业务方服务器提供自己的推送证书,供APNs服务器校验。
  • APNs服务器验证业务方服务器提供的推送证书无误后,TLS连接就已经建立完成,之后业务方服务器就可以直接向APNs发送消息推送请求了。


业务方与 APNs 建立请求的简易实现的 PHP 代码实现如下:


$deviceToken= '22124c450762170ca2ddb32a50381dd2c3026dbdb020f6dddcabefdca724fdd6';//dev params$devUrl = 'ssl://gateway.sandbox.push.apple.com:2195';$devCertificate = 'push_dev.pem';//product params$proUrl = 'ssl://gateway.push.apple.com:2195';$proCertificate = 'push_pro.pem';// Change 2 : If any$title = '标题';//消息标题$content = '消息内容';//内容$ctx = stream_context_create();// Change 3 : APNS Cert File name and location.stream_context_set_option($ctx, 'ssl', 'local_cert', $devCertificate);// Open a connection to the APNS server$fp = stream_socket_client($devUrl, $err, $errstr, 60, STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT, $ctx);if (!$fp)    exit("Failed to connect: $err $errstr" . PHP_EOL);echo 'Connected to APNS' . PHP_EOL;// Create the payload body$body['aps'] = array(    'alert' =>array(        'title'=>$title,        'body'=>$content    ),    'sound' => 'default'    );//自定义内容$body['userInfo'] = array(    'url' => 'https://www.qq.com',);// Encode the payload as JSON$payload = json_encode($body);// Build the binary notification$msg = chr(0) . pack('n', 32) . pack('H*', $deviceToken) . pack('n', strlen($payload)) . $payload;// Send it to the server$result = fwrite($fp, $msg, strlen($msg));//发送多个就调用多次fwrite//$result = fwrite($fp, $msg, strlen($msg));echo $msg;if (!$result)    echo 'Message not delivered' . PHP_EOL;else    echo 'Message successfully delivered' . PHP_EOL;// Close the connection to the serverfclose($fp);

业务方服务器通过证书与 APNs 建立安全连接后可以进行连续多次的消息推送操作,每次消息推送都要指定 deviceToken 和 Payload 参数。
Payload 是一个 json 对象,用于配置 iOS 在收到远程消息推送时的展现形式,aps 参数包含了苹果预设的 alert、sound、badge 等参数,其中 alert 参数可以是字符串,或者包含 title、body 等参数的字典类型;badge 参数使用整形设置 App 图标右上角显示的数字,badge 设置为 0 时角标不会显示;sound 参数用于设置推送的声音,不传该参数或者传递空字符串则推送不会发出提示音,设置为 default 时使用系统默认提示音,也可以设置为具体的音频文件名,需要提前音频文件放到项目的 bundle 目录,且时长不能超过 30s。
除了预设参数以外,我们还可以在 aps 的同级自定义一些参数,这些参数也可以是字典类型,再嵌套其他参数,例如示例代码中我们自定义的 userInfo 对象,但是一般推送消息的 payload 不宜过大,应控制在 4K 以内,建议只透传一些 id 和 url 等关键参数,具体的内容由客户端在收到推送时再去通过网络请求获取。


{    "aps" : {        "alert" : {            "title" : "Game Request",            "subtitle" : "Five Card Draw",            "body" : "Bob wants to play poker",        },        "badge" : 9,        "sound" : "gameMusic.wav",    },    "gameID" : "12345678"}

上述 payload 包含了常见的推送消息的标题、副标题、内容、消息提示音、App 的角标数字等预设参数,以及一个开发者自定义的 gameID 参数。用户点击推送消息后会自动启动或从后台唤醒 App,我们可以在系统的回调方法中获取到自定义参数,并根据 gameID 自动为用户打开该游戏页面。

3. 消息推送调试工具


在进行 APNs 接口调试时,我们可以利用一些优秀的推送调试工具帮助我们验证 payload 或证书等内容的合法性。本文介绍两款比较流行的开源软件,分别是国外的 Knuff 和国内开发者维护的 smartPush。
图片

  • Knuff:https://github.com/KnuffApp/Knuff
  • SmartPush:https://github.com/shaojiankui/SmartPush


六、App推送消息的处理



在 iOS10 中,UserNotifications 框架为开发者提供了 UNUserNotificationCenterDelegate 协议,开发者可以通过实现协议中的方法,在 App 接收到推送消息和用户点击推送消息时进行一些业务逻辑的处理。
无论是本地推送还是远程推送的消息,App的运行状态都可能处于以下三种状态:
  • App 正在前台运行,此时用户正在使用 App,收到推送消息时默认不会弹出消息提示框,willPresentNotification 回调方法会被调用,开发者可以从 UNNotification 对象中获取该推送消息的 payload 内容,进而获取自定义参数,然后显示一个自定义弹窗提示用户收到了新的消息;也可以在 willPresentNotification 方法中通过 completionHandler 函数的调用让推送消息直接在前台显示,用户点击前台显示的推送消息时,didReceiveNotificationResponse 回调方法也会被执行。
  • App 在后台运行,此时用户点击推送消息会将 App 从后台唤醒,didReceiveNotificationResponse 回调方法会被执行,开发者可以在该方法中获得 payload,解析自定义参数并自动打开对应的页面。
  • App 尚未启动,此时用户点击推送消息会打开 App,开发者可以从 launchOptions 中获取本地或远程推送消息中的自定义参数,待页面初始化完成后进行相关页面的跳转。



#import <UserNotifications/UserNotifications.h>@interface AppDelegate ()<UNUserNotificationCenterDelegate>@end
@implementation AppDelegate//在App启动后就将AppDelegate对象配置为NotificationCenter的delegate- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    [UNUserNotificationCenter currentNotificationCenter].delegate = self;    // NSDictionary *localNotification = [launchOptions valueForKey:UIApplicationLaunchOptionsLocalNotificationKey];    NSDictionary *remoteNotification = [launchOptions valueForKey:UIApplicationLaunchOptionsRemoteNotificationKey];    if(remoteNotification){        //app已退出,点击拉起了app        NSDictionary *params = userInfo[@"userInfo"];        //此时NavigationController还未初始化,可以先暂存参数,稍后跳转        [PageSwitch handlePushSwitch:params];    }}//用户点击推送消息的回调- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler API_AVAILABLE(ios(10.0)){    UNNotification *noti = ((UNNotificationResponse *)response).notification;    NSDictionary *userInfo = noti.request.content.userInfo;    NSDictionary *params = userInfo[@"userInfo"];    //根据消息推送中的参数,在用户点击推送时自动进行跳转    [PageSwitch handlePushSwitch:params];}//App在前台运行时收到推送消息的回调- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(nonnull UNNotification *)notification withCompletionHandler:(nonnull void (^)(UNNotificationPresentationOptions))completionHandler API_AVAILABLE(ios(10.0)){    //可以让App在前台运行时也能收到推送消息    completionHandler(UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionAlert);}

在 iOS9 中,UIApplication 提供了下面三个消息推送的处理方法,分别是远程消息推送、远程静默推送和本地消息推送的回调处理方法。
前两个回调方法都能够用于 App 远程消息推送的处理,同时使用时只有远程静默推送方法会被调用,当 payload 包含参数 content-available=1 时,该推送就是静默推送,静默推送不会显示任何推送消息,当 App 在后台挂起时,静默推送的回调方法会被执行,开发者有 30s 的时间内在该回调方法中处理一些业务逻辑,并在处理完成后调用 fetchCompletionHandler。



//远程消息推送回调方法,ios(3.0, 10.0)- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo;//远程静默推送回调方法,ios(7.0, *)- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo     fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler API_AVAILABLE(ios(7.0));//本地消息推送回调方法,ios(4.0, 10.0)-(void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification;

UIApplication 中的这三个方法在以下两种场景下都会被调用:
  • App 在前台运行时收到通知;
  • App 在后台运行时用户点击推送消息拉起 App。


区别是前两种方法对应远程消息推送的接收和点击触发响应,didReceiveLocalNotification 用于本地消息推送。我们可以通过 UIApplication的applicationState 属性来判断 App 是否在前台运行,然后分别实现:

  • 用户点击消息唤起后台App并打开对应页面;
  • 用户前台使用App时显示自定义弹窗。





- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo{    if([UIApplication sharedApplication].applicationState == UIApplicationStateActive){        NSLog(@"在前台,%@",userInfo);    }else{        NSLog(@"从后台进入前台,%@",userInfo);        NSDictionary *params = userInfo[@"userInfo"];        if([Tools isValidString:params[@"url"]]){            NSString *routeUrl = params[@"url"];            [PageSwitch handlePushSwitch:params];        }    }}

七、结语



本文首先介绍了消息推送相关的工程配置和推送权限的申请,然后分别介绍了本地和远程消息推送的不同使用场景和实现方法,最后介绍了 App 在收到推送消息后的相关回调方法和处理逻辑。
在实际的项目开发中,我们往往会选择腾讯云推送或极光推送等更加成熟的第三方消息推送平台,这些平台都提供了相对完善的推送和数据统计服务,通过接口和 SDK 屏蔽了底层逻辑的实现,通过对 iOS 消息推送的实现过程的了解也能够帮助我们更好的使用这些平台。
由于时间的关系,自己的研究并不深入,如有疏漏和错误,欢迎留言指正交流~


Guess you like

Origin blog.51cto.com/15060467/2678855