iOS 推送手机消息背后的技术
作者:allenzzhao,騰訊 ?IEG運營開發工程師
消息推送我們幾乎每天都會用到,但你知道iOS中的消息推送是如何實現的嗎?本文將從推送權限申請,到本地和遠程消息推送,再到App對推送消息的處理等多個步驟,詳細介紹iOS中消息推送的工作流程。
1、概述
消息推送是一種App向用戶傳遞信息的重要方式,無論App是否正在運行,只要用戶打開了通知權限就能夠收到推送消息。開發者通過調用iOS系統方法就可以發起本地消息推送,例如我們最常見的鬧鐘應用,App能夠根據本地存儲的鬧鐘信息直接發起本地通知,因此即使沒有網絡也能收到鬧鐘提醒。
遠程消息推送則是由業務方服務器將消息內容按照固定格式發送到Apple Push Notitfication service(簡稱APNs),然后再經由蘋果的APNs服務器推送到用戶設備上,例如騰訊新聞可以向用戶推送時事熱點新聞,QQ郵箱可以為用戶推送收到新郵件的提醒,游戲App可以通過這種方式通知玩家有新的游戲福利,既能夠及時地通知用戶重要信息,也能夠促使用戶通過推送消息打開或喚醒App,提高App的使用率。除了標題、內容、提示音和角標數字等固定推送參數以外,開發者還可以在推送消息中增加自定義參數,讓用戶在點擊推送消息時能夠直達相關新聞、郵件或福利頁面,提供更好的用戶體驗和頁面的曝光率。
2、XCode配置
在使用消息推送相關功能之前,我們首先需要準備支持推送功能的證書,個人開發者可以參考騰訊云的TPNS文檔,在蘋果開發者中心中配置和導出推送證書。
此外,還需要在XCode的工程配置Signing & Capabilities配置中增加消息推送權限,在操作完成后Xcode會自動生成或更新工程的entitlements文件,增加如圖所示的APS Environment字段。
3、申請消息推送權限
無論是本地推送還是遠程推送,在推送前都必須要先向用戶申請推送權限,只有用戶授權后才能夠收到推送消息。
蘋果在iOS10中引入了UserNotifications框架,將推送相關功能進行了封裝和升級,除了以前UIApplication可以做到的一些基本的本地和遠程消息推送功能外,還增加了撤回或修改推送消息、自定義通知UI、推送消息前臺顯示等功能。在iOS10及以上的版本中,蘋果推薦開發者使用requestAuthorizationWithOptions:completionHandler:方法向用戶申請消息推送權限,該方法需要指定一個用于描述推送權限的UNAuthorizationOptions類型參數,包括alert(消息的標題、文字等內容)、sound(消息提示音)、badge(App右上角顯示的角標);還可以在該方法的completionHandler回調方法中通過granted參數來判斷用戶是否允許了授權。相關代碼如下:
#import?<UserNotifications/UserNotifications.h> …… [[UNUserNotificationCenter?currentNotificationCenter] requestAuthorizationWithOptions:UNAuthorizationOptionSound|UNAuthorizationOptionAlert|UNAuthorizationOptionBadge completionHandler:^(BOOL?granted,?NSError?*?_Nullable?error)?{if(granted){//用戶允許了推送權限申請}else{//用戶拒絕了推送權限申請} }];在iOS9中,直接使用UIApplication的registerUserNotificationSettings方法即可,該方法同樣需要通過配置sound、alert、badge等參數,但是沒有提供用于判斷用戶點擊了授權還是拒絕的回調方法。相關代碼如下:
[[UIApplication?sharedApplication]?registerUserNotificationSettings:[UIUserNotificationSettings?settingsForTypes:(UIUserNotificationTypeSound?|?UIUserNotificationTypeAlert?|?UIUserNotificationTypeBadge)categories:nil]];要注意無論是UserNotifications還是UIApplication的申請推送權限的方法,上文中的申請用戶授權的系統彈窗都只會顯示一次,iOS會記錄用戶對于該App的授權狀態,不會向用戶重復申請授權。消息推送是App的一項重要功能,同時也是很好的運營手段,因此很多App在啟動后會檢查消息推送的授權狀態,如果用戶拒絕了消息推送權限,仍然會以一定的頻率彈窗提醒用戶,在iOS的設置中心中再去打開App的推送權限。相關代碼如下:
if(@available(iOS?10.0,*)){[[UNUserNotificationCenter?currentNotificationCenter]?getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings?*?_Nonnull?settings)?{if?(UNAuthorizationStatusDenied?==?settings.authorizationStatus)?{//用戶拒絕消息推送,彈窗提示引導用戶去系統設置中進行授權UIAlertController*?alert?=?[UIAlertController?alertControllerWithTitle:@"未打開推送功能"?message:@"請在設備的\"設置-App-通知\"選項中,允許通知"?preferredStyle:UIAlertControllerStyleAlert];UIAlertAction*?cancel?=?[UIAlertAction?actionWithTitle:@"取消"?style:UIAlertActionStyleCancel?handler:^(UIAlertAction*?action){[alert?dismissViewControllerAnimated:?YES?completion:?nil];}];UIAlertAction*?ok?=?[UIAlertAction?actionWithTitle:@"去設置"?style:UIAlertActionStyleDefault?handler:^(UIAlertAction*?action){[alert?dismissViewControllerAnimated:?YES?completion:?nil];NSURL?*?url?=?[NSURL?URLWithString:UIApplicationOpenSettingsURLString];if([[UIApplication?sharedApplication]?canOpenURL:url]){NSURL*url?=[NSURL?URLWithString:UIApplicationOpenSettingsURLString];[[UIApplication?sharedApplication]?openURL:url];}}];[alert?addAction:?cancel];[alert?addAction:?ok];[[UIApplication?sharedApplication].keyWindow.rootViewController?presentViewController:alert?animated:?YES?completion:?nil];}}]; }else{UIUserNotificationSettings?*setting?=?[[UIApplication?sharedApplication]?currentUserNotificationSettings];if?(UIUserNotificationTypeNone?==?setting.types)?{//用戶拒絕消息推送,處理方式同上} }4、本地推送
在iOS10中,UserNotifications框架為我們提供了UNMutableNotificationContent對象描述消息推送的標題、內容、提示音、角表等內容,UNNotificationTrigger對象描述消息推送的推送時間策略,UNNotificationRequest對象整合推送內容和時間。每個Request對象都需要配置一個id來標識該條推送內容,UNUserNotificationCenter通過該id來管理(包括增加、刪除、查詢和修改)所有的Request。UNNotificationTrigger有四個子類,分別是UNTimeIntervalNotificationTrigger用于通過時間間隔控制消息推送,UNCalendarNotificationTrigger通過日期控制消息推送,UNLocationNotificationTrigger通過地理位置控制消息推送,UNPushNotificationTrigger遠程消息推送對象。相關代碼如下:
在iOS9中,UIApplication提供了presentLocalNotificationNow和scheduleLocalNotification兩個本地消息推送的方法,分別表示立即推送和按照固定日期推送,UILocalNotification同時描述了消息內容和推送的時機。示例代碼是一個2s后推送的本地消息,soundName屬性用于描述消息的提示音,用戶可以自定義提示音(需要將音頻文件打包到安裝包中)或者使用默認提示音樂,repeatInterval和repeatCalendar屬性分別用于根據時間差和日期進行重復提示的操作。相關代碼如下:
UILocalNotification?*notification?=?[[UILocalNotification?alloc]?init]; notification.fireDate?=?[NSDate?dateWithTimeIntervalSinceNow:2]; notification.alertTitle?=?@"推送標題"; notification.alertBody?=?@"推送內容"; //notification.soundName?=?UILocalNotificationDefaultSoundName; notification.soundName?=?@"mysound.wav"; [[UIApplication?sharedApplication]?scheduleLocalNotification:notification];5、遠程推送
不同于本地消息推送不依賴網絡請求,可以直接調用iOS系統方法,遠程消息推送的實現涉及到用戶設備、我們自己的業務方服務器和蘋果的APNs服務的交互。不同于Android系統中遠程消息推送的實現,需要App自身通過后臺服務與業務服務器維持長鏈接通信,iOS中的消息推送是操作系統與蘋果的APNs服務器直接交互實現的,App自身并不需要維持與服務器的連接。只要用戶開啟了推送權限,我們的業務服務器就可以隨時通過調用APNs服務向用戶推送通知,這樣既能夠為開發者和用戶提供安全穩定的推送服務,也夠節省系統資源消耗,提高系統流暢度和電池續航能力。
iOS客戶端遠程消息推送的實現可以分為以下幾個流程:
用戶的iphone通過iOS的系統方法調用與蘋果的APNs服務器通信,獲取設備的deviceToken,它是由APNs服務分配的用于唯一標識不同設備上的不同App,可以認為是由deviceID、bundleId和安裝時的相關信息生成的,App的升級操作deviceToken不變,卸載重裝App、恢復和重裝操作系統后的deviceToken會發生變化。
蘋果的APNs服務是基于deviceToken實現的,因此需要將設備的deviceToken發送到我們的業務服務器中,用于后續的消息推送。一個設備可能登錄過多個用戶,一個用戶也可能在多個設備中登錄過,當我們需要給不同用戶推送不同的消息時,除了deviceToken之外,我們還需要保存用戶的openid與deviceToken的映射關系。我們可以在用戶登錄成功后的時機更新openid和deviceToken的映射關系,用戶退出后取消映射關系,只保存用戶最后登錄設備的deviceToken,避免一個設備收到多個重復通知和一個用戶在不同設備收到多個通知等情況。
在新聞類App出現事實熱點新聞時,后臺服務就可以攜帶消息內容和deviceToken等內容,向蘋果的APNs服務發起消息推送請求,推送消息的實現是異步的,只要請求格式和deviceToken檢查通過APNs服務就不會報錯,但是用戶還是可能因為網絡異常或者關閉了推送權限等原因收不到推送消息。
APNs服務向用戶設備推送消息這一步也是異步的,在用戶關機或網絡異常收不到推送的情況下,APNs會為每個deviceToken保留最后一條推送消息,待網絡恢復后再次推送。
5.1、獲取設備deviceToken
在App啟動時,我們可以通過UIApplication的registerForRemoteNotifications方法向蘋果的APNS服務器請求deviceToken,如果請求成功則didRegisterForRemoteNotificationsWithDeviceToken回調方法會被執行,為了便于業務服務器的調用,我們一般會將二進制的deviceToken轉換為16進制的字符串后再進行存儲;如果請求失敗則didFailToRegisterForRemoteNotificationsWithError方法也會被調用,并附帶具體的錯誤信息。相關代碼如下:
//調用系統方法請求deviceToken -?(BOOL)application:(UIApplication?*)application?didFinishLaunchingWithOptions:(NSDictionary?*)launchOptions?{[[UIApplication?sharedApplication]?registerForRemoteNotifications]; } //deviceToken獲取成功的回調 -?(void)application:(UIApplication?*)application?didRegisterForRemoteNotificationsWithDeviceToken:(NSData?*)deviceToken{NSString?*deviceTokenStr;NSUInteger?length?=?deviceToken.length;if?(![deviceToken?isKindOfClass:[NSData?class]]?||?length?==?0)?{return;}const?unsigned?char?*bytes?=?(const?unsigned?char?*)deviceToken.bytes;NSMutableString?*hex?=?[NSMutableString?new];for?(NSInteger?i?=?0;?i?<?deviceToken.length;?i++)?{[hex?appendFormat:@"%02x",?bytes[i]];}deviceTokenStr?=?[hex?copy];NSLog(@"%@",?deviceTokenStr); } //deviceToken獲取失敗的回調 -?(void)application:(UIApplication?*)application?didFailToRegisterForRemoteNotificationsWithError:(NSError?*)error{NSLog(@"error,%@",error); }5.2、后臺調用APNs推送
業務方服務器調用APNs服務時首先要建立安全連接,進行開發者身份的認證,分為基于證書(Certificate-Based)和基于Token(Token-Based)的認證兩種方式,比較常用的是基于證書的認證方式。推送證書分為開發環境和生產環境的證書,分別對應不同的APNs推送接口,我們從蘋果開發者平臺或者第三方平臺導出的推送證書一般有p12和pem兩種格式的文件,為了便于接口調用我們可以通過以下命令將p12格式的文件轉換為pem證書。
openssl?pkcs12?-in?push_dev.p12?-out?push_dev.pem?-nodes基于證書建立TLS連接的流程如下圖所示:
業務方服務器(Provider)向APNs服務器發起建立TLS連接的請求。
APNs服務器返回的它的證書,供業務方服務器校驗。
業務方服務器提供自己的推送證書,供APNs服務器校驗。
APNs服務器驗證業務方服務器提供的推送證書無誤后,TLS連接就已經建立完成,之后業務方服務器就可以直接向APNs發送消息推送請求了。
業務方與APNs建立請求的簡易實現的PHP代碼實現如下:
$deviceToken=?'22124c450762170ca2ddb32a50381dd2c3026dbdb020f6dddcabefdca724fdd6'; //dev?params $devUrl?=?'ssl://gateway.sandbox.push.apple.com:2195'; $devCertificate?=?'push_dev.pem'; //product?params $proUrl?=?'ssl://gateway.push.apple.com:2195'; $proCertificate?=?'push_pro.pem'; //?Change?2?:?If?any $title?=?'標題';//消息標題 $content?=?'消息內容';//內容 $ctx?=?stream_context_create(); //?Change?3?:?APNS?Cert?File?name?and?location. stream_context_set_option($ctx,?'ssl',?'local_cert',?$devCertificate); //?Open?a?connection?to?the?APNS?server $fp?=?stream_socket_client($devUrl,?$err,?$errstr,?60,?STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT,?$ctx); if?(!$fp)exit("Failed?to?connect:?$err?$errstr"?.?PHP_EOL); echo?'Connected?to?APNS'?.?PHP_EOL; //?Create?the?payload?body $body['aps']?=?array('alert'?=>array('title'=>$title,'body'=>$content),'sound'?=>?'default'); //自定義內容 $body['userInfo']?=?array('url'?=>?'https://www.qq.com', ); //?Encode?the?payload?as?JSON $payload?=?json_encode($body); //?Build?the?binary?notification $msg?=?chr(0)?.?pack('n',?32)?.?pack('H*',?$deviceToken)?.?pack('n',?strlen($payload))?.?$payload; //?Send?it?to?the?server $result?=?fwrite($fp,?$msg,?strlen($msg)); //發送多個就調用多次fwrite //$result?=?fwrite($fp,?$msg,?strlen($msg)); echo?$msg; if?(!$result)echo?'Message?not?delivered'?.?PHP_EOL; elseecho?'Message?successfully?delivered'?.?PHP_EOL; //?Close?the?connection?to?the?server fclose($fp);業務方服務器通過證書與APNs建立安全連接后可以進行連續多次的消息推送操作,每次消息推送都要指定deviceToken和Payload參數。Payload是一個json對象,用于配置iOS在收到遠程消息推送時的展現形式,aps參數包含了蘋果預設的alert、sound、badge等參數,其中alert參數可以是字符串,或者包含title、body等參數的字典類型;badge參數使用整形設置App圖標右上角顯示的數字,badge設置為0時角標不會顯示;sound參數用于設置推送的聲音,不傳該參數或者傳遞空字符串則推送不會發出提示音,設置為default時使用系統默認提示音,也可以設置為具體的音頻文件名,需要提前音頻文件放到項目的bundle目錄,且時長不能超過30s。
除了預設參數以外,我們還可以在aps的同級自定義一些參數,這些參數也可以是字典類型,再嵌套其他參數,例如示例代碼中我們自定義的userInfo對象,但是一般推送消息的payload不宜過大,應控制在4K以內,建議只透傳一些id和url等關鍵參數,具體的內容由客戶端在收到推送時再去通過網絡請求獲取。
{"aps"?:?{"alert"?:?{"title"?:?"Game?Request","subtitle"?:?"Five?Card?Draw","body"?:?"Bob?wants?to?play?poker",},"badge"?:?9,"sound"?:?"gameMusic.wav",},"gameID"?:?"12345678" }上述payload包含了常見的推送消息的標題、副標題、內容、消息提示音、App的角標數字等預設參數,以及一個開發者自定義的gameID參數。用戶點擊推送消息后會自動啟動或從后臺喚醒App,我們可以在系統的回調方法中獲取到自定義參數,并根據gameID自動為用戶打開該游戲頁面。
5.3、消息推送調試工具
在進行APNs接口調試時,我們可以利用一些優秀的推送調試工具幫助我們驗證payload或證書等內容的合法性。本文介紹兩款比較流行的開源軟件,分別是國外的Knuff和國內開發者維護的smartPush。
Knuff:https://github.com/KnuffApp/Knuff
SmartPush:https://github.com/shaojiankui/SmartPush
6、App推送消息的處理
在iOS10中,UserNotifications框架為開發者提供了UNUserNotificationCenterDelegate協議,開發者可以通過實現協議中的方法,在App接收到推送消息和用戶點擊推送消息時進行一些業務邏輯的處理。無論是本地推送還是遠程推送的消息,App的運行狀態都可能處于以下三種狀態:
App正在前臺運行,此時用戶正在使用App,收到推送消息時默認不會彈出消息提示框,willPresentNotification回調方法會被調用,開發者可以從UNNotification對象中獲取該推送消息的payload內容,進而獲取自定義參數,然后顯示一個自定義彈窗提示用戶收到了新的消息;也可以在willPresentNotification方法中通過completionHandler函數的調用讓推送消息直接在前臺顯示,用戶點擊前臺顯示的推送消息時,didReceiveNotificationResponse回調方法也會被執行。
App在后臺運行,此時用戶點擊推送消息會將App從后臺喚醒,didReceiveNotificationResponse回調方法會被執行,開發者可以在該方法中獲得payload,解析自定義參數并自動打開對應的頁面。
App尚未啟動,此時用戶點擊推送消息會打開App,開發者可以從launchOptions中獲取本地或遠程推送消息中的自定義參數,待頁面初始化完成后進行相關頁面的跳轉。
在iOS9中,UIApplication提供了下面三個消息推送的處理方法,分別是遠程消息推送、遠程靜默推送和本地消息推送的回調處理方法。前兩個回調方法都能夠用于App遠程消息推送的處理,同時使用時只有遠程靜默推送方法會被調用,當payload包含參數content-available=1時,該推送就是靜默推送,靜默推送不會顯示任何推送消息,當App在后臺掛起時,靜默推送的回調方法會被執行,開發者有30s的時間內在該回調方法中處理一些業務邏輯,并在處理完成后調用fetchCompletionHandler。
//遠程消息推送回調方法,ios(3.0,?10.0) -?(void)application:(UIApplication?*)application?didReceiveRemoteNotification:(NSDictionary?*)userInfo; //遠程靜默推送回調方法,ios(7.0,?*) -?(void)application:(UIApplication?*)application?didReceiveRemoteNotification:(NSDictionary?*)userInfo?fetchCompletionHandler:(void?(^)(UIBackgroundFetchResult?result))completionHandler?API_AVAILABLE(ios(7.0)); //本地消息推送回調方法,ios(4.0,?10.0) -(void)application:(UIApplication?*)application?didReceiveLocalNotification:(UILocalNotification?*)notification;UIApplication中的這三個方法在:①App在前臺運行時收到通知,②App在后臺運行時用戶點擊推送消息拉起App,這兩種場景下都會被調用,區別是前兩種方法對應遠程消息推送的接收和點擊觸發響應,didReceiveLocalNotification用于本地消息推送。我們可以通過UIApplication的applicationState屬性來判斷App是否在前臺運行,然后分別實現:①用戶點擊消息喚起后臺App并打開對應頁面,②用戶前臺使用App時顯示自定義彈窗。
-?(void)application:(UIApplication?*)application?didReceiveRemoteNotification:(NSDictionary?*)userInfo{if([UIApplication?sharedApplication].applicationState?==?UIApplicationStateActive){NSLog(@"在前臺,%@",userInfo);}else{NSLog(@"從后臺進入前臺,%@",userInfo);NSDictionary?*params?=?userInfo[@"userInfo"];if([Tools?isValidString:params[@"url"]]){NSString?*routeUrl?=?params[@"url"];[PageSwitch?handlePushSwitch:params];}} }7、總結
本文首先介紹了消息推送相關的工程配置和推送權限的申請,然后分別介紹了本地和遠程消息推送的不同使用場景和實現方法,最后介紹了App在收到推送消息后的相關回調方法和處理邏輯。在實際的項目開發中,我們往往會選擇騰訊云推送或極光推送等更加成熟的第三方消息推送平臺,這些平臺都提供了相對完善的推送和數據統計服務,通過接口和SDK屏蔽了底層邏輯的實現,通過對iOS消息推送的實現過程的了解也能夠幫助我們更好的使用這些平臺。
由于時間的關系,自己的研究并不深入,如有疏漏和錯誤,歡迎留言指正交流~
8、擴展閱讀
蘋果官方技術文檔,https://developer.apple.com/documentation/usernotifications
史上最全iOS Push技術詳解,https://cloud.tencent.com/developer/article/1198303
iOS遠程推送-APNs詳解,https://juejin.im/post/6844903893592178696
iOS靜默推送進階知識,https://www.jianshu.com/p/c211bd295d58
iOS10自定義通知UI,https://www.jianshu.com/p/85ac47bdf387
信鴿文檔-推送服務介紹,https://xg.qq.com/docs/ios_access/ios_push_introduction.html
淺談iOS和Android后臺實時消息推送的原理和區別,https://cloud.tencent.com/developer/article/1150967
淺談基于HTTP2推送消息到APNs,http://www.linkedkeeper.com/167.html
PHP基于socket的ios 推送的實現,https://www.fzb.me/2015-9-7-sockect-implement-for-apns.html
如何構建一套高可用的移動消息推送平臺?,https://www.infoq.cn/article/HA-mobile-message-push-platform
總結
以上是生活随笔為你收集整理的iOS 推送手机消息背后的技术的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何有效地进行代码 Review?
- 下一篇: 从无盘启动看 Linux 启动原理