一、推送原理
當用戶打開應用程序的通知中心之后,蘋果遠程推送服務器就能把消息推送到裝有該應用的設備上,具有強制性、實時性的特點,並且用戶無需打開應用都能收到推送的消息。
1.1 名詞介紹
- Provider:消息提供者,一般是我們的后台服務器或者第三方推送服務器后台
- APNs(Apple Push Notification service):蘋果推送通知服務。
- APNs Server(Apple Push Notification service Server):蘋果推送通知服務的服務器。
- notification:需要推送給 iOS 客戶端(iPhone或者是iPad)上的消息
- Client App:客戶端 App,一般是安裝在iPhone或者是iPad上的應用程序(App)
- deviceToken:是由 APNs 根據設備和App來生成的唯一的一串數據。deviceToken 在以下三種情況下會發生改變:
- 同一個設備上重新安裝同一款應用
- 同一個應用安裝在不同的設備上
- 設備重新安裝了系統,同一個應用對應的 deviceToken 也會改變
1.2 推送原理
從圖中可以很清楚的看出來推送的原理主要分為以下幾步:
- 由 App 向 iOS 設備發送一個注冊通知,用戶需要同意系統發送推送;
- iOS 向 APNs 遠程推送服務器發送 App 的 Bundle Id 和設備的 UDID;
- APNs 根據設備的 UDID 和 App 的 Bundle Id 生成 deviceToken 再發回給 App;
App 再將 deviceToken 發送給遠程推送服務器(自己的服務器), 由服務器保存在數據庫中。
當自己的服務器想發送推送時,在遠程推送服務器中輸入要發送的消息並選擇發給哪些用戶的deviceToken,由遠程推送服務器發送給 APNs。
APNs 根據 deviceToken 發送給對應的用戶。
詳細流程:
在今日頭條 App 的 AppDelegate 的
didFinishLaunchingWithOptions
方法中注冊遠程推送通知,此時只要 iOS 設備正常聯網能夠訪問到外網,iOS 設備默認就會和 APNs 服務器維持一個基於 TCP 的長連接,就會把 iOS 設備的 UDID(Unique Device Identifier:唯一設備標識碼,用來標識唯一一台蘋果設備)和 App 的 Bundle Identifier 通過長連接發送給 APNs 服務器,然后蘋果通過這兩個的值根據一定的加密算法得出 deviceToken,並將 deviceToken 返回給 iOS 設備。(注:APNs服務器會留有 UDID+Bundle Identifier+deviceToken 的映射表)實現 UIApplicationDelegate 代理中的有關於注冊遠程通知的相關方法,包括注冊成功、注冊失敗、對接收到通知的處理等。
如果注冊成功,實現注冊成功的代理方法,就能夠接收到 deviceToken,並將 deviceToken 發送給 App 服務器,App 服務器將此 deviceToken 存儲在數據庫中(一般如果是及時通訊類應用那么還會與用戶的賬號進行映射)。
如果注冊失敗,那么實現注冊失敗的協議方法,處理失敗后的事情。
app 服務器接收到 deviceToken 之后,就可以根據這些 deviceToken 向 APNs 發送推送消息。
APNs 接收到 deviceToken 和消息之后,根據 deviceToken 查找映射表找到對應的 UDID 和 Bundle Identifier,根據 UDID 找到唯一一台蘋果設備,再在找到的蘋果設備上根據 Bundle Identifier 找到唯一的應用,然后推送消息。
當設備接收到消息的時候,如果 App 在前台,那么不會在設備上方彈出橫幅(如果使用了音效,還會觸發音效的播放),直接調用我們實現的 UIApplicationDelegate 中的接收消息的方法;如果 App 在后台或者未運行時就會在設備的上方彈出橫幅(如果使用了音效,還會觸發音效的播放),點擊橫幅才會觸發調用我們實現的 UIApplicationDelegate 中的接收消息的方法,這個時候你直接點擊應用圖標進來是不會調用的。
二、信息包
信息包結構圖:
上圖顯示的這個消息體就是我們的應用服務器(Provider)發送給 APNs 服務器的消息結構,APNs 驗證這個結構正確並提取其中的信息后,再將消息推送到指定的 iOS 設備。
這個結構體包括五個部分
- 第一部分是命令標示符
- 第二部分是 devicetoken 的長度
- 第三部分是 devicetoken 字符串
- 第四部分是推送消息體(Payload)的長度
- 最后一部分也就是真正的消息內容了,里面包含了推送消息的基本信息,比如消息內容,應用 Icon 右上角顯示多少數字以及推送消息到達時所播放的聲音等
Payload(消息體)的結構:
{
“aps”:{
“alert”:“CSDN給您發送了新消息”,
“badge”:1,
“sound”:“default”
},
}
這其實就是個 JSON 結構體,alert 標簽的內容就是會顯示在用戶手機上的推送信息,badge 顯示的數量(注意是整型)是會在應用 Icon 右上角顯示的數量,提示有多少條未讀消息等,sound 就是當推送信息送達是手機播放的聲音,傳 defalut 就標明使用系統默認聲音。
三、證書
應用的調試證書、描述文件
應用的發布證書、描述文件
推送的調試證書和發布證書
四、后台接收通知
開啟推送。
當推送信息中包含 content-available
字段,並且等於 1
{
"_j_business" = 1;
"_j_msgid" = 29273432613945685;
"_j_uid" = 31254343846;
aps = {
alert = 11111;
badge = 1;
"content-available" = 1;
sound = default;
};
}
app 即使在后台也能在 appDelegate 中觸發代理回調
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler: (void (^)(UIBackgroundFetchResult))completionHandler
{
}
五、Notification Extension
iOS10推送通知進階(Notification Extension)
- UNNotificationContentExtension(通知內容擴展)給通知創建一個自定義的用戶界面;
- UNNotificationServiceExtension(通知服務擴展)是在收到通知后,展示通知前,做一些事情的。比如:增加附件,網絡請求等。
5.1 UNNotificationServiceExtension - 通知服務擴展
如果經常使用 iMessage 的朋友們,就會經常收到一些信息,附帶了一些照片或者視頻,所以推送中能附帶這些多媒體是非常重要的。如果推送中包含了這些多媒體信息,可以使用戶不用打開 app,不用下載就可以快速瀏覽到內容。眾所周知,推送通知中帶了 push payload,即使去年蘋果已經把 payload 的 size 提升到了 4k bites,但是這么小的容量也無法使用戶能發送一張高清的圖片,甚至把這張圖的縮略圖包含在推送通知里面,也不一定放的下去。在 iOS X 中,我們可以使用新特性來解決這個問題。我們可以通過新的 service extensions 來解決這個問題。
iOS10 給通知添加附件有兩種情況:本地通知和遠程通知。
本地推送通知
只需給 content.attachments 設置 UNNotificationAttachment 附件對象
遠程推送通知
需要實現 UNNotificationServiceExtension(通知服務擴展),在回調方法中處理 推送內容時設置 request.content.attachments(請求內容的附件)屬性,之后調用 contentHandler 方法即可。
UNNotificationServiceExtension 提供在遠程推送將要被 push 出來前,處理推送顯示內容的機會。此時可以對通知的 request.content
進行內容添加,如添加附件、userInfo 等。下圖顯示了Notification Service Extension 的流程:
處理的細節如下:
為了能在 service extension 里面的
attachment
,必須給apns
增加"mutable-content":1
字段,使你的推送通知是動態可變的。{ "aps":{
"alert":"Testing.. (34)",
"badge":1,
"sound":"default",
"mutable-content":1
}
}給項目新建一個 Notification Service Extension 的擴展。自動生成下列文件。
在 -didReceiveNotificationRequest:withContentHandler: 方法中處理request.content,用來給通知的內容做修改。如下面代碼示例了收到通知后,給通知增加圖片附件:
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title];
//1. 下載
NSURL *url = [NSURL URLWithString:@"http://img1.gtimg.com/sports/pics/hv1/194/44/2136/138904814.jpg"];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (!error) {
//2. 保存數據
NSString *path = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject
stringByAppendingPathComponent:@"download/image.jpg"];
UIImage *image = [UIImage imageWithData:data];
NSError *err = nil;
[UIImageJPEGRepresentation(image, 1) writeToFile:path options:NSAtomicWrite error:&err];
//3. 添加附件
UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"remote-atta1" URL:[NSURL fileURLWithPath:path] options:nil error:&err];
if (attachment) {
self.bestAttemptContent.attachments = @[attachment];
}
}
//4. 返回新的通知內容
self.contentHandler(self.bestAttemptContent);
}];
[task resume];
}
使用 UNNotificationServiceExtension,你有 30 秒的時間處理這個通知,可以同步下載圖像和視頻到本地,然后包裝為一個 UNNotificationAttachment 扔給通知,這樣就能展示用服務器獲取的圖像或者視頻了。
注意:如果數據處理失敗、超時,extension 會報一個崩潰信息,但是通知會用默認的形式展示出來,app不會崩潰。
附件通知所帶的附件格式大小都是有限的,並不能做所有事情,視頻的前幾幀作為一個通知的附件是個不錯的選擇。
UNNotificationAttachment:attachment 支持
- 音頻 5M(kUTTypeWaveformAudio/kUTTypeMP3/kUTTypeMPEG4Audio/kUTTypeAudioInterchangeFileFormat)
- 圖片10M(kUTTypeJPEG/kUTTypeGIF/kUTTypePNG)
- 視頻50M(kUTTypeMPEG/kUTTypeMPEG2Video/kUTTypeMPEG4/kUTTypeAVIMovie)
5.2 UNNotificationContentExtension - 通知內容擴展
要想創建一個自定義的用戶界面,需要用到 Notification Content Extension(通知內容擴展)。
Notification Content Extension(通知內容擴展)允許開發者加入自定義的界面,在這個界面里面,你可以繪制任何你想要的東西。但是有一個最重要的限制就是,這個自定義的界面沒有交互。它們不能接受點擊事件,用戶並不能點擊它們。但是推送通知還是可以繼續與用戶進行交互,因為用戶可以使用 notificaiton 的 actions。
注意:extension 也可以處理這些 actions。
推送界面的組成
- header 的 UI 是系統提供的一套標准的 UI。這套 UI 會提供給所有的推送通知。
- header 下面的 custom content 是自定義的內容,就是 Notification Content Extension。在這里,就可以顯示任何你想繪制的內容了。你可以展示任何額外的有用的信息給用戶。
- default content 是系統的界面。這也就是 iOS 9 之前的推送的樣子。
- notification action 用戶可以觸發一些操作。並且這些操作還會相應的反映到上面的自定義的推送界面 content extension 中。
創建 Notification Content Extension
創建一個新的 Notification Content 的 target。Xcode 自動生成一個新的模板以及下列文件。
然后打開這里的 ViewController。
#import "NotificationViewController.h" #import <UserNotifications/UserNotifications.h>
#import <UserNotificationsUI/UserNotificationsUI.h>
@interface NotificationViewController () <UNNotificationContentExtension>
@property IBOutlet UILabel *label;
@end
@implementation NotificationViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any required interface initialization here.
}
- (void)didReceiveNotification:(UNNotification *)notification {
self.label.text = notification.request.content.body;
}
@end發現這里的 ViewController 就是一個普通的 UIViewController, 但是它實現了 UNNotificationContentExtension 協議。
UNNotificationContentExtension 協議有一個 required方法 didReceiveNotification:。當收到指定 categroy 的推送時,didReceiveNotification: 方法會隨着 ViewController 的生命周期方法,一起被調用,這樣就能接受 notification object,更新UI。
5.3 配置category
接下來就是要讓推送到達后,系統怎樣找到自定義的 UI。這時候就需要配置 extension 的 info.plist 文件。
這里和我們給 notification actions 注冊 category 一樣,給這個通知擴展指定相應的 category。在 UNNotificationExtensionCategory 字段里寫入相應的 category id。值得提到的一點是,這里對應的 category 是可以為一個數組的,里面可以為多個 category,這樣做的目的是多個 category 共用同一套 UI。
上圖中 category id 為 myNotificationCategory1 和 myNotificationCategory2 的通知就共用了一套 UI。
設置了 category 后,只要在通知里面增加 category 字段,值是上面在 extension 的 plist 里面配置的 category id,收到的通知就會通過自定義的樣式顯示。
遠程通知在 apns 里面增加 category 字段。
{
"aps":{
"alert":"Testing.. (34)",
"badge":1,
"sound":"default",
"category":"myNotificationCategory1"
}
}
5.4 自定義UI
然后開始寫自定義UI。
- (void)didReceiveNotification:(UNNotification *)notification
{
self.label.text = [NSString stringWithFormat:@"%@ [modified]", notification.request.content.title];
self.subLabel.text = [NSString stringWithFormat:@"%@ [modified]", notification.request.content.body];
self.imageView.image = [UIImage imageNamed:@"hong.png"];
}
可以在 ViewController 中增加一些 Label 和 ImageView,收到通知的時候,提取想要的內容,或者添加額外的內容,設置到我們自定義的 View 上。
5.5 優化
發現是自定義界面的大小很不美觀
這時候可以通過設置 ViewController 的 preferredContentSize大小,控制自定義視圖的大小。也可以通過約束,控制自定義視圖的大小。
- (void)viewDidLoad {
[super viewDidLoad];
self.preferredContentSize = CGSizeMake(CGRectGetWidth(self.view.frame), 100);
}視圖恢復成正確的尺寸前,先展示有一大片空白的樣子,然后變成正確的樣子。當通知展示出來之后,它的大小並不是正常的我們想要的尺寸。iOS 系統會去做一個動畫來 Resize 它的大小。這樣體驗很差。
會出現上面這張圖的原因是,在推送送達的那一刻,iOS 系統需要知道我們推送界面的最終大小。但是我們自定義的extension在系統打算展示推送通知的那一刻,並還沒有啟動。所以這個時候,在我們代碼都還沒有跑起來之前,我們需要告訴iOS系統,我們的View最終要展示的大小。
為了解決這個問題,我們需要在 extension 的 info.plist 里設置一個 content size ratio。增加字段 UNNotificationExtensionInitialContentSizeRatio。
這個屬性定義了寬和高的比例。當然設置了這個比例以后,也並不是萬能的。因為你並不知道你會接受到多長的content。當你僅僅只設置比例,還是不能完整的展示所有的內容。有些時候如果我們可以知道最終的尺寸,那么我們固定尺寸會更好。
這時候我們發現我們自定義的界面顯示的內容(custom content)和系統默認的內容(default content)重復了。
可以在 extension 的 info.plist 里設置,把系統默認的樣式隱藏。增加字段UNNotificationExtensionDefaultContentHidden。
將系統內容隱藏后效果如下:
5.6 自定義操作
iOS8 開始引入的 action 的工作原理:
默認系統的 Action 的處理是:當用戶點擊的按鈕,就把 action 傳遞給 app,與此同時,推送通知會立即消失。這種做法很方便。
但是有的情況是,希望用戶點擊 action 按鈕后,效果及時響應在我們自定義的 UI 上。這個時候,用戶點擊完按鈕,我們把這個 action 直接傳遞給 extension,而不是傳遞給 app。當 actions 傳遞給 extension 時,它可以延遲推送通知的消失時間。在這段延遲的時間之內,我們就可以處理用戶點擊按鈕的事件了,並且更新 UI,一切都處理完成之后,我們再去讓推送通知消失掉。
這里我們可以運用 UNNotificationContentExtension 協議的第二個方法,這方法是 Optional
- (void)didReceiveNotificationResponse:(UNNotificationResponse *)response completionHandler:(void (^)(UNNotificationContentExtensionResponseOption option))completion
{
if ([response.actionIdentifier isEqualToString:@"action-like"]) {
self.label.text = @"點贊成功~";
}
else if ([response.actionIdentifier isEqualToString:@"action-collect"]){
self.label.text = @"收藏成功~";
}
else if ([response.actionIdentifier isEqualToString:@"action-comment"]){
self.label.text = [(UNTextInputNotificationResponse *)response userText];
}
//這里如果點擊的action類型為UNNotificationActionOptionForeground,
//則即使completion設置成Dismiss的,通知也不能消失
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
completion(UNNotificationContentExtensionResponseOptionDismiss);
});
}
在這個方法里判斷所有的 action,更新界面,並延遲 1.5 秒后讓通知消失。真實情況可能是,點擊“贊”按鈕后,發送請求給服務器,根據服務器返回結果,展示不同的UI效果在通知界面上,然后消失。如果是評論,則將評論內容更新到界面上。
如果還想把這個 action 傳遞給 app,最后消失的參數應該這樣:
completion(UNNotificationContentExtensionResponseOptionDismissAndForwardAction);
但是我實際運行遇見這種情況,如果點擊的 action 類型為 UNNotificationActionOptionForeground,則即使 completion 設置成 Dismiss 的,通知也不能消失,也沒有啟動 app。
5.7 自定義輸入型操作
action 有 2 種類型:
- UNNotificationAction 普通按鈕樣式
- UNTextInputNotificationAction 輸入框樣式
UNTextInputNotificationAction 的樣式如下:
系統的輸入樣式的 action,只有在點擊發送按鈕時,才能接受到 action 的響應回調。(比如上面的didReceiveNotificationResponse:completionHandler: 方法)。但有的時候系統的樣式或者功能不能滿足需求,這時候可以自定義鍵盤上面的 inputAccessoryView。
首先,重寫ViewController的下面兩個方法:
- (BOOL)canBecomeFirstResponder
{
return YES;
}
- (UIView *)inputAccessoryView
{
return self.customInputView;
}
自定義 inputAccessoryView,以繪制自定義的輸入樣式。
- (void)didReceiveNotificationResponse:(UNNotificationResponse *)response completionHandler:(void (^)(UNNotificationContentExtensionResponseOption option))completion
{
...
}
else if ([response.actionIdentifier isEqualToString:@"action-comment"]){
self.label.text = [(UNTextInputNotificationResponse *)response userText];
[self becomeFirstResponder];
[self.textField becomeFirstResponder];
self.completion = completion;
}
}
實現了點擊評論按鈕,ViewController 成為第一響應者,使自定義的輸入樣式顯示出來。然后,讓textField成為第一響應者,使鍵盤彈出。
這里將操作的completion保存,以便在需要的時候調用。比如,可以在點擊鍵盤右下的send按鈕時,調用completion,使通知消失。
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[textField resignFirstResponder];
self.label.text = textField.text;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.completion(UNNotificationContentExtensionResponseOptionDismiss);
});
return YES;
}
實現效果如下:
5.8 結合使用兩個擴展
可以在 content extension 里面繪制界面時,通過 notification.request.content.attachments 獲取附件放到自定義控件里面。
- (void)didReceiveNotification:(UNNotification *)notification {
...
UNNotificationAttachment * attachment = notification.request.content.attachments.firstObject;
if (attachment) {
if ([attachment.URL startAccessingSecurityScopedResource]) {
self.imageView.image = [UIImage imageWithContentsOfFile:attachment.URL.path];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[attachment.URL stopAccessingSecurityScopedResource];
});
}
}
}
我們可以提取 content 的 attachments。前文提到過,attachment 是由系統管理的,系統會把它們單獨的管理,這意味着它們存儲在我們 sandbox 之外。所以這里我們要使用 attachment 之前,我們需要告訴 iOS 系統,我們需要使用它,並且在使用完畢之后告訴系統我們使用完畢了。對應上述代碼就是 -startAccessingSecurityScopedResource和-stopAccessingSecurityScopedResource
的操作。當我們獲取到了 attachment 的使用權之后,我們就可以使用那個文件獲取我們想要的信息了。
5.9 關於調試
很多人在開發 iOS extension 時遇到了調試的問題,可以看這里的解決方法,如果還不能有效解決您的問題,歡迎評論留言。
Demo
【WWDC2016 Session】iOS 10 推送Notification新特性
iOS- 實現APP前台、后台、甚至殺死進程下收到通知后進行語音播報(金額)。
文章
官方文檔:Local and Remote Notification Programming Guide
iOS中使用本地通知為你的APP添加提示用戶功能
iOS遠程推送之(一):APNs原理和基本配置
iOS遠程推送之(二):角標applicationIconNumber設置
iOS遠程推送之(三):點擊通知橫幅啟動應用
iOS 遠程消息推送 APNS推送原理和一步一步開發詳解篇
SmartPush
iOS遠程推送原理及實現過程
iOS 推送通知及通知擴展
iOS10 推送通知 UserNotifications
iOS 推送全解析,你不可不知的所有 Tips!
iOS推送之遠程推送(iOS Notification Of Remote Notification)