注:此文只現(xiàn)在已經(jīng)不能適配iOS10了堂飞,iOS10推送采用了新的方法,做iOS9及以下的系統(tǒng)可讀此篇文章绑咱。
最近公司項目升級重構(gòu)(重寫)绰筛,除了本來我所負責的模塊,最后臨危受命接了推送(遠程和本地)相關(guān)的模塊描融,順便把推送的相關(guān)知識復習了一遍铝噩。后期連續(xù)工作十幾天加上最后一天的通(瞎)宵(熬)達(一)旦(夜),也算是不辱使命窿克。此文除了講解遠程推送相關(guān)的基本知識外骏庸,也會涉及一些推送相關(guān)的奇淫技巧。另外本文主要講解遠程推送年叮,后續(xù)會出一篇iOS推送之本地推送(iOS Notification Of Local Notification)的姊妹篇具被。
此篇文章的邏輯如下圖所示:
遠程推送原理
學習一些東西前我認為最好能了解它的原理,這樣以后我們遇到問題的時候只损,就可以很快速的找到錯誤之所在一姿,如果對原理不感興趣的同學可直接下翻到應(yīng)用部分【遠程推送應(yīng)用】。
iOS app大多數(shù)都是基于client/server模式開發(fā)的跃惫,client就是安裝在我們設(shè)備上的app叮叹,server就是遠程服務(wù)器,主要給我們的app提供數(shù)據(jù)爆存,因為也被稱為Provider蛉顽。那么問題來了,當App處于Terminate狀態(tài)的時候先较,當client與server斷開的時候携冤,client如何與server進行通信呢悼粮?是的,這時候Remote Notifications很好的解決了這個困境曾棕。蘋果所提供的一套服務(wù)稱之為Apple Push Notification service扣猫,就是我們所謂的APNs。
推送消息傳輸路徑: Provider-APNs-Client App
我們的設(shè)備聯(lián)網(wǎng)時(無論是蜂窩聯(lián)網(wǎng)還是Wi-Fi聯(lián)網(wǎng))都會與蘋果的APNs服務(wù)器建立一個長連接(persistent IP connection)睁蕾,當Provider推送一條通知的時候,這條通知并不是直接推送給了我們的設(shè)備债朵,而是先推送到蘋果的APNs服務(wù)器上面子眶,而蘋果的APNs服務(wù)器再通過與設(shè)備建立的長連接進而把通知推送到我們的設(shè)備上(參考圖1-1,圖1-2)序芦。而當設(shè)備處于非聯(lián)網(wǎng)狀態(tài)的時候臭杰,APNs服務(wù)器會保留Provider所推送的最后一條通知,當設(shè)備轉(zhuǎn)換為連網(wǎng)狀態(tài)時谚中,APNs則把其保留的最后一條通知推送給我們的設(shè)備渴杆;如果設(shè)備長時間處于非聯(lián)網(wǎng)狀態(tài)下,那么APNs服務(wù)器為其保存的最后一條通知也會丟失宪塔。Remote Notification必須要求設(shè)備連網(wǎng)狀態(tài)下才能收到磁奖,并且太頻繁的接收遠程推送通知對設(shè)備的電池壽命是有一定的影響的。
deviceToken的生成
當一個App注冊接收遠程通知時某筐,系統(tǒng)會發(fā)送請求到APNs服務(wù)器比搭,APNs服務(wù)器收到此請求會根據(jù)請求所帶的key值生成一個獨一無二的value值也就是所謂的deviceToken,而后APNs服務(wù)器會把此deviceToken包裝成一個NSData對象發(fā)送到對應(yīng)請求的App上南誊。然后App把此deviceToken發(fā)送給我們自己的服務(wù)器身诺,就是所謂的Provider。Provider收到deviceToken以后進行儲存等相關(guān)處理抄囚,以后Provider給我們的設(shè)備推送通知的時候霉赡,必須包含此deviceToken。(參考圖1-3幔托,圖1-4)
這個時候你可能會問deviceToken到底是什么穴亏?有什么用?為什么是獨一無二的重挑?
- 是什么:deviceToken其實就是根據(jù)注冊遠程通知的時候向APNs服務(wù)器發(fā)送的Token key迫肖,Token key中包含了設(shè)備的UDID和App的Bundle Identifier,然后蘋果APNs服務(wù)器根據(jù)此Token key編碼生成一個deviceToken攒驰。deviceToken可以簡單理解為就是包含了設(shè)備信息和應(yīng)用信息的一串編碼蟆湖。
- 有什么用:上面提到Provider推送消息的時候必須帶有此deviceToken,然后此消息就根據(jù)deviceToken(UDID + App's Bundle Identifier)找到對應(yīng)的設(shè)備以及該設(shè)備上對應(yīng)的應(yīng)用玻粪,從而把此推送消息推送給此應(yīng)用隅津。
- 唯一性:蘋果APNs的編碼技術(shù)和deviceToken的獨特作用保證了他的唯一性诬垂。唯一性并不是說一臺設(shè)備上的一個應(yīng)用程序永遠只有一個deviceToken,當用戶升級系統(tǒng)的時候deviceToken是會變化的伦仍。
<a id="遠程推送應(yīng)用"></a>遠程推送應(yīng)用
注冊遠程通知(獲取deviceToken)
注冊遠程通知的方法
一般都是在App啟動完成的時候去注冊遠程通知注冊方法調(diào)用一般都在didFinishLaunchingWithOptions:方法中
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 在iOS8之前注冊遠程通知的方法结窘,如果項目要支持iOS8以前的版本,必須要寫此方法
UIRemoteNotificationType types = UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert;
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:types];
// iOS8之后注冊遠程通知的方法
UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert;
UIUserNotificationSettings *mySettings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];
}
處理注冊遠程通知的回調(diào)方法
// 注冊成功回調(diào)方法充蓝,其中deviceToken即為APNs返回的token
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
[self sendProviderDeviceToken:deviceToken]; // 將此deviceToken發(fā)送給Provider
}
// 注冊失敗回調(diào)方法隧枫,處理失敗情況
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
}
在iOS8之后增加了可操作通知類型,可操作通知允許開發(fā)者添加自定義跳轉(zhuǎn)事件谓苟。這些高級功能此篇文章不講解官脓,有興趣的同學可自己去了解UIUserNotificationAction
UIMutableUserNotificationAction
UIUserNotificationCategory
UIMutableUserNotificationCategory
這幾個類。
處理接收到遠程通知消息(會回調(diào)以下方法中的某一個)
application: didFinishLaunchingWithOptions:
此方法在程序第一次啟動是調(diào)用涝焙,也就是說App從Terminate狀態(tài)進入Foreground狀態(tài)的時候卑笨,根據(jù)方法內(nèi)代碼判斷是否有推送消息。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// userInfo為收到遠程通知的內(nèi)容
NSDictionary *userInfo = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
if (userInfo) {
// 有推送的消息仑撞,處理推送的消息
}
return YES赤兴;
}
application: didReceiveRemoteNotification:
如果App處于Background狀態(tài)時,只用用戶點擊了通知消息時才會調(diào)用該方法隧哮;如果App處于Foreground狀態(tài)桶良,會直接調(diào)用該方法。
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
}
application: didReceiveRemoteNotification: fetchCompletionHandler:
iOS7之前蘋果是不支持多任務(wù)的沮翔,這也是iOS系統(tǒng)對硬件要求低艺普,流暢性好的原因之一。iOS7之后鉴竭,蘋果開始支持多任務(wù)歧譬,即App可在后臺做一些更新UI、下載數(shù)據(jù)的操作等搏存。若要接收到遠程推送的時候要在后臺做一些事情則需要把后臺遠程推送模式打開。不適配iOS7之前系統(tǒng)的項目建議使用此后臺模式璧眠,充分利用蘋果推出的多任務(wù)模式缩焦,不枉費蘋果的一片苦心啊责静!設(shè)置后臺模式方法項目對應(yīng)TARGETS-Capabilities-Background Modes-Remote Notifications具體設(shè)置方法如下圖(圖2-1)袁滥。
此方法不論App處于Foreground狀態(tài)還是處于Background狀態(tài),收到遠程推送消息的時候都會立即調(diào)用此方法灾螃。此方法需要配置后臺模式并且在推送負載中必須有content-available此key值题翻,對應(yīng)的value值為1(詳細介紹參考下面【遠程通知負載內(nèi)容】)。
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
// 在此方法中一定要調(diào)用completionHandler這個回調(diào)腰鬼,告訴系統(tǒng)是否處理成功
UIBackgroundFetchResultNewData, // 成功接收到數(shù)據(jù)
UIBackgroundFetchResultNoData, // 沒有接收到數(shù)據(jù)
UIBackgroundFetchResultFailed // 接受失敗
if (userInfo) {
completionHandler(UIBackgroundFetchResultNewData);
} else {
completionHandler(UIBackgroundFetchResultNoData);
}
}
可操作通知類型收到推送消息時回調(diào)方法
// 此兩個回調(diào)方法對應(yīng)可操作通知類型嵌赠,具體使用方法參考以上方法很容易理解塑荒,不在詳細敘述
- (void)application:(UIApplication *)application handleActionWithIdentifier:(nullable NSString *)identifier
forRemoteNotification:(NSDictionary *)userInfo
completionHandler:(void(^)())completionHandler {
}
- (void)application:(UIApplication *)application handleActionWithIdentifier:(nullable NSString *)identifier
forRemoteNotification:(NSDictionary *)userInfo withResponseInfo:(NSDictionary *)responseInfo
completionHandler:(void(^)())completionHandler {
}
客戶端和服務(wù)端的交互
說到這里我就隨意吐槽一下推送,做推送個人感覺還是比較費勁的姜挺。而第一次啟動App時詢問用戶是否接受推送消息的時候齿税,大部分用戶都會點擊拒絕推送的吧,反正我是這樣的炊豪。你辛辛苦苦做好了凌箕,想辦法保證其推送準時性,想辦法保證其推送到達率词渤,結(jié)果用戶一個拒絕牵舱,你所以的努力全都白費了啊,哈哈哈掖肋。
我這里主要想說的就是:我們要把對應(yīng)的.p12(個人信息交換證書)證書給服務(wù)端的開發(fā)人員就好了仆葡。具體可參看我另一篇文章不讓蘋果開發(fā)者賬號折磨我中的團隊開發(fā)證書的管理中的導出.p12章節(jié)赏参。
遠程推送負載
遠程推送負載大小
遠程通知負載的大小根據(jù)Provider使用的API不同而不同志笼。當使用HTTP/2 provider API時,負載最大為4096bytes把篓,即4kB纫溃;當使用legacy binary interface時,負載最大為2048bytes韧掩,即2kB紊浩。當負載大小超過規(guī)定的負載大小時,APNs會拒絕發(fā)送此消息疗锐。
<a id="遠程推送負載內(nèi)容"></a>遠程推送負載內(nèi)容
內(nèi)容格式必要要知道的啊坊谁,服務(wù)端一般會要我們客戶端定義好格式給他們的。
每一條通知的消息都會組成一個JSON字典對象滑臊,其格式如下所示口芍,示例中的key值為蘋果官方所用key。自定義字段的時候要避開這些key值雇卷。
{
"aps" : {
"alert" : { // string or dictionary
"title" : "string"
"body" : "string",
"title-loc-key" : "string or null"
"title-loc-args" : "array of strings or null"
"action-loc-key" : "string or null"
"loc-key" : "string"
"loc-args" : "array of strings"
"launch-image" : "string"
},
"badge" : number,
"sound" : "string"
"content-available" : number;
"category" : "string"
},
}
aps:推送消息必須有的key
alert:推送消息包含此key值鬓椭,系統(tǒng)就會根據(jù)用戶的設(shè)置展示標準的推送信息
badge:在app圖標上顯示消息數(shù)量,缺少此key值关划,消息數(shù)量就不會改變小染,消除標記時把此key對應(yīng)的value設(shè)置為0
sound:設(shè)置推送聲音的key值,系統(tǒng)默認提示聲音對應(yīng)的value值為default
content-available:此key值設(shè)置為1贮折,系統(tǒng)接收到推送消息時就會調(diào)用不同的回調(diào)方法裤翩,iOS7之后配置后臺模式
category:UIMutableUserNotificationCategory's identifier 可操作通知類型的key值
title:簡短描述此調(diào)推送消息的目的,適用系統(tǒng)iOS8.2之后版本
body:推送的內(nèi)容
title-loc-key:功能類似title调榄,附加功能是國際化岛都,適用系統(tǒng)iOS8.2之后版本
title-loc-args:配合title-loc-key字段使用律姨,適用系統(tǒng)iOS8.2之后版本
action-loc-key:可操作通知類型key值,不詳細敘述
loc-key:參考title-loc-key
loc-args:參考title-loc-args
launch-image:點擊推送消息或者移動事件滑塊時臼疫,顯示的圖片择份。如果缺少此key值,會加載app默認的啟動圖片烫堤。
當然以上key值并不是每條推送消息都必帶的key值荣赶,應(yīng)當根據(jù)需求來選擇所需要的key值,除了以上系統(tǒng)所提供的key值外,你還可以自定義自己的key值,來作為消息推送的負載氮兵,自定義key值與aps此key值并列把跨。如下格式:
{
"aps" : {
"alert" : "Provider push messag.",
"badge" : 9,
"sound" : "toAlice.aiff"
},
"Id" : 1314, // 自定義key值
"type" : "customType" // 自定義key值
}
指定用戶的推送
對于要求用戶登錄的App,推送是可以指定用戶的黎棠,同一條推送有些用戶可以收到,但是有些用戶又不能收到。說起來這個就要提到另外的一個token了灭红,一般稱之為userToken,userToken一般都是根據(jù)自己公司自定義的規(guī)則去生成的口注。userToken是以用戶的賬號加對應(yīng)的密碼生成的变擒。這樣結(jié)合上面提到的deviceToken,就可以做到根據(jù)不同的用戶推送不同的消息寝志。deviceToken找到對應(yīng)某臺設(shè)備和該設(shè)備上的應(yīng)用娇斑,而userToken對應(yīng)找到該用戶〔牟浚客戶端在上報deviceToken的時候毫缆,要把userToken對應(yīng)一起上報給服務(wù)端也就是Provider。
淺談推送第三方SDK
關(guān)于第三方推送的SDK有很多乐导,常見的有極光推送 百度推送 個推 友盟推送等等苦丁。其實推送的原理都是大同小異的,理解了蘋果推送的原理兽叮,這些第三方SDK還在是基本原理上面進行了擴展芬骄。對于用不用第三方SDK其實對我們客戶端影響不大,推送第三方SDK主要是方便了服務(wù)端開發(fā)者鹦聪。主要表現(xiàn)為服務(wù)端開發(fā)者不需要去開發(fā)維護自己的推送服務(wù)器與 APNs 對接账阻,不必自己維護更新 deviceToken。當然了泽本,第三方SDK也會提供一些額外的附屬功能例如JPush提供了應(yīng)用內(nèi)消息推送淘太,這在類似于聊天的場景里很方便的。看完這段是不是發(fā)現(xiàn)集成推送的第三方SDK和客戶端沒什么關(guān)系蒲牧,我們工作量不僅沒有減少撇贺,反而增加了一點點啊。至于第三方SDK的其他功能冰抢,大家可自行去對應(yīng)官網(wǎng)學習松嘶,這里不再過多描述。
利用runtime實現(xiàn)推送消息萬能跳轉(zhuǎn)
此段參考了@漢斯哈哈哈的一篇iOS 萬能跳轉(zhuǎn)界面方法萬能跳轉(zhuǎn)就是可以跳轉(zhuǎn)到指定的任意一個界面挎扰,但是這個和服務(wù)端耦合性太強翠订,使用的時候要慎重考慮,而且公司一般都是iOS遵倦,Android共用同一套推送規(guī)則很難讓服務(wù)端在給你開一條新的推送規(guī)則尽超,不便于維護,而且成本也是需要考慮的梧躺。寫此段的目的就是當產(chǎn)品有這樣的需求的時候還是可以參考一下的似谁。
定義推送規(guī)則
// 客戶端控制器的屬性
@interface YBViewController : UIViewController
/** 頻道Id */
@property (nonatomic, copy) NSString *Id;
/** 頻道type */
@property (nonatomic, copy) NSString *type;
@end
// 服務(wù)端推送數(shù)據(jù)格式
{
"aps" : { "alert" : "Provider push messag" },
"class" : "YBViewController",
"property" : {
"Id" : 1314,
"type" : "customType"
}
}
跳轉(zhuǎn)邏輯
// 接收到推送后跳轉(zhuǎn)
- (void)didReceiveRemoteNotificationAndPushToViewController:(NSDictionary *)userInfo {
// 創(chuàng)建類
NSString *class = userInfo[@"class"];
const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];
Class newClass = objc_getClass(className);
if (!newClass) {
Class superClass = [NSObject class];
newClass = objc_allocateClassPair(superClass, className, 0);
objc_registerClassPair(newClass);
}
// 創(chuàng)建跳轉(zhuǎn)控制器對象
id destinationViewController = [[newClass alloc] init];
// 對該對象賦值屬性
NSDictionary *propertys = userInfo[@"property"];
[propertys enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
// 檢測這個對象是否存在該屬性
if ([self checkIsExitPropertyWithdestinationViewController:destinationViewController verifyPropertyName:key]) {
[destinationViewController setValue:obj forKey:key];
}
}];
// 跳轉(zhuǎn)
UITabBarController *tabViewController = (UITabBarController *)self.window.rootViewController;
UINavigationController *sourceViewController = (UINavigationController *)tabViewController.viewControllers[tabViewController.selectedIndex];
[sourceViewController pushViewController:destinationViewController animated:YES];
}
// 檢測對象是否存在該屬性
- (BOOL)checkIsExitPropertyWithdestinationViewController:(id)destinationViewController verifyPropertyName:(NSString *)verifyPropertyName {
// 獲取對象里的屬性列表
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList([destinationViewController class], &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
// 屬性名轉(zhuǎn)成字符串
NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
// 判斷該屬性是否存在
if ([propertyName isEqualToString:verifyPropertyName]) {
free(properties);
return YES;
}
}
free(properties);
return NO;
}
總結(jié)
好好理解遠程推送的原理就會發(fā)現(xiàn),其實遠程推送并沒有那么難做啊掠哥。上面的一些圖片有些來源于蘋果官方文檔巩踏,有些是自己所截圖。一些知識也是參考了蘋果的官網(wǎng)文檔龙致。其中一些深入的推送相關(guān)知識普遍性不是太高蛀缝,所以也沒有提到顷链,例如:可操作通知類型目代,通知顯示國際化,自定義通知聲音嗤练,Provider-APNs-Device詳細連接情況及推送負載的底層數(shù)據(jù)格式等榛了。如果你對這些知識很感興趣也很歡迎私密我私下交流,共同進步煞抬。敬請期待本篇的姊妹篇iOS推送之本地推送(iOS Notification Of Local Notification)霜大。