iOS 推送(蘋果原生態)

Hsusue發表於2018-09-08

前言

推送對App的重要性不言而喻,是每一個iOS開發者必修的技能。網上的資料對於初學者並不友好(至少對於我來說),其中有許多坑。並且由於要配置證書,只能真機除錯等,學起來更是難上加難。這篇文章是從剛開始接觸推送的起點寫起。最近有點忙,有些地方沒有細究,只是暫時知道了能實現什麼,並且貼出了一些文章的連結,方便進一步研究,如果有錯誤的地方請指出來。

image

這篇先探究蘋果原生態推送,下篇再寫極光推送和IMiOS —— 極光推送和極光IM

目錄

  1. 蘋果原生態推送

  2. 推送知識點及疑惑的地方

  3. 推送訊息呼叫方法的時機,以及系統能做到的更多騷擴充套件。

蘋果原生態推送

  • iOS推送分為本地推送和遠端推送。

    它們都需要使用者授權。本地不需要聯網和證書,遠端需要聯網和證書。

    本地推送比如鬧鐘,可以設定推送時間,是否重複推送,通知內容等。

    遠端推送,就多坑了。本文主要探討遠端推送。

遠端推送整體流程

image


實現步驟

  1. 在專案 target 中,開啟Capabilitie -> Push Notifications,並會自動在專案中生成 .entitlement 檔案。開啟Capablitie -> Background Modes -> Remote notifications。

  2. iOS8.0以上在AppDelegate.m中

#ifdef NSFoundationVersionNumber_iOS_9_x_Max
#import <UserNotifications/UserNotifications.h>
#endif
複製程式碼

並且遵循協議UNUserNotificationCenterDelegate(該協議是iOS10.0+用,後面再提到)。

  1. 向APNs伺服器註冊deviceToken
  • 作用:蘋果APNs伺服器找到對應的裝置和App。

  • 原理:通過裝置UDID和App的Bundle Identifier生成deviceToken。這樣APNs伺服器接受到資訊就知道轉發給哪個裝置的哪個App。

  • 做法:

在程式首次呼叫以下方法時,會請求推送許可權。

首先使用者要允許App傳送通知。然後App發起註冊,最終註冊成功回撥方法獲得deviceToken。這個過程通常是在程式剛啟動的時候。

注意:發起註冊的方法在iOS8.0後更新了一次,10.0後又更新了一次。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [self replyPushNotificationAuthorization:application];
    return YES;
}


- (void)replyPushNotificationAuthorization:(UIApplication *)application{
    if (IOS10_OR_LATER) {
        //iOS 10 later
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        //必須寫代理,不然無法監聽通知的接收與點選事件
        center.delegate = self;
        [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error) {
            if (!error && granted) {
                //使用者點選允許
                NSLog(@"註冊成功");
            }else{
                //使用者點選不允許
                NSLog(@"註冊失敗");
            }
        }];
 
        // 可以通過 getNotificationSettingsWithCompletionHandler 獲取許可權設定
        //之前註冊推送服務,使用者點選了同意還是不同意,以及使用者之後又做了怎樣的更改我們都無從得知,現在 apple 開放了這個 API,我們可以直接獲取到使用者的設定資訊了。注意UNNotificationSettings是隻讀物件哦,不能直接修改!
        [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
            NSLog(@"========%@",settings);
//列印結果 ========<UNNotificationSettings: 0x1740887f0; authorizationStatus: Authorized, notificationCenterSetting: Enabled, soundSetting: Enabled, badgeSetting: Enabled, lockScreenSetting: Enabled, alertSetting: NotSupported, carPlaySetting: Enabled, alertStyle: Banner>        
        }];
    }else if (IOS8_OR_LATER){
        //iOS 8 - iOS 10系統
        UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound categories:nil];
        [application registerUserNotificationSettings:settings];
    }else{
        //iOS 8.0系統以下
        [application registerForRemoteNotificationTypes:UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeSound];
    }
 
    //註冊遠端訊息通知獲取device token
    [application registerForRemoteNotifications];

}
    
