一、前言
iOS中消息推送有兩種方式,本地推送和遠程推送。本地推送在iOS中使用本地通知為你的APP添加提示用戶功能這篇博客中有詳細的介紹,我們在此主要討論遠程推送的流程與配置過程以及注意事項。
官方文檔:Local and Remote Notification Programming Guide
二、簡介
什么是遠程推送?
- 蘋果提供的一項給終端設備推送消息的服務
為何使用遠程推送?
- 當用戶打開應用程序的通知中心之后,蘋果遠程推送服務器就能把消息推送到裝有該應用的設備上,具有強制性、實時性的特點,并且用戶無需打開應用都能收到推送的消息。
三、遠程推送原理
說到遠程推送不得不說下下面這張圖,下面這張圖把遠程推送的過程大致描述勾畫了一遍,在此解析一下下圖。
名詞解釋:
- Provider:消息提供者,一般是我們的后臺服務器或者第三方推送服務器后臺
- APNs(Apple push notification server):蘋果的遠程推送服務器,可以說是消息中轉站,需要發送給iOS客戶端的消息統一發往蘋果的APNs服務器
- notification:需要推送給iOS客戶端(iPhone或者是iPad)上的消息
- Client App:客戶端App,一般是安裝在iPhone或者是iPad上的應用程序(App)
- deviceToken:是唯一的由APNs根據設備和App來生成的一串數據,那么在以下三種情況下會發生改變:
①同一個設備上重新安裝同一款應用
②同一個應用安裝在不同的設備上
③設備重新安裝了系統,同一個應用對應的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之間的網絡連接
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連同其他需要的數據發給你的后臺服務器(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](鏈接已貼出如下)
釋義:當后臺服務器稍后發送了遠程通知請求給APNs的時候,應當包含deviceToken和遠程推送消息內容
Never cache device tokens in your app; instead, get them from the system when you need them.
釋義:永遠不要緩存deviceToken在你的應用上,而是需要的時候就從APNs系統獲取
APNs issues a new device token to your app when certain events happen.
釋義:當某些事件發生的時候APNs會發送一個新的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是確保不一樣的,比如說以下情況:當一個用戶在同一個設備上重新安裝同一個應用或者將應用裝在不同的設備上;或者用戶重裝手機系統都會導致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)將消息發送給蘋果遠程推送服務器(APNs),蘋果遠程推送服務器(APNs)再將消息推送給裝有該應用的設備。
詳細流程:(以今日頭條為例)
- 在今日頭條App的AppDelegate的didFinishLaunchingWithOptions方法中注冊遠程推送通知,此時只要iOS設備正常聯網能夠訪問到外網,iOS設備默認就會和APNs建立長連接,就會把iOS設備的UDID(Unique Device Identifier:唯一設備標識碼,用來標識唯一一臺蘋果設備)和今日頭條的Bundle Identifier通過長連接發送給APNs服務器,然后蘋果通過這兩個的值根據一定的加密算法得出deviceToken,并將deviceToken返回給iOS設備。(注:APNs服務器會留有UDID+Bundle Identifier+deviceToken的映射表)
- 實現UIApplicationDelegate代理中的有關于注冊遠程通知的相關方法,包括注冊成功、注冊失敗、對接收到通知的處理等。
- 如果注冊成功,實現注冊成功的代理方法,就能夠接收到deviceToken,并將deviceToken發送給今日頭條服務器,今日頭條服務器將此deviceToken存儲在數據庫中(一般如果是及時通訊類應用那么還會與用戶的賬號進行映射)。
- 如果注冊失敗,那么實現注冊失敗的協議方法,處理失敗后的事情(包括發送給今日頭條服務器注冊失敗等)。
- 今日頭條服務器接收到deviceToken之后,就可以根據這些deviceToken向APNs發送推送一條新聞簡要消息。
- APNs接收到deviceToken和新聞簡要消息之后,根據deviceToken查找映射表找到對應的UDID和Bundle Identifier,根據UDID找到唯一一臺蘋果設備,再在找到的蘋果設備上根據Bundle Identifier找到唯一的應用(此處為今日頭條),然后推送消息。
- 當設備接收到消息的時候,如果今日頭條在前臺也就是用戶正在使用今日頭條,那么不會在設備上方彈出橫幅(如果使用了音效,還會觸發音效的播放),直接調用我們實現的UIApplicationDelegate中的接收消息的方法。反之如果今日頭條在后臺或者未運行時就會在設備的上方彈出橫幅(如果使用了音效,還會觸發音效的播放),點擊橫幅才會觸發調用我們實現的UIApplicationDelegate中的接收消息的方法,這個時候你直接點擊應用圖標進來是不會調用的。
四、條件前提
設備條件:
- 蘋果iOS設備一臺:iPad/iPhone,此處選用iPhone
- 裝有Xcode的電腦一臺:強烈建議MBP或者iMAC,切不要用mini,坑貨!
- 開發者賬號:這個如果公司有就用公司的賬號,如果處于自學階段的買一個吧,不貴,¥688
證書和描述文件條件:
- 應用的調試證書、描述文件
iOS- 最全的真機測試教程 - 應用的發布證書、描述文件
iOS-最全的App上架教程 - 推送的調試證書和發布證書
推送的調試證書和發布證書
-
進入[開發者中心],選擇Account,輸入開發者賬號和密碼,進入如下頁面:
Snip20170614_28.png -
選擇第一個之后進入如下頁面然后選擇Identifiers
Snip20170614_29.png
PS:當然做到這一步是需要 iOS- 最全的真機測試教程和iOS-最全的App上架教程中的證書和描述文件都配置OK了的(注意如果只做測試用的話,那么就只需要看關于調試的即可,就不需要看上架部分的了) 選中對應的App ID,點進去往下滾能看到如下頁面:
- 點擊Edit進行編輯,往下滾能看到未打鉤
-
打上勾,然后狀態變成黃色的configurable,然后配置調試證書,先創建CSR文件:
Snip20170614_21.png -
點擊Create Cerfificate然后進入證書創建頁面,此處需要CSR文件,點擊繼續
Snip20170614_22.png -
選擇我們之前創建普通調試證書的CSR文件:
Snip20170614_23.png
就是這哥們:
Snip20170614_30.png 選擇完成之后,然后點擊Continue就可以看見推送調試證書已經創建好了,點擊Download即可下載到本地:
- 配置好推送調試證書之后,那么接下來就是配置推送發布證書(如果有必要的話)
- 經過以上步驟后,檢查是否推送配置成功,如果圖中變綠了就是成功了:
PS:想要生活過得去,頭上必須帶點綠!
經過以上的步驟,會有以下的文件:
- CSR:
- 調試證書和描述文件:
-
發布證書和描述文件:
Snip20170614_32.png -
推送調試和發布證書
Snip20170614_33.png
證書和描述文件添加:
- 雙擊證書,將證書添加到鑰匙串中
- 雙擊描述文件,將描述文件放入路徑中,這個自動就放入了,無需手動,只有當我們需要刪除這個描述文件的時候,才會手動找到以下路徑去刪除掉這些描述文件
~/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推送原理和一步一步開發詳解篇
注意其中蘋果的遠程推送是分測試和發布服務器的:
測試服務器地址:gateway.sandbox.push.apple.com 2195
發布服務器地址: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;
//這是處理發送過來的推送
- (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"];
//獲取用戶的通知設置狀態
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);
//以下是發送DeviceToken給后臺了,有人會問,為什么要傳這么多參數,這個具體根據你們后臺來哈,不要問我,問你們后臺要傳什么就傳什么,但是DeviceToken是一定要傳的
NSURL *url = [NSURL URLWithString:urlString];
if (!url) {
NSLog(@"傳入的URL為空或者有非法字符,請檢查參數");
return;
}
NSLog(@"%@",url);
//發送異步請求
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及以下(前臺是直接走這個方法不會出現提示的,后臺是需要點擊相應的通知才會走這個方法的)
- (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上一經彈出推送系統就會給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
其中修改一下需要上傳的參數還有后臺的主機地址以及增加相應的處理就可以了。
JAVA后臺的配置:
注意以上是PHP后臺的配置方式,那么如果是JAVA后臺又該怎么配置呢,請自行閱讀下一篇文章,對比發現其中的不同之處:
IOS 基于APNS消息推送原理與實現(JAVA后臺)--轉
此處稍微細說一下,就是PHP用的是pem文件,而JAVA用的是p12文件
六、角標問題
最后一部分內容就是處理我們的角標問題:iOS遠程推送之(二):角標applicationIconNumber設置
Local and Remote Notification Programming Guide
iOS中使用本地通知為你的APP添加提示用戶功能
APNs Overview.
iOS- 最全的真機測試教程
iOS-最全的App上架教程
iOS 遠程消息推送 APNS推送原理和一步一步開發詳解篇
SmartPush
IOS 基于APNS消息推送原理與實現(JAVA后臺)--轉