系统推送的集成(一) —— 基本集成流程(一)

版本记录

版本号 时间
V1.0 2018.06.21

前言

我们做APP很多时候都需要推送功能,以直播为例,如果你关注的主播开播了,那么就需要向关注这个主播的人发送开播通知,提醒用户去看播,这个只是一个小的方面,具体应用根据公司的业务逻辑而定。前面已经花了很多篇幅介绍了极光推送,其实极光推送无非就是将我们客户端和服务端做的很多东西封装了一下,节省了我们很多处理逻辑和流程,这一篇开始,我们就利用系统的原生推送类结合工程实践说一下系统推送的集成,希望我的讲解能让大家很清楚的理解它。

几个需要提前知道的问题

1. 推送流程是如何的?

推送是苹果系统提供的功能,其实基本流程就是我们的客户端要用deviceToken在苹果服务器进行注册,并将这个deviceToken发送给我们的服务端绑定我们的账号,服务端将要推送的内容发送给苹果,苹果就会将定制化的内容根据deviceToken发送给我们客户端,我们就收到推送了。

也就是说,我们客户端除了注册PUSH,接收苹果给我们的deviceToken,发送给我们服务端和当前账号绑定以外,我们除了等着收到PUSH进行处理就决定不了别的事情了,至于推送什么是我们服务端决定的,什么时候能收到就看苹果服务器了。

下面这张图就是很好的说明了推送的原理,图片源于网络。

PUSH原理

2. deviceToken是如何获取的?有什么用?

deviceToken是我们用苹果的API注册PUSH的时候,苹果的服务器发给我们的,我们在代理方法- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken中进行接收。那么这个是做什么的呢?这个就是苹果发送个性化推送找到你机器的凭证,有了它,苹果系统就知道这个消息要推送给你。

我们在代理方法中收到推送后,一般要将这个deviceToken发送给我们的服务器,让服务器将这个deviceToken和你当前App的账号进行绑定,这样才能进行个性化的推送和消息订制。

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
    NSLog(@"push - didRegisterForRemoteNotificationsWithDeviceToken");

    NSString * deviceid = [NSString stringWithFormat:@"%@", deviceToken];
    deviceid = [deviceid stringByReplacingOccurrencesOfString:@"<" withString:@""];
    deviceid = [deviceid stringByReplacingOccurrencesOfString:@">" withString:@""];
    deviceid = [deviceid stringByReplacingOccurrencesOfString:@" " withString:@""];
    
    NSLog(@"push - deviceToken = %@", deviceid);
    // 这个是我自己写的单例,用来存储关于App的一些基本信息,这里是用NSUserDefaults存储当前收到的deviceToken
    [JJAppPrefs setDeviceToken:deviceid];

    //这个是我写的一个单例,用来专门处理Push相关的,这个就是绑定token,将这个收到的deviceToken发送给你的服务器,
    // 服务器进行绑定,你只能知道绑定成功或者失败,不用做其他的。
    [[JJPushManager manager] bindDeviceToken];
}

3. 客户端要如何开启推送?

这个是一个开关,只要点击一下就可以了,就能开启推送了。

开启推送

4. 关于证书要做什么?

我们做推送是要证书的,让有账号的人生成两个证书,一个是测试环境证书,一个是生产环境证书。

证书

具体如何生成证书可以看网上的资料,跟着一步步的做就可以了,这里就不多说了。


注册PUSH

截止目前,iOS已经有了11的系统,注册PUSH从10系统以后就要考虑版本的差异了。

如果你是在iOS10以前做PUSH是不用这么麻烦的,但是随着iOS10的更新,一个新类UNUserNotificationCenter出来了,所以要根据版本的不同进行不同的注册逻辑。

iOS10及其以后版本

//10及其以后系统使用下面方法进行注册

UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center setDelegate:self];
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert |
                                         UNAuthorizationOptionBadge |
                                         UNAuthorizationOptionSound)
                      completionHandler:^(BOOL granted, NSError * _Nullable error) {
                          if (granted) {
                              NSLog(@"push - 注册成功");
                          }
                          else{
                              NSLog(@"push - 注册失败");
                          }
                      }];
[[UIApplication sharedApplication] registerForRemoteNotifications];

这里,[[UIApplication sharedApplication] registerForRemoteNotifications];一定不能少,要不你是收不到deviceToken的。因为你不写这句话表示你没有注册。

同时,你要设置好代理@interface AppDelegate () <UNUserNotificationCenterDelegate>,否则你处理PUSH的方法都不会进行回调的。

iOS9及其以前版本

iOS9及其以前版本使用类UIUserNotificationSettings进行注册PUSH的。

//9及其以前系统使用下面方法进行注册