複製程式碼

對以上註冊方法說幾句

  • iOS8.0和iOS10.0方法都有個categories,這裡為了方便寫為nil。其作用是針對推送做擴充功能。這裡給出圖,讓讀者暫時知道有什麼用,還能自定義這個介面,文章後面會再提到。

iOS8的方法,能直接輸入。

image

iOS10的方法,能增加按鈕事件。

image
iOS10的方法,自定義介面。
image

  • iOS8.0以上的裝置不能用registerForRemoteNotificationTypes:,否則無論如何也不能註冊成功。
  • 引數列舉,表示收到推送後要如何顯示。
None:不顯示任何東西。
Alert:彈出提示框。
Badge:App小紅點。
Sound:發出聲音或震動。
複製程式碼

實測極光推送,Alert< Badge < Sound。即只設定了Badge還是會預設帶上Alert,設定了Sound會預設帶上Badge和Sound。不過看介面是按位或操作,還是乖乖寫JPAuthorizationOptionAlert|JPAuthorizationOptionBadge|JPAuthorizationOptionSound

image


註冊結果回撥

#pragma  mark - 獲取device Token
//獲取DeviceToken成功
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{
 
    //解析NSData獲取字串
    //我看網上這部分直接使用下面方法轉換為string,你會得到一個nil(別怪我不告訴你哦)
    //錯誤寫法
    //NSString *string = [[NSString alloc] initWithData:deviceToken encoding:NSUTF8StringEncoding];
 
 
    //正確寫法
    NSString *deviceString = [[deviceToken description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];
    deviceString = [deviceString stringByReplacingOccurrencesOfString:@" " withString:@""];
 
    NSLog(@"deviceToken===========%@",deviceString);
}
 
//獲取DeviceToken失敗
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{
    NSLog(@"[DeviceToken Error]:%@\n",error.description);
}

複製程式碼

獲取deviceToken存在本地,等使用者登入後,和賬號id一起傳送到伺服器繫結起來。如整體流程圖那樣。

  1. 收到推送後回撥方法

iOS10.0之前

  • 處理本地推送
// 處理本地推送
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
    
}
複製程式碼
  • 處理遠端訊息

呼叫時機:App處於前臺收到推送;在iOS7後,開啟了 Remote Notification,App處於後臺收到推送。

// 處理遠端訊息
// 方法二是在iOS 7之後新增的方法,可以說是 方法一 的升級版本,如果app最低支援iOS 7的話可以不用新增 方法一了。
//- (void)application:(UIApplication *)application //didReceiveRemoteNotification:(NSDictionary *)userInfo
//{
//    NSLog(@"userinfo:%@",userInfo);
//    NSLog(@"收到推送訊息:%@",[[userInfo objectForKey:@"aps"] objectForKey:@"alert"]);
//}

// 其中completionHandler這個block可以填寫的引數UIBackgroundFetchResult是一個列舉值。主要是用來在後臺狀態下進行一些操作的,比如請求資料,操作完成之後,必須通知系統獲取完成,可供選擇的結果有
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
    NSLog(@"userinfo:%@",userInfo);
    NSLog(@"收到推送訊息:%@",[[userInfo objectForKey:@"aps"] objectForKey:@"alert"]);
    
    completionHandler(UIBackgroundFetchResultNewData);
//    typedef NS_ENUM(NSUInteger, UIBackgroundFetchResult) {
//        UIBackgroundFetchResultNewData,
//        UIBackgroundFetchResultNoData,
//        UIBackgroundFetchResultFailed
//    }

}

複製程式碼

這裡解釋一下userInfo,詳細點進連結看。

{
  "aps" : {
    "alert" : {
      "title" : "iOS遠端訊息,我是主標題!-title",
      "subtitle" : "iOS遠端訊息,我是主標題!-Subtitle",
      "body" : "Dely,why am i so handsome -body"
    },
    "badge" : "2"
  }
}

