一郎楼、前言
iOS中消息推送有兩種方式,本地推送和遠程推送敦姻。本地推送在iOS中使用本地通知為你的APP添加提示用戶功能這篇博客中有詳細的介紹耕蝉,我們在此主要討論遠程推送的流程與配置過程以及注意事項。
官方文檔:Local and Remote Notification Programming Guide
二佛嬉、簡介
什么是遠程推送逻澳?
- 蘋果提供的一項給終端設備推送消息的服務
為何使用遠程推送?
- 當用戶打開應用程序的通知中心之后暖呕,蘋果遠程推送服務器就能把消息推送到裝有該應用的設備上斜做,具有強制性、實時性的特點湾揽,并且用戶無需打開應用都能收到推送的消息瓤逼。
三、遠程推送原理
說到遠程推送不得不說下下面這張圖库物,下面這張圖把遠程推送的過程大致描述勾畫了一遍霸旗,在此解析一下下圖。
名詞解釋:
- Provider:消息提供者戚揭,一般是我們的后臺服務器或者第三方推送服務器后臺
- APNs(Apple push notification server):蘋果的遠程推送服務器诱告,可以說是消息中轉站,需要發(fā)送給iOS客戶端的消息統(tǒng)一發(fā)往蘋果的APNs服務器
- notification:需要推送給iOS客戶端(iPhone或者是iPad)上的消息
- Client App:客戶端App,一般是安裝在iPhone或者是iPad上的應用程序(App)
- deviceToken:是唯一的由APNs根據(jù)設備和App來生成的一串數(shù)據(jù)民晒,那么在以下三種情況下會發(fā)生改變:
①同一個設備上重新安裝同一款應用
②同一個應用安裝在不同的設備上
③設備重新安裝了系統(tǒng)精居,同一個應用對應的deviceToken也會改變
An app-specific device token is globally unique and identifies one app-device combination.
釋義:deviceToken是device是App和device結合的唯一編碼
Upon receiving a device token from APNs in your app, it is your responsibility to open a network connection to your provider.
釋義:在你的設備上接到來自于APNs的deviceToken之前,你應該或者有責任先打開provider和APNs之間的網(wǎng)絡連接
It is also your responsibility, in your app, to then forward the device token along with any other relevant data you want to send to the provider.
釋義:你依然應該在你的App上將deviceToken連同其他需要的數(shù)據(jù)發(fā)給你的后臺服務器(provider)
When the provider later sends remote notification requests to APNs, it must include the device token, along with the notification payload. For more on this, see [APNs Overview](鏈接已貼出如下)
釋義:當后臺服務器稍后發(fā)送了遠程通知請求給APNs的時候镀虐,應當包含deviceToken和遠程推送消息內容
Never cache device tokens in your app; instead, get them from the system when you need them.
釋義:永遠不要緩存deviceToken在你的應用上箱蟆,而是需要的時候就從APNs系統(tǒng)獲取
APNs issues a new device token to your app when certain events happen.
釋義:當某些事件發(fā)生的時候APNs會發(fā)送一個新的deviceToken到你的App
The device token is guaranteed to be different, for example, when a user restores a device from a backup, when the user installs your app on a new device, and when the user reinstalls the operating system.
釋義:deviceToken是確保不一樣的,比如說以下情況:當一個用戶在同一個設備上重新安裝同一個應用或者將應用裝在不同的設備上刮便;或者用戶重裝手機系統(tǒng)都會導致deviceToken的不一樣
Fetching the token, rather than relying on a cache, ensures that you have the current device token needed for your provider to communicate with APNs.
釋義:直接獲取deviceToken而不是依靠緩存的deviceToken空猜,這樣確保你的后臺服務器和APNs通信的時候用的是當前有效的deviceToken
When you attempt to fetch a device token but it has not changed, the fetch method returns quickly.
釋義:當你嘗試去獲取deviceToken但是這個deviceToken并沒有改變提前,那么這個獲取就會很快粱坤。
APNs Overview.
圖意理解:
- 目的意圖:我們需要給我們的App推送一條消息(活動促銷類信息往堡,提醒用戶升級等信息等)給我們用戶乙埃,讓用戶了解應用的最新信息。一般出于用戶留存的考慮谆沃。
- 圖解流程:消息提供者(Provider)將消息發(fā)送給蘋果遠程推送服務器(APNs),蘋果遠程推送服務器(APNs)再將消息推送給裝有該應用的設備。
詳細流程:(以今日頭條為例)
- 在今日頭條App的AppDelegate的didFinishLaunchingWithOptions方法中注冊遠程推送通知哟沫,此時只要iOS設備正常聯(lián)網(wǎng)能夠訪問到外網(wǎng),iOS設備默認就會和APNs建立長連接,就會把iOS設備的UDID(Unique Device Identifier:唯一設備標識碼拂蝎,用來標識唯一一臺蘋果設備)和今日頭條的Bundle Identifier通過長連接發(fā)送給APNs服務器,然后蘋果通過這兩個的值根據(jù)一定的加密算法得出deviceToken鹅士,并將deviceToken返回給iOS設備趾痘。(注:APNs服務器會留有UDID+Bundle Identifier+deviceToken的映射表)
- 實現(xiàn)UIApplicationDelegate代理中的有關于注冊遠程通知的相關方法,包括注冊成功侣集、注冊失敗、對接收到通知的處理等。
- 如果注冊成功,實現(xiàn)注冊成功的代理方法,就能夠接收到deviceToken,并將deviceToken發(fā)送給今日頭條服務器,今日頭條服務器將此deviceToken存儲在數(shù)據(jù)庫中(一般如果是及時通訊類應用那么還會與用戶的賬號進行映射)论巍。
- 如果注冊失敗鞋怀,那么實現(xiàn)注冊失敗的協(xié)議方法村斟,處理失敗后的事情(包括發(fā)送給今日頭條服務器注冊失敗等)钱反。
- 今日頭條服務器接收到deviceToken之后尚卫,就可以根據(jù)這些deviceToken向APNs發(fā)送推送一條新聞簡要消息尸红。
- APNs接收到deviceToken和新聞簡要消息之后外里,根據(jù)deviceToken查找映射表找到對應的UDID和Bundle Identifier,根據(jù)UDID找到唯一一臺蘋果設備,再在找到的蘋果設備上根據(jù)Bundle Identifier找到唯一的應用(此處為今日頭條)顶瞳,然后推送消息焰络。
- 當設備接收到消息的時候甜孤,如果今日頭條在前臺也就是用戶正在使用今日頭條协饲,那么不會在設備上方彈出橫幅(如果使用了音效,還會觸發(fā)音效的播放)缴川,直接調用我們實現(xiàn)的UIApplicationDelegate中的接收消息的方法茉稠。反之如果今日頭條在后臺或者未運行時就會在設備的上方彈出橫幅(如果使用了音效,還會觸發(fā)音效的播放),點擊橫幅才會觸發(fā)調用我們實現(xiàn)的UIApplicationDelegate中的接收消息的方法把夸,這個時候你直接點擊應用圖標進來是不會調用的而线。
四、條件前提
設備條件:
- 蘋果iOS設備一臺:iPad/iPhone,此處選用iPhone
- 裝有Xcode的電腦一臺:強烈建議MBP或者iMAC,切不要用mini恋日,坑貨膀篮!
- 開發(fā)者賬號:這個如果公司有就用公司的賬號,如果處于自學階段的買一個吧岂膳,不貴誓竿,¥688
證書和描述文件條件:
- 應用的調試證書、描述文件
iOS- 最全的真機測試教程 - 應用的發(fā)布證書谈截、描述文件
iOS-最全的App上架教程 - 推送的調試證書和發(fā)布證書
推送的調試證書和發(fā)布證書
-
進入[開發(fā)者中心]筷屡,選擇Account,輸入開發(fā)者賬號和密碼簸喂,進入如下頁面:
-
選擇第一個之后進入如下頁面然后選擇Identifiers
PS:當然做到這一步是需要 iOS- 最全的真機測試教程和iOS-最全的App上架教程中的證書和描述文件都配置OK了的(注意如果只做測試用的話毙死,那么就只需要看關于調試的即可,就不需要看上架部分的了) 選中對應的App ID娘赴,點進去往下滾能看到如下頁面:
- 點擊Edit進行編輯规哲,往下滾能看到未打鉤
-
打上勾,然后狀態(tài)變成黃色的configurable,然后配置調試證書诽表,先創(chuàng)建CSR文件:
-
點擊Create Cerfificate然后進入證書創(chuàng)建頁面唉锌,此處需要CSR文件,點擊繼續(xù)
-
選擇我們之前創(chuàng)建普通調試證書的CSR文件:
就是這哥們:
選擇完成之后竿奏,然后點擊Continue就可以看見推送調試證書已經(jīng)創(chuàng)建好了袄简,點擊Download即可下載到本地:
- 配置好推送調試證書之后,那么接下來就是配置推送發(fā)布證書(如果有必要的話)
- 經(jīng)過以上步驟后泛啸,檢查是否推送配置成功绿语,如果圖中變綠了就是成功了:
PS:想要生活過得去,頭上必須帶點綠候址!
經(jīng)過以上的步驟吕粹,會有以下的文件:
- CSR:
- 調試證書和描述文件:
-
發(fā)布證書和描述文件:
-
推送調試和發(fā)布證書
證書和描述文件添加:
- 雙擊證書,將證書添加到鑰匙串中
- 雙擊描述文件岗仑,將描述文件放入路徑中匹耕,這個自動就放入了,無需手動荠雕,只有當我們需要刪除這個描述文件的時候稳其,才會手動找到以下路徑去刪除掉這些描述文件
~/Library/MobileDevice/Provisioning Profiles
五驶赏、工程配置:
1>在對應Bundle ID的工程中開啟ATS:
方式一:
代碼如下:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
方式二:
2>在Targets->Capabilities->Push Notifications,右邊開啟次功能
開啟完成之后既鞠,會在左邊文件列表中多一個entitlements文件
然后再開啟Targets->Capabilities->Background Modes:
3>按照接下來的文章中第五點開始一直看到最后即可完成推送測試了
文章如右:iOS 遠程消息推送 APNS推送原理和一步一步開發(fā)詳解篇
注意其中蘋果的遠程推送是分測試和發(fā)布服務器的:
測試服務器地址:gateway.sandbox.push.apple.com 2195
發(fā)布服務器地址:gateway.push.apple.com 2195
4>推薦我們自己MAC端服務器測試工具:SmartPush
這里還額外推薦一款應用程序:APNS-Tool
推送消息的格式為:
{"aps":
{"alert":"I'm a very handsome boy! Nice IT guys!",
"badge":6,
"sound": "default"
}
}
當然還可以加上自定義的:
{"aps":
{"alert":"I'm a very handsome boy! Nice IT guys!",
"badge":6,
"sound": "default"
},
"custom":"http://www.baidu.com"
}
針對于以上文章中的第七點項目測試此處提上我的代碼:
我是把這個推送功能直接封裝成一個類了DSPushService:
/* ________ ________
* | | / / / ______ \ | _____ \
* | | / / / / \ \ | | \ \
* | |/ / | | | | | | | |
* | |\ \ | | | | | | | |
* | | \ \ \ \______/ / | |_____/ /
* | | \ \ \________/ |________/
*
* Copyright ? 2014~2017年 KODIE. All rights reserved.
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface DSPushService : NSObject
+ (instancetype)defaultPushService;
//授權和注冊
- (BOOL)DSPushApplication:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
//這個是為了在HomeScreen點擊App圖標進程序
- (void)DSBecomeActive:(UIApplication *)application;
//注冊成功得到deviceToken
- (void)DSPushApplication:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken;
//注冊失敗報錯
- (void)DSPushApplication:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error;
//這是處理發(fā)送過來的推送
- (void)DSPushApplication:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo;
- (void)DSPushApplication:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler;
@end
/* ________ ________
* | | / / / ______ \ | _____ \
* | | / / / / \ \ | | \ \
* | |/ / | | | | | | | |
* | |\ \ | | | | | | | |
* | | \ \ \ \______/ / | |_____/ /
* | | \ \ \________/ |________/
*
* Copyright ? 2014~2017年 KODIE. All rights reserved.
*/
#import "DSPushService.h"
#import <UserNotifications/UserNotifications.h>
@interface DSPushService ()
@end
@implementation DSPushService
#pragma mark - lifeCycle
- (instancetype)init{
if (self = [super init]) {
//code here...
}
return self;
}
+ (instancetype)defaultPushService{
static DSPushService *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[super alloc]init];
});
return instance;
}
#pragma mark - 注冊和授權
- (BOOL)DSPushApplication:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
if ([[UIDevice currentDevice].systemVersion floatValue] >= 10.0f) {
//iOS10-
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
UNAuthorizationOptions options = UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert;
[center requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError * _Nullable error) {
//判斷
}];
}else if([[UIDevice currentDevice].systemVersion floatValue] >= 8.0f){
//iOS8-iOS10
[application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert | UIUserNotificationTypeSound | UIUserNotificationTypeBadge categories:nil]];
}else{
//iOS8以下
[application registerForRemoteNotificationTypes:UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeSound];
}
// 注冊遠程推送通知 (獲取DeviceToken)
[application registerForRemoteNotifications];
//這個是應用未啟動但是通過點擊通知的橫幅來啟動應用的時候
NSDictionary *userInfo = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
if (userInfo != nil) {
//如果有值煤傍,說明是通過遠程推送來啟動的
//code here...
}
return YES;
}
//處理從后臺到前臺后的角標處理
-(void) DSBecomeActive:(UIApplication *)application{
if (application.applicationIconBadgeNumber > 0) {
application.applicationIconBadgeNumber = 0;
}
}
#pragma mark - 遠程推送的注冊結果的相關方法
//成功
- (void)DSPushApplication:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{ //獲取設備相關信息
NSString *deviceName = dev.name;
NSString *deviceModel = dev.model;
NSString *deviceSystemVersion = dev.systemVersion;
UIDevice *myDevice = [UIDevice currentDevice];
NSString *deviceUDID = [myDevice identifierForVendor].UUIDString;
NSString *appName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
NSString *appVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
//獲取用戶的通知設置狀態(tài)
NSString *pushBadge= @"disabled";
NSString *pushAlert = @"disabled";
NSString *pushSound = @"disabled";
if ([[UIDevice currentDevice].systemVersion floatValue]>=8.0f) {
UIUserNotificationSettings *setting = [[UIApplication sharedApplication] currentUserNotificationSettings];
if (UIUserNotificationTypeNone == setting.types) {
NSLog(@"推送關閉");
}else{
NSLog(@"推送打開");
pushBadge = (setting.types & UIRemoteNotificationTypeBadge) ? @"enabled" : @"disabled";
pushAlert = (setting.types & UIRemoteNotificationTypeAlert) ? @"enabled" : @"disabled";
pushSound = (setting.types & UIRemoteNotificationTypeSound) ? @"enabled" : @"disabled";
}
}else{
UIRemoteNotificationType type = [[UIApplication sharedApplication] enabledRemoteNotificationTypes];
if(UIRemoteNotificationTypeNone == type){
NSLog(@"推送關閉");
}else{
NSLog(@"推送打開");
pushBadge = (type & UIRemoteNotificationTypeBadge) ? @"enabled" : @"disabled";
pushAlert = (type & UIRemoteNotificationTypeAlert) ? @"enabled" : @"disabled";
pushSound = (type & UIRemoteNotificationTypeSound) ? @"enabled" : @"disabled";
}
}
//獲取設備的UUID
NSString *deviceUuid;
UIDevice *dev = [UIDevice currentDevice];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
id uuid = [defaults objectForKey:@"deviceUuid"];
if (uuid)
deviceUuid = (NSString *)uuid;
else {
CFStringRef cfUuid = CFUUIDCreateString(NULL, CFUUIDCreate(NULL));
deviceUuid = (NSString *)CFBridgingRelease(cfUuid);
NSLog(@"%@",deviceUuid);
NSLog(@"%@",deviceUuid);
[defaults setObject:deviceUuid forKey:@"deviceUuid"];
[defaults release];
}
NSString *deviceTokenString = [[[[deviceToken description]
stringByReplacingOccurrencesOfString:@"<"withString:@""]
stringByReplacingOccurrencesOfString:@">" withString:@""]
stringByReplacingOccurrencesOfString: @" " withString: @""];
NSString *host = @"http://www.baidu.com";//你們自己后臺服務器的地址
//時間戳
NSTimeInterval interval = [[NSDate date] timeIntervalSince1970] * 1000;
NSInteger time = interval;
NSString *timestamp = [NSString stringWithFormat:@"%zd",time];
//MD5校驗
NSString *md5String = [NSString stringWithFormat:@"%@%@%@",deviceTokenString,deviceUDID,timestamp];
NSString *credential = [Util encodeToMd5WithStr:md5String];
NSString *urlString = [NSString stringWithFormat:@"%@?device_token=%@&device_uuid=%@&device_name=%@&device_version=%@&app_name=%@×tamp=%@&push_badge=%@&push_alert=%@&push_sound=%@&credential=%@", host, deviceTokenString, deviceUDID, deviceModel, deviceSystemVersion, appName, timestamp, pushBadge, pushAlert, pushSound, credential];
//打印值看一下,是否正確嘱蛋,當然打印的可以用一個宏判斷一下
NSLog(@"????%@", host);
NSLog(@"????%@", gameId);
NSLog(@"????%@", deviceTokenString);
NSLog(@"????%@", deviceUDID);
NSLog(@"????%@", deviceModel);
NSLog(@"????%@", deviceSystemVersion);
NSLog(@"????%@", appName);
NSLog(@"????%@", timestamp);
NSLog(@"????%@", pushBadge);
NSLog(@"????%@", pushAlert);
NSLog(@"????%@", pushSound);
NSLog(@"????%@", credential);
NSLog(@"????%@", urlString);
//以下是發(fā)送DeviceToken給后臺了蚯姆,有人會問,為什么要傳這么多參數(shù)浑槽,這個具體根據(jù)你們后臺來哈蒋失,不要問我,問你們后臺要傳什么就傳什么桐玻,但是DeviceToken是一定要傳的
NSURL *url = [NSURL URLWithString:urlString];
if (!url) {
NSLog(@"傳入的URL為空或者有非法字符,請檢查參數(shù)");
return;
}
NSLog(@"%@",url);
//發(fā)送異步請求
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setTimeoutInterval:5.0];
[request setHTTPMethod:@"GET"];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode == 200 && data) {
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
if (dict && [dict[@"ret"] integerValue] == 0) {
NSLog(@"上傳deviceToken成功篙挽!deviceToken dict = %@",dict);
}else{
NSLog(@"返回ret = %zd, msg = %@",[dict[@"ret"] integerValue],dict[@"msg"]);
}
}else if (error) {
NSLog(@"請求失敗,error = %@",error);
}
});
}];
[task resume];
}
//失敗
- (void)DSPushApplication:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
NSLog(@"注冊推送失敗镊靴,error = %@", error);
//failed fix code here...
}
#pragma mark - 收到遠程推送通知的相關方法
//iOS6及以下(前臺是直接走這個方法不會出現(xiàn)提示的铣卡,后臺是需要點擊相應的通知才會走這個方法的)
- (void)DSPushApplication:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
[self DSPushApplication:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:nil];
}
//iOS7及以上
- (void)DSPushApplication:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSLog(@"%@", userInfo);
//注意HomeScreen上一經(jīng)彈出推送系統(tǒng)就會給App的applicationIconBadgeNumber設為對應值
if (application.applicationIconBadgeNumber > 0) {
application.applicationIconBadgeNumber = 0;
}
NSLog(@"remote notification: %@",[userInfo description]);
NSDictionary *apsInfo = [userInfo objectForKey:@"aps"];
NSString *alert = [apsInfo objectForKey:@"alert"];
NSLog(@"Received Push Alert: %@", alert);
NSString *sound = [apsInfo objectForKey:@"sound"];
NSLog(@"Received Push Sound: %@", sound);
NSString *badge = [apsInfo objectForKey:@"badge"];
NSLog(@"Received Push Badge: %@", badge);
//這是播放音效
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
//處理customInfo
if ([userInfo objectForKey:@"custom"] != nil) {
//custom handle code here...
}
completionHandler(UIBackgroundFetchResultNoData);
}
@end
其中修改一下需要上傳的參數(shù)還有后臺的主機地址以及增加相應的處理就可以了。
JAVA后臺的配置:
注意以上是PHP后臺的配置方式偏竟,那么如果是JAVA后臺又該怎么配置呢煮落,請自行閱讀下一篇文章,對比發(fā)現(xiàn)其中的不同之處:
IOS 基于APNS消息推送原理與實現(xiàn)(JAVA后臺)--轉
此處稍微細說一下踊谋,就是PHP用的是pem文件蝉仇,而JAVA用的是p12文件
六、角標問題
最后一部分內容就是處理我們的角標問題:iOS遠程推送之(二):角標applicationIconNumber設置
Local and Remote Notification Programming Guide
iOS中使用本地通知為你的APP添加提示用戶功能
APNs Overview.
iOS- 最全的真機測試教程
iOS-最全的App上架教程
iOS 遠程消息推送 APNS推送原理和一步一步開發(fā)詳解篇
SmartPush
IOS 基于APNS消息推送原理與實現(xiàn)(JAVA后臺)--轉