else if ([UIUserNotificationSettings self]) {
    id settings = [UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeAlert |
                                                                UIUserNotificationTypeSound |
                                                                UIUserNotificationTypeBadge)
                                                    categories:nil];
    [[UIApplication sharedApplication] registerUserNotificationSettings:settings];
    [[UIApplication sharedApplication] registerForRemoteNotifications];
}
else {
    [[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge |
                                                                           UIRemoteNotificationTypeSound |
                                                                           UIRemoteNotificationTypeAlert)];
}

这里有几个小的问题,UIUserNotificationSettings在10系统出现就废弃了这个类。

NS_CLASS_DEPRECATED_IOS(8_0, 10_0, "Use UserNotifications Framework's UNNotificationSettings") __TVOS_PROHIBITED
@interface UIUserNotificationSettings : NSObject

这里大家也注意到了这个方法

- (void)registerForRemoteNotificationTypes:(UIRemoteNotificationType)types NS_DEPRECATED_IOS(3_0, 8_0, "Use -[UIApplication registerForRemoteNotifications] and UserNotifications Framework's -[UNUserNotificationCenter requestAuthorizationWithOptions:completionHandler:]") __TVOS_PROHIBITED;

可以看见,这个方法在iOS 8.0也已经废弃了,改用上面提示的那两个方法了。

上面这两种注册方法,需要像iOS10以上的那个方法设置代理吗?简单来说,不用你去做,他们的代理叫UIApplicationDelegate,系统已经给你设置好了。也就是说10以前的系统直接就可以写代理方法进行相应的处理了,不用再进行遵循代理这一个步骤了。

// 这个系统已经给你写好了

@interface AppDelegate : UIResponder <UIApplicationDelegate>

获取deviceToken

上面你去注册PUSH,注册成功后,苹果服务器就会返回给你deviceToken,在下面几个代理方法中获取deviceToken的状态已经进行其他处理。

- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings
{
    NSLog(@"push - didRegisterUserNotificationSettings");
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
    NSLog(@"push - didRegisterForRemoteNotificationsWithDeviceToken");

    NSString * deviceid = [NSString stringWithFormat:@"%@", deviceToken];
    deviceid = [deviceid stringByReplacingOccurrencesOfString:@"<" withString:@""];
    deviceid = [deviceid stringByReplacingOccurrencesOfString:@">" withString:@""];
    deviceid = [deviceid stringByReplacingOccurrencesOfString:@" " withString:@""];
    
    NSLog(@"push - deviceToken = %@", deviceid);
    // 这个是我自己写的单例,用来存储关于App的一些基本信息,这里是用NSUserDefaults存储当前收到的deviceToken
    [JJAppPrefs setDeviceToken:deviceid];

    //这个是我写的一个单例,用来专门处理Push相关的,这个就是绑定token,将这个收到的deviceToken发送给你的服务器,
    // 服务器进行绑定,你只能知道绑定成功或者失败,不用做其他的。
    [[JJPushManager manager] bindDeviceToken];
}

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
    NSLog(@"push - didFailToRegisterForRemoteNotificationsWithError = %@", error);
}

这里我们在代理方法中收到deviceToken,首先进行本地存储,然后在发给服务器进行和当前账号的绑定。


收到PUSH的处理

和注册PUSH一样,对于PUSH的处理也是要分系统版本的。

1. 代理方法的调用

iOS 10及其以后

//iOS10新增
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler API_AVAILABLE(ios(10.0))
{
    NSLog(@"push - didReceiveNotificationResponse = %@", response);

    [[JJPushManager manager]  jj_userNotificationCenter:center didReceiveNotificationResponse:response];

    completionHandler();
}

//iOS10新增
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler API_AVAILABLE(ios(10.0))
{
    NSLog(@"push - willPresentNotification");

    [[JJPushManager manager] jj_userNotificationCenter:center willPresentNotification:notification];

    completionHandler(UNNotificationPresentationOptionBadge|
                      UNNotificationPresentationOptionSound|
                      UNNotificationPresentationOptionAlert);
}

这里有几个问题需要注意:

  • JJPushManager是我自己写的单例,用来处理接收到的PUSH以及绑定和解绑deviceToken。这里在单例分出来进行处理,可以减少AppDelegate中代码的冗杂,分离出单利进行处理更清晰。

  • 关于iOS10这两个方法,收到横幅的时候- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler API_AVAILABLE(ios(10.0))方法会调用,用户点击查看横幅的时候- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler API_AVAILABLE(ios(10.0))会进行调用。

  • 对于iOS10以后的系统,无论App在前台、后台或者是杀死都会收到推送的横幅。

这样就收到开播通知了,下一步就是点击进行处理了。

iOS 9及其以后

这里接收通知的就一个方法

//iOS9及其以后方法
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
    NSLog(@"push - didReceiveRemoteNotification = %@", userInfo);
 
    [[JJPushManager manager] jj_application:application didReceiveRemoteNotification:userInfo];
}