作者:Dely
連結:https://www.jianshu.com/p/c58f8322a278
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。
複製程式碼

image


這裡補充一點東西。

遠端推送通知,分為 普通推送/後臺推送/靜默推送 3 種型別。

推送通知-後臺通知/靜默通知文章裡,有深入介紹。

簡單的說,userInfo中的aps中可以設定一個鍵值對"content-available" : 1,其代表後臺推送。在後臺推送基礎上不設定badge、sound、aleert的靜默推送可以靜悄悄讓App更新資料。

這三種型別對應呼叫的方法會有所不同,文末會說。


iOS10.0之後,新增兩個方法。原來的方法還是需要實現的,各自的呼叫時機不一樣。

  • 本地推送和遠端推送合在同一個方法裡。
// App處於前臺接收到通知
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
    if([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
        NSLog(@"iOS10 收到遠端通知");
    }else {
        // 判斷為本地通知
        NSLog(@"iOS10 收到本地通知");
    }
    
    // 在前臺預設不顯示推送,如果要顯示,就要設定以下內容
    // 微信設定裡-新訊息通知-微信開啟時-聲音or振動  就是該原理
    // 需要執行這個方法,選擇是否提醒使用者,有Badge、Sound、Alert三種型別可以設定
    completionHandler(UNNotificationPresentationOptionBadge|
                      UNNotificationPresentationOptionSound|
                      UNNotificationPresentationOptionAlert);

}
複製程式碼

個人覺得這裡iOS 10的方法didRecieve有歧義,實際是點選推送才呼叫。

// 點選通知後會呼叫
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler {
    completionHandler(UIBackgroundFetchResultNewData);
    NSDictionary *userInfo = response.notification.request.content.userInfo;
    //程式關閉狀態點選推送訊息開啟 可以在App啟動方法的launchOptions獲知
    if (self.isLaunchedByNotification) {
        //TODO
    }
    else{
        //前臺執行
        if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
            //TODO
        }
        //後臺掛起時
        else{
            //TODO
        }
        //收到推送訊息手機震動,播放音效
        AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
        AudioServicesPlaySystemSound(1007);
    }
    //設定應用程式角標數為1
    [UIApplication sharedApplication].applicationIconBadgeNumber = 1;
    
    // 此處必須要執行下行程式碼,不然會報錯
    completionHandler();
}
複製程式碼

這些方法還有坑,筆者放到本文最後的呼叫時機裡再說。


到這裡已經把整個流程大致描繪出來了。

這裡補充一些知識點,和提出一些疑惑的地方。

補充的知識點

補充點小知識點,應該能更好地理解原理。

  • APNs與裝置保持長連線。(APNs伺服器真猛,資訊量這麼大)

  • 基於上一點,不難理解,推送本身是 iOS 系統的行為。所以在 App 沒有執行(沒有在前臺也沒有在後臺)的時候:

1⃣️仍然能夠推送及接收(通知中心通知、頂部橫幅、重新整理 App 右上角的小圓點即 badge [以下簡稱角標] 等都會由系統來控制和展示)。

2⃣️(收到推送時,是無法在 App 的程式碼中獲取到通知內容的。因為沙盒機制,此時 App 的任何程式碼都不可能被執行。)這個觀點是在另一篇文章看到的,但筆者實測發現並不是這麼回事。處於後臺收到推送是會執行程式碼的。

所以就算App關掉後臺應用重新整理,照樣能接收到推送。

以微信為例,如果你開啟了後臺應用重新整理,那麼當你收到新訊息提醒之後,你開啟微信,未讀資訊已經在那了(如果網速沒問題的話);而當你收到新訊息提示,開啟微信後,訊息才剛剛開始收取。

  • iOS的推送,內容的大小最大是4KB。 A傳送很長的文字給B,是發到伺服器,由伺服器暫存資料,通知B來取。B收到推送後去伺服器下載。(圖片、語音等類似)

  • iOS10以後,收到推送能在後臺下載附件,限制:圖片是10M Video是50M。 有興趣的可以去研究下,www.jianshu.com/p/f5337e8f3…

  • APNs伺服器會向App伺服器返回一個傳送結果。(雖然這是後臺的事,還是稍微瞭解下)

