iOS 推送


一、推送原理

當用戶打開應用程序的通知中心之后,蘋果遠程推送服務器就能把消息推送到裝有該應用的設備上,具有強制性、實時性的特點,並且用戶無需打開應用都能收到推送的消息。

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 推送原理



從圖中可以很清楚的看出來推送的原理主要分為以下幾步:


  1. 由 App 向 iOS 設備發送一個注冊通知,用戶需要同意系統發送推送;
  2. iOS 向 APNs 遠程推送服務器發送 App 的 Bundle Id 和設備的 UDID;
  3. APNs 根據設備的 UDID 和 App 的 Bundle Id 生成 deviceToken 再發回給 App;
  4. App 再將 deviceToken 發送給遠程推送服務器(自己的服務器), 由服務器保存在數據庫中。


  5. 當自己的服務器想發送推送時,在遠程推送服務器中輸入要發送的消息並選擇發給哪些用戶的deviceToken,由遠程推送服務器發送給 APNs。

  6. APNs 根據 deviceToken 發送給對應的用戶。

詳細流程:

  1. 在今日頭條 App 的 AppDelegate 的 didFinishLaunchingWithOptions 方法中注冊遠程推送通知,此時只要 iOS 設備正常聯網能夠訪問到外網,iOS 設備默認就會和 APNs 服務器維持一個基於 TCP 的長連接,就會把 iOS 設備的 UDID(Unique Device Identifier:唯一設備標識碼,用來標識唯一一台蘋果設備)和 App 的 Bundle Identifier 通過長連接發送給 APNs 服務器,然后蘋果通過這兩個的值根據一定的加密算法得出 deviceToken,並將 deviceToken 返回給 iOS 設備。(注:APNs服務器會留有 UDID+Bundle Identifier+deviceToken 的映射表)

  2. 實現 UIApplicationDelegate 代理中的有關於注冊遠程通知的相關方法,包括注冊成功、注冊失敗、對接收到通知的處理等。

  3. 如果注冊成功,實現注冊成功的代理方法,就能夠接收到 deviceToken,並將 deviceToken 發送給 App 服務器,App 服務器將此 deviceToken 存儲在數據庫中(一般如果是及時通訊類應用那么還會與用戶的賬號進行映射)。

  4. 如果注冊失敗,那么實現注冊失敗的協議方法,處理失敗后的事情。

  5. app 服務器接收到 deviceToken 之后,就可以根據這些 deviceToken 向 APNs 發送推送消息。

  6. APNs 接收到 deviceToken 和消息之后,根據 deviceToken 查找映射表找到對應的 UDID 和 Bundle Identifier,根據 UDID 找到唯一一台蘋果設備,再在找到的蘋果設備上根據 Bundle Identifier 找到唯一的應用,然后推送消息。

  7. 當設備接收到消息的時候,如果 App 在前台,那么不會在設備上方彈出橫幅(如果使用了音效,還會觸發音效的播放),直接調用我們實現的 UIApplicationDelegate 中的接收消息的方法;如果 App 在后台或者未運行時就會在設備的上方彈出橫幅(如果使用了音效,還會觸發音效的播放),點擊橫幅才會觸發調用我們實現的 UIApplicationDelegate 中的接收消息的方法,這個時候你直接點擊應用圖標進來是不會調用的。

二、信息包

信息包結構圖:


上圖顯示的這個消息體就是我們的應用服務器(Provider)發送給 APNs 服務器的消息結構,APNs 驗證這個結構正確並提取其中的信息后,再將消息推送到指定的 iOS 設備。

這個結構體包括五個部分

  1. 第一部分是命令標示符
  2. 第二部分是 devicetoken 的長度
  3. 第三部分是 devicetoken 字符串
  4. 第四部分是推送消息體(Payload)的長度
  5. 最后一部分也就是真正的消息內容了,里面包含了推送消息的基本信息,比如消息內容,應用 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)


  1. UNNotificationContentExtension(通知內容擴展)給通知創建一個自定義的用戶界面;
  2. UNNotificationServiceExtension(通知服務擴展)是在收到通知后,展示通知前,做一些事情的。比如:增加附件,網絡請求等。

5.1 UNNotificationServiceExtension - 通知服務擴展

如果經常使用 iMessage 的朋友們,就會經常收到一些信息,附帶了一些照片或者視頻,所以推送中能附帶這些多媒體是非常重要的。如果推送中包含了這些多媒體信息,可以使用戶不用打開 app,不用下載就可以快速瀏覽到內容。眾所周知,推送通知中帶了 push payload,即使去年蘋果已經把 payload 的 size 提升到了 4k bites,但是這么小的容量也無法使用戶能發送一張高清的圖片,甚至把這張圖的縮略圖包含在推送通知里面,也不一定放的下去。在 iOS X 中,我們可以使用新特性來解決這個問題。我們可以通過新的 service extensions 來解決這個問題。