这里有几个问题需要注意:

  • iOS9系统当App在前台的时候是没有横幅的,这个时候就需要我们进行特殊的处理,一个方案是模拟成本地通知进行展示,另外一个就是弹出系统的AlertView,然后进行相关的业务逻辑处理,比如跳进某个主播的直播间。

2. PUSH处理

PUSH我统一在自己写的单例JJPushManager中进行的处理。

@interface JJPushManager()

@property (nonatomic, strong) NSDictionary *currentPushDict;
@property (nonatomic, strong) JJFeed *feed;

@end

@implementation CPPushManager

#pragma mark -  Class Public Function

+ (instancetype)manager
{
    static JJPushManager *pushManager;
    static dispatch_once_t predicate;
    dispatch_once(&predicate, ^{
        pushManager = [[JJPushManager alloc] init];
        [[NSNotificationCenter defaultCenter] addObserver:pushManager selector:@selector(handlePushWhenApplicationActive) name:UIApplicationDidBecomeActiveNotification object:nil];
    });
    return pushManager;
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

#pragma mark -  Object Private Function

//根据信息数据返回Feed模型
- (CPFeed *)gainFeedInfo
{
    self.feed = [[JJFeed alloc] initWithDict:self.currentPushDict];
    return self.feed;
}

- (void)initCurrentPush:(NSDictionary *)currentPushDict
{
    if (!currentPushDict) {
        return;
    }
    
    self.currentPushDict = currentPushDict;
}

//直接跳到直播间
- (void)gotoPlayingRoom
{
    // 未登录时,不处理push信息
    if ([JJUserManager loadUserPref] == NO) {
        return;
    }
    
    CPFeed *feedInfo = [self gainFeedInfo];
    
    if (!feedInfo) {
        return;
    }
    
    JJViewController *vc = (JJViewController *)JJCurrentNaviController.topViewController;
    if ([vc isKindOfClass:[JJPlayingViewController class]]) {
        return;
    }
    
    //直接跳转到直播间
    JJPlayingViewController *playVC = [[JJPlayingViewController alloc] initWithFeed:feedInfo];
    [CPCurrentNaviController pushViewController:playVC animated:YES];
}

#pragma mark -  Object Public Function

//iOS10以下使用这个方法接收通知
- (void)jj_application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
    [self initCurrentPush:userInfo];
    if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
        JJViewController *vc = (JJViewController *)JJCurrentNaviController.topViewController;
        if ([vc isKindOfClass:[JJPlayingViewController class]]) {
            return;
        }
        [self gainFeedInfo];
        NSString *titleStr = [NSString stringWithFormat:@"快来围观,%@开播啦~", self.feed.anchorPrefs.userNickName];
        IMP_WSELF();
        [vc showAlertViewWithTitle:titleStr message:nil cancelAction:nil ensureAction:^{
            [wself gotoPlayingRoom];
        }];
    }
}

//iOS10新增
- (void)jj_userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response API_AVAILABLE(ios(10.0))
{
    NSDictionary * userInfo = response.notification.request.content.userInfo;
    [self initCurrentPush:userInfo];
    if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
        [self gotoPlayingRoom];
    }
}

//iOS10新增
- (void)jj_userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification API_AVAILABLE(ios(10.0))
{
    NSDictionary * userInfo = notification.request.content.userInfo;
    [self initCurrentPush:userInfo];
}

#pragma mark -  Action && Notification

//进入Active消息通知
- (void)handlePushWhenApplicationActive
{
    if (!self.currentPushDict) {
        return;
    }

    [self gotoPlayingRoom];
    self.currentPushDict = nil;
}

注意:上面只是简单的逻辑,更复杂的处理根据产品的需求进行额外的处理。


几个其他问题

1. 关于测试

开发和QA在进行测试的时候,都应该使用推送的开发证书进行测试,直接用正式证书测试是收不到的。

2. 关于到达率

利用开发证书进行推送测试,有的时候到达率并不是100%,我用11系统试了一下,基本是100%,用9系统测试,很大几率就收不到,偶尔可以收到,这个和苹果测试环境证书有关系,正式环境就没事了,这个我们客户端控制不了,也不用担心,这并不是bug。

3. 关于用户登录和退出

这个需要额外的处理,当用户登录(不管用手机号还是三方)都需要重新注册下PUSH,绑定deviceToken,当用户退出登录的时候,需要发送到服务端解绑deviceToken并移除deviceToken的本地存储。

登录

[(AppDelegate *)([UIApplication sharedApplication].delegate) registerPushNotifications];

退出登录

[[JJPushManager manager] unbindDeviceToken];
[JJUserManager clearData];

参考资料

1. IOS Push消息推送原理及应用

后记

本篇主要讲述了系统PUSH的相关集成及问题,感兴趣的给个赞或者关注~~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容