疑惑的地方

  • 怎麼針對特定的人群發出推送。例如群聊。

    是把群id發到伺服器,然後伺服器查出成員各自的id對應的deviceToken發出推送嗎?極光有個根據Tag推送,以後瞭解到會更新文章。

  • 如果該賬號並不線上,那這條訊息要怎麼處理?

    把訊息暫時存在後臺伺服器,等待有裝置登入這個賬號後繫結userId和deviceToken後,發起推送。


點選推送預設只是開啟App。但也能增加一些操作,例如跳轉到某個介面、微信直接回復等。

所以最後深入瞭解推送還能怎麼玩,以及解釋App在前臺、掛起、關閉狀態下對應的方法。

推送訊息的方法呼叫時機以及特殊處理

  • App在各種狀態下的呼叫方法

1.如果App被kill掉的情況下,點選了推送,那麼首先要啟動App,會呼叫- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions。 引數launchOptions能分辨出是通過點選App icon還是點選推送開啟App。

在iOS7.0之前,處於前臺收到推送,呼叫(void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo

在iOS7.0~10.0-,處於前臺收到推送,呼叫- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler

坑了筆者的是,在iOS7.0+(包括10.0後),收到後臺推送或靜默推送,呼叫- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler

3.iOS10.0+就功能就豐(皮)富了。

在前臺時收到推送,呼叫- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler

所以,App處於前臺時收到後臺推送或靜默推送的話,在iOS10+會呼叫兩個方法,注意不要重複處理。- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler。如果你還點選了推送,那麼還會呼叫下面這個方法。。。

點選推送,呼叫- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler

在iOS10後,針對這三個方法,為了不重複處理同一條推送的內容。筆者建議儘量同一模組的推送,在這各個方法中不要相同邏輯處理。

例如QQ,如果採用後臺或靜默推送,就只在靜默中處理;

不是靜默推送。在前臺收到,就紅點顯示,通知欄不要顯示。如果不在前臺,通知欄顯示,點選後可跳轉到對應聊天框。

  • 之前上面提過的category對推送做擴充。因為專案暫時沒有這個需求,而求最近有點忙,就先不鑽研這部分了。找到了一篇好文章,介紹了iOS10的推送按鈕操作。

  • iOS10.0後能對推送進行查詢和刪除 利用場景,例如微信撤回後,原本的推送將被改為"使用者"撤回了一條資訊。

// Notification requests that are waiting for their trigger to fire
//獲取未送達的所有訊息列表
- (void)getPendingNotificationRequestsWithCompletionHandler:(void(^)(NSArray<UNNotificationRequest *> *requests))completionHandler;
//刪除所有未送達的特定id的訊息
- (void)removePendingNotificationRequestsWithIdentifiers:(NSArray<NSString *> *)identifiers;
//刪除所有未送達的訊息
- (void)removeAllPendingNotificationRequests;

// Notifications that have been delivered and remain in Notification Center. Notifiations triggered by location cannot be retrieved, but can be removed.
//獲取已送達的所有訊息列表
- (void)getDeliveredNotificationsWithCompletionHandler:(void(^)(NSArray<UNNotification *> *notifications))completionHandler __TVOS_PROHIBITED;
//刪除所有已送達的特定id的訊息
- (void)removeDeliveredNotificationsWithIdentifiers:(NSArray<NSString *> *)identifiers __TVOS_PROHIBITED;
//刪除所有已送達的訊息
- (void)removeAllDeliveredNotifications __TVOS_PROHIBITED;

作者:Dely
連結:https://www.jianshu.com/p/c58f8322a278
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。
複製程式碼

參考

相關文章