(點擊上方公眾號,,可快速關(guān)注我們)
中文:陳思(@陳思Siming) 鏈接:http://ios./83952/
最近公司項目升級重構(gòu)(重寫),,除了本來我所負責的模塊,最后臨危受命接了推送(遠程和本地)相關(guān)的模塊,,順便把推送的相關(guān)知識復(fù)習了一遍,。后期連續(xù)工作十幾天加上最后一天的通(瞎)宵(熬)達(一)旦(夜),也算是不辱使命,。此文除了講解遠程推送相關(guān)的基本知識外,,也會涉及一些推送相關(guān)的奇淫技巧。另外本文主要講解遠程推送,,后續(xù)會出一篇iOS推送之本地推送(iOS Notification Of Local Notification)的姊妹篇,。
此篇文章的邏輯如下圖所示:
圖0-0 此篇文章的邏輯圖 遠程推送原理
學習一些東西前我認為最好能了解它的原理,這樣以后我們遇到問題的時候,,就可以很快速的找到錯誤之所在,,如果對原理不感興趣的同學可直接下翻到應(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è)備的電池壽命是有一定的影響的,。
圖1-1 Pushing a remote notification from a provider to a client app 圖1-2 Pushing remote notifications from multiple providers to multiple devices 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)
圖1-3 Managing the device token 圖1-4 Sharing the device token 這個時候你可能會問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是會變化的,。
遠程推送應(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ù)模式,,不枉費蘋果的一片苦心?。≡O(shè)置后臺模式方法項目對應(yīng)TARGETS-Capabilities-Background Modes-Remote Notifications具體設(shè)置方法如下圖(圖2-1),。
圖2-1 Setting App Background Modes 此方法不論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ā)送此消息。 遠程推送負載內(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)找到該用戶,??蛻舳嗽谏蠄骴eviceToken的時候,要把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
總結(jié)
好好理解遠程推送的原理就會發(fā)現(xiàn),,其實遠程推送并沒有那么難做啊,。上面的一些圖片有些來源于蘋果官方文檔,有些是自己所截圖,。一些知識也是參考了蘋果的官網(wǎng)文檔,。其中一些深入的推送相關(guān)知識普遍性不是太高,所以也沒有提到,,例如:可操作通知類型,,通知顯示國際化,自定義通知聲音,,Provider-APNs-Device詳細連接情況及推送負載的底層數(shù)據(jù)格式等,。如果你對這些知識很感興趣也很歡迎私密我私下交流,共同進步,。敬請期待本篇的姊妹篇iOS推送之本地推送(iOS Notification Of Local Notification),。
參考文獻
蘋果開發(fā)者文檔Local and Remote Notification Programming Guide iOS 萬能跳轉(zhuǎn)界面方法
|