最近公司專案升級重構(重寫),除了本來我所負責的模組,最後臨危受命接了推送(遠端和本地)相關的模組,順便把推送的相關知識複習了一遍。後期連續工作十幾天加上最後一天的通(瞎)宵(熬)達(一)旦(夜),也算是不辱使命。此文除了講解遠端推送相關的基本知識外,也會涉及一些推送相關的奇淫技巧。另外本文主要講解遠端推送,後續會出一篇iOS推送之本地推送(iOS Notification Of Local Notification)的姊妹篇。
此篇文章的邏輯如下圖所示:
遠端推送原理
學習一些東西前我認為最好能瞭解它的原理,這樣以後我們遇到問題的時候,就可以很快速的找到錯誤之所在,如果對原理不感興趣的同學可直接下翻到應用部分【遠端推送應用】。
iOS app大多數都是基於client/server模式開發的,client就是安裝在我們裝置上的app,server就是遠端伺服器,主要給我們的app提供資料,因為也被稱為Provider。那麼問題來了,當App處於Terminate狀態的時候,當client與server斷開的時候,client如何與server進行通訊呢?是的,這時候Remote Notifications很好的解決了這個困境。蘋果所提供的一套服務稱之為Apple Push Notification service,就是我們所謂的APNs。
推送訊息傳輸路徑: Provider-APNs-Client App
我們的裝置聯網時(無論是蜂窩聯網還是Wi-Fi聯網)都會與蘋果的APNs伺服器建立一個長連線(persistent IP connection),當Provider推送一條通知的時候,這條通知並不是直接推送給了我們的裝置,而是先推送到蘋果的APNs伺服器上面,而蘋果的APNs伺服器再通過與裝置建立的長連線進而把通知推送到我們的裝置上(參考圖1-1,圖1-2)。而當裝置處於非聯網狀態的時候,APNs伺服器會保留Provider所推送的最後一條通知,當裝置轉換為連網狀態時,APNs則把其保留的最後一條通知推送給我們的裝置;如果裝置長時間處於非聯網狀態下,那麼APNs伺服器為其儲存的最後一條通知也會丟失。Remote Notification必須要求裝置連網狀態下才能收到,並且太頻繁的接收遠端推送通知對裝置的電池壽命是有一定的影響的。
deviceToken的生成
當一個App註冊接收遠端通知時,系統會傳送請求到APNs伺服器,APNs伺服器收到此請求會根據請求所帶的key值生成一個獨一無二的value值也就是所謂的deviceToken,而後APNs伺服器會把此deviceToken包裝成一個NSData物件傳送到對應請求的App上。然後App把此deviceToken傳送給我們自己的伺服器,就是所謂的Provider。Provider收到deviceToken以後進行儲存等相關處理,以後Provider給我們的裝置推送通知的時候,必須包含此deviceToken。(參考圖1-3,圖1-4)
這個時候你可能會問deviceToken到底是什麼?有什麼用?為什麼是獨一無二的?
- 是什麼:deviceToken其實就是根據註冊遠端通知的時候向APNs伺服器傳送的Token key,Token key中包含了裝置的UDID和App的Bundle Identifier,然後蘋果APNs伺服器根據此Token key編碼生成一個deviceToken。deviceToken可以簡單理解為就是包含了裝置資訊和應用資訊的一串編碼。
- 有什麼用:上面提到Provider推送訊息的時候必須帶有此deviceToken,然後此訊息就根據deviceToken(UDID + App’s Bundle Identifier)找到對應的裝置以及該裝置上對應的應用,從而把此推送訊息推送給此應用。
- 唯一性:蘋果APNs的編碼技術和deviceToken的獨特作用保證了他的唯一性。唯一性並不是說一臺裝置上的一個應用程式永遠只有一個deviceToken,當使用者升級系統的時候deviceToken是會變化的。
遠端推送應用
註冊遠端通知(獲取deviceToken)
註冊遠端通知的方法
一般都是在App啟動完成的時候去註冊遠端通知註冊方法呼叫一般都在didFinishLaunchingWithOptions:方法中
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (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]; } |
處理註冊遠端通知的回撥方法
1 2 3 4 5 6 7 8 |
// 註冊成功回撥方法,其中deviceToken即為APNs返回的token - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { [self sendProviderDeviceToken:deviceToken]; // 將此deviceToken傳送給Provider } // 註冊失敗回撥方法,處理失敗情況 - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { } |
在iOS8之後增加了可操作通知型別,可操作通知允許開發者新增自定義跳轉事件。這些高階功能此篇文章不講解,有興趣的同學可自己去了解UIUserNotificationAction
UIMutableUserNotificationAction
UIUserNotificationCategory
UIMutableUserNotificationCategory
這幾個類。
處理接收到遠端通知訊息(會回撥以下方法中的某一個)
application: didFinishLaunchingWithOptions:
此方法在程式第一次啟動是呼叫,也就是說App從Terminate狀態進入Foreground狀態的時候,根據方法內程式碼判斷是否有推送訊息。
1 2 3 4 5 6 7 8 9 |
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // userInfo為收到遠端通知的內容 NSDictionary *userInfo = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (userInfo) { // 有推送的訊息,處理推送的訊息 } return YES; } |
application: didReceiveRemoteNotification:
如果App處於Background狀態時,只用使用者點選了通知訊息時才會呼叫該方法;如果App處於Foreground狀態,會直接呼叫該方法。
1 2 3 |
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { } |
application: didReceiveRemoteNotification: fetchCompletionHandler:
iOS7之前蘋果是不支援多工的,這也是iOS系統對硬體要求低,流暢性好的原因之一。iOS7之後,蘋果開始支援多工,即App可在後臺做一些更新UI、下載資料的操作等。若要接收到遠端推送的時候要在後臺做一些事情則需要把後臺遠端推送模式開啟。不適配iOS7之前系統的專案建議使用此後臺模式,充分利用蘋果推出的多工模式,不枉費蘋果的一片苦心啊!設定後臺模式方法專案對應TARGETS-Capabilities-Background Modes-Remote Notifications具體設定方法如下圖(圖2-1)。
此方法不論App處於Foreground狀態還是處於Background狀態,收到遠端推送訊息的時候都會立即呼叫此方法。此方法需要配置後臺模式並且在推送負載中必須有content-available此key值,對應的value值為1(詳細介紹參考下面【遠端通知負載內容】)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { // 在此方法中一定要呼叫completionHandler這個回撥,告訴系統是否處理成功 UIBackgroundFetchResultNewData, // 成功接收到資料 UIBackgroundFetchResultNoData, // 沒有接收到資料 UIBackgroundFetchResultFailed // 接受失敗 if (userInfo) { completionHandler(UIBackgroundFetchResultNewData); } else { completionHandler(UIBackgroundFetchResultNoData); } } |
可操作通知型別收到推送訊息時回撥方法
1 2 3 4 5 6 7 8 9 10 11 12 |
// 此兩個回撥方法對應可操作通知型別,具體使用方法參考以上方法很容易理解,不在詳細敘述 - (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 { } |
客戶端和服務端的互動
說到這裡我就隨意吐槽一下推送,做推送個人感覺還是比較費勁的。而第一次啟動App時詢問使用者是否接受推送訊息的時候,大部分使用者都會點選拒絕推送的吧,反正我是這樣的。你辛辛苦苦做好了,想辦法保證其推送準時性,想辦法保證其推送到達率,結果使用者一個拒絕,你所以的努力全都白費了啊,哈哈哈。
我這裡主要想說的就是:我們要把對應的.p12(個人資訊交換證書)證書給服務端的開發人員就好了。具體可參看我另一篇文章不讓蘋果開發者賬號折磨我中的團隊開發證書的管理中的匯出.p12章節。
遠端推送負載
遠端推送負載大小
遠端通知負載的大小根據Provider使用的API不同而不同。當使用HTTP/2 provider API時,負載最大為4096bytes,即4kB;當使用legacy binary interface時,負載最大為2048bytes,即2kB。當負載大小超過規定的負載大小時,APNs會拒絕傳送此訊息。
遠端推送負載內容
內容格式必要要知道的啊,服務端一般會要我們客戶端定義好格式給他們的。
每一條通知的訊息都會組成一個JSON字典物件,其格式如下所示,示例中的key值為蘋果官方所用key。自定義欄位的時候要避開這些key值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
{ "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值,系統就會根據使用者的設定展示標準的推送資訊 badge:在app圖示上顯示訊息數量,缺少此key值,訊息數量就不會改變,消除標記時把此key對應的value設定為0 sound:設定推送聲音的key值,系統預設提示聲音對應的value值為default content-available:此key值設定為1,系統接收到推送訊息時就會呼叫不同的回撥方法,iOS7之後配置後臺模式 category:UIMutableUserNotificationCategory's identifier 可操作通知型別的key值 title:簡短描述此調推送訊息的目的,適用系統iOS8.2之後版本 body:推送的內容 title-loc-key:功能類似title,附加功能是國際化,適用系統iOS8.2之後版本 title-loc-args:配合title-loc-key欄位使用,適用系統iOS8.2之後版本 action-loc-key:可操作通知型別key值,不詳細敘述 loc-key:參考title-loc-key loc-args:參考title-loc-args launch-image:點選推送訊息或者移動事件滑塊時,顯示的圖片。如果缺少此key值,會載入app預設的啟動圖片。 |
當然以上key值並不是每條推送訊息都必帶的key值,應當根據需求來選擇所需要的key值,除了以上系統所提供的key值外,你還可以自定義自己的key值,來作為訊息推送的負載,自定義key值與aps此key值並列。如下格式:
1 2 3 4 5 6 7 8 9 |
{ "aps" : { "alert" : "Provider push messag.", "badge" : 9, "sound" : "toAlice.aiff" }, "Id" : 1314, // 自定義key值 "type" : "customType" // 自定義key值 } |
指定使用者的推送
對於要求使用者登入的App,推送是可以指定使用者的,同一條推送有些使用者可以收到,但是有些使用者又不能收到。說起來這個就要提到另外的一個token了,一般稱之為userToken,userToken一般都是根據自己公司自定義的規則去生成的。userToken是以使用者的賬號加對應的密碼生成的。這樣結合上面提到的deviceToken,就可以做到根據不同的使用者推送不同的訊息。deviceToken找到對應某臺裝置和該裝置上的應用,而userToken對應找到該使用者。客戶端在上報deviceToken的時候,要把userToken對應一起上報給服務端也就是Provider。
淺談推送第三方SDK
關於第三方推送的SDK有很多,常見的有極光推送 百度推送 個推 友盟推送等等。其實推送的原理都是大同小異的,理解了蘋果推送的原理,這些第三方SDK還在是基本原理上面進行了擴充套件。對於用不用第三方SDK其實對我們客戶端影響不大,推送第三方SDK主要是方便了服務端開發者。主要表現為服務端開發者不需要去開發維護自己的推送伺服器與 APNs 對接,不必自己維護更新 deviceToken。當然了,第三方SDK也會提供一些額外的附屬功能例如JPush提供了應用內訊息推送,這在類似於聊天的場景裡很方便的。看完這段是不是發現整合推送的第三方SDK和客戶端沒什麼關係,我們工作量不僅沒有減少,反而增加了一點點啊。至於第三方SDK的其他功能,大家可自行去對應官網學習,這裡不再過多描述。
利用runtime實現推送訊息萬能跳轉
此段參考了@漢斯哈哈哈的一篇iOS 萬能跳轉介面方法萬能跳轉就是可以跳轉到指定的任意一個介面,但是這個和服務端耦合性太強,使用的時候要慎重考慮,而且公司一般都是iOS,Android共用同一套推送規則很難讓服務端在給你開一條新的推送規則,不便於維護,而且成本也是需要考慮的。寫此段的目的就是當產品有這樣的需求的時候還是可以參考一下的。
定義推送規則
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 客戶端控制器的屬性 @interface YBViewController : UIViewController /** 頻道Id */ @property (nonatomic, copy) NSString *Id; /** 頻道type */ @property (nonatomic, copy) NSString *type; @end // 服務端推送資料格式 { "aps" : { "alert" : "Provider push messag" }, "class" : "YBViewController", "property" : { "Id" : 1314, "type" : "customType" } } |
跳轉邏輯
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// 接收到推送後跳轉 - (void)didReceiveRemoteNotificationAndPushToViewController:(NSDictionary *)userInfo { // 建立類 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); } // 建立跳轉控制器物件 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]; } }]; // 跳轉 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 |
總結
好好理解遠端推送的原理就會發現,其實遠端推送並沒有那麼難做啊。上面的一些圖片有些來源於蘋果官方文件,有些是自己所截圖。一些知識也是參考了蘋果的官網文件。其中一些深入的推送相關知識普遍性不是太高,所以也沒有提到,例如:可操作通知型別,通知顯示國際化,自定義通知聲音,Provider-APNs-Device詳細連線情況及推送負載的底層資料格式等。如果你對這些知識很感興趣也很歡迎私密我私下交流,共同進步。敬請期待本篇的姊妹篇iOS推送之本地推送(iOS Notification Of Local Notification)。