iOS10 給通知添加附件有兩種情況:本地通知和遠程通知。

  1. 本地推送通知

    只需給 content.attachments 設置 UNNotificationAttachment 附件對象

  2. 遠程推送通知

    需要實現 UNNotificationServiceExtension(通知服務擴展),在回調方法中處理 推送內容時設置 request.content.attachments(請求內容的附件)屬性,之后調用 contentHandler 方法即可。

UNNotificationServiceExtension 提供在遠程推送將要被 push 出來前,處理推送顯示內容的機會。此時可以對通知的 request.content 進行內容添加,如添加附件、userInfo 等。下圖顯示了Notification Service Extension 的流程:


處理的細節如下:

  1. 為了能在 service extension 里面的 attachment,必須給 apns 增加 "mutable-content":1 字段,使你的推送通知是動態可變的。

    {
         "aps":{
    "alert":"Testing.. (34)",
    "badge":1,
    "sound":"default",
    "mutable-content":1
    }
    }
  2. 給項目新建一個 Notification Service Extension 的擴展。自動生成下列文件。


  3. 在 -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 支持

  1. 音頻 5M(kUTTypeWaveformAudio/kUTTypeMP3/kUTTypeMPEG4Audio/kUTTypeAudioInterchangeFileFormat)
  2. 圖片10M(kUTTypeJPEG/kUTTypeGIF/kUTTypePNG)
  3. 視頻50M(kUTTypeMPEG/kUTTypeMPEG2Video/kUTTypeMPEG4/kUTTypeAVIMovie)

5.2 UNNotificationContentExtension - 通知內容擴展

要想創建一個自定義的用戶界面,需要用到 Notification Content Extension(通知內容擴展)。

Notification Content Extension(通知內容擴展)允許開發者加入自定義的界面,在這個界面里面,你可以繪制任何你想要的東西。但是有一個最重要的限制就是,這個自定義的界面沒有交互。它們不能接受點擊事件,用戶並不能點擊它們。但是推送通知還是可以繼續與用戶進行交互,因為用戶可以使用 notificaiton 的 actions。

注意:extension 也可以處理這些 actions。

  1. 推送界面的組成


    • header 的 UI 是系統提供的一套標准的 UI。這套 UI 會提供給所有的推送通知。
    • header 下面的 custom content 是自定義的內容,就是 Notification Content Extension。在這里,就可以顯示任何你想繪制的內容了。你可以展示任何額外的有用的信息給用戶。
    • default content 是系統的界面。這也就是 iOS 9 之前的推送的樣子。
    • notification action 用戶可以觸發一些操作。並且這些操作還會相應的反映到上面的自定義的推送界面 content extension 中。
  2. 創建 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 優化

  1. 發現是自定義界面的大小很不美觀

    這時候可以通過設置 ViewController 的 preferredContentSize大小,控制自定義視圖的大小。也可以通過約束,控制自定義視圖的大小。

    - (void)viewDidLoad 
    {
    [super viewDidLoad];
    self.preferredContentSize = CGSizeMake(CGRectGetWidth(self.view.frame), 100);
    }
  2. 視圖恢復成正確的尺寸前,先展示有一大片空白的樣子,然后變成正確的樣子。當通知展示出來之后,它的大小並不是正常的我們想要的尺寸。iOS 系統會去做一個動畫來 Resize 它的大小。這樣體驗很差。


    會出現上面這張圖的原因是,在推送送達的那一刻,iOS 系統需要知道我們推送界面的最終大小。但是我們自定義的extension在系統打算展示推送通知的那一刻,並還沒有啟動。所以這個時候,在我們代碼都還沒有跑起來之前,我們需要告訴iOS系統,我們的View最終要展示的大小。

    為了解決這個問題,我們需要在 extension 的 info.plist 里設置一個 content size ratio。增加字段 UNNotificationExtensionInitialContentSizeRatio。


    這個屬性定義了寬和高的比例。當然設置了這個比例以后,也並不是萬能的。因為你並不知道你會接受到多長的content。當你僅僅只設置比例,還是不能完整的展示所有的內容。有些時候如果我們可以知道最終的尺寸,那么我們固定尺寸會更好。

  3. 這時候我們發現我們自定義的界面顯示的內容(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 種類型:

  1. UNNotificationAction 普通按鈕樣式
  2. 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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM