網(wǎng)上介紹iOS Push的文章有很多,但是大部分都總結(jié)得非常零散,加上之前也一直沒(méi)好好總結(jié)過(guò),對(duì)某些地方也不求甚解。于是抽空把蘋(píng)果這套復(fù)雜而有趣的推送機(jī)制總結(jié)了一遍,終有此文!
注意:本文大部分內(nèi)容基于iOS10新增通知框架UserNotifications。
由于行文篇幅較長(zhǎng),建議移步到有目錄版iOS Push的前世今生
全文導(dǎo)圖

對(duì)Push的理解
蘋(píng)果對(duì)Push的所有優(yōu)化最終目的都是為了提升用戶體驗(yàn)。這一點(diǎn)上不得不佩服Apple!
在講Push之前先談?wù)劄槭裁葱枰扑?。?duì)用戶而言大部分就是為了獲得最新的信息,感興趣的資訊;對(duì)app開(kāi)發(fā)者而言大部分是為了通過(guò)推送讓用戶打開(kāi)app增加日活(別噴我),其次是為了向用戶提供更好的資訊;對(duì)蘋(píng)果而言因?yàn)閕OS系統(tǒng)給app在后臺(tái)最多存活的時(shí)間最多三分鐘(后來(lái)新增后臺(tái)模式后增加到十分鐘),為了保證這個(gè)iOS平臺(tái)能夠給用戶良好的體驗(yàn)(對(duì)推送可控),app可以主動(dòng)和用戶溝通。
對(duì)APNS(Remote Push)的理解
可以通俗的把APNS理解為iOS系統(tǒng)為每個(gè)app提供的長(zhǎng)連接通道。只是這個(gè)通道需要通過(guò)蘋(píng)果中轉(zhuǎn),為什么蘋(píng)果要設(shè)計(jì)這么一套服務(wù)呢。上面也提到了過(guò),下面在針對(duì)用戶體驗(yàn)詳細(xì)一點(diǎn)介紹。
- 提升用戶體驗(yàn):蘋(píng)果限制了每個(gè)app在后臺(tái)存活的時(shí)間,最重要的目的是為了省電,其次優(yōu)化內(nèi)存這些。如果徹徹底底的將app殺死了,服務(wù)端永遠(yuǎn)不能主動(dòng)和客戶端建立聯(lián)系。所以需要一種機(jī)制來(lái)保證在必要的時(shí)候讓用戶知道服務(wù)端所做的改變。技術(shù)上只要只有長(zhǎng)連接可以做到。
- 便于蘋(píng)果、用戶控制:如果直接讓app和服務(wù)端建立長(zhǎng)連接(比如iOS8之前的voip,就是app在后臺(tái)保持長(zhǎng)連接),蘋(píng)果是不能控制的。所以通過(guò)在app和服務(wù)端中間加一個(gè)APNS可以有效的進(jìn)行攔截處理。比如可以由用戶開(kāi)啟是否接受遠(yuǎn)程推送。退一萬(wàn)步講,如果哪天蘋(píng)果對(duì)你上架的app進(jìn)行了下架處理。即使有用戶安裝了你的app,切掉你的APNS,用戶也無(wú)法收到推送,除非用戶自己點(diǎn)開(kāi)app否則你的app永遠(yuǎn)不會(huì)存活。
APNS缺點(diǎn)也很明顯
- 可靠性、穩(wěn)定性。一般情況下,Apple會(huì)保證這個(gè)通道的Qaulity of Service,也就是推送的消息能及時(shí)穩(wěn)定到達(dá)設(shè)備。不過(guò)一旦用戶的設(shè)備處于offline狀態(tài),Apple只會(huì)存儲(chǔ)發(fā)送給用戶的最新一條push,之前發(fā)送的push會(huì)被直接丟掉。而且這最后一條離線push也是有過(guò)期時(shí)間的。一些用戶應(yīng)該有過(guò)這種經(jīng)歷,在使用某些的時(shí)候,明明對(duì)方發(fā)送了多條消息,卻只收到了一條push。
- 消息大小限制:由于蘋(píng)果APNS服務(wù)于以萬(wàn)計(jì)的app,所以對(duì)消息內(nèi)容大小有嚴(yán)格限制。只能傳遞一些文本信息。Apple在文檔里清楚的說(shuō)明,push只應(yīng)該用來(lái)通知用戶有新的內(nèi)容,而不應(yīng)該用來(lái)承載內(nèi)容本身。理論上payload size越小,push到達(dá)設(shè)備的概率就越高。蘋(píng)果一直在改善,在iOS8之前max payload size是256字節(jié),到iOS8發(fā)布這個(gè)最大值被調(diào)整到了2048字節(jié),再到的iOS9發(fā)布,引入了HTTP2.0,payload size又被設(shè)為4KB(4 * 1024字節(jié))了。
對(duì)Local Push的理解
有了APNS(Remote Push)為什么蘋(píng)果還搞了一套Local Push呢。從筆者的角度能做出如下猜測(cè):
- 蘋(píng)果開(kāi)發(fā)者中,有很大一部分是個(gè)人開(kāi)發(fā)者。對(duì)于個(gè)人開(kāi)發(fā)者而言,做一款小型的app,很多時(shí)候用APNS需要搭建服務(wù),成本太高。
- APNS前提條件是必須在聯(lián)網(wǎng)的狀態(tài)下,這樣一些不需要聯(lián)網(wǎng)的app,比如日程提醒類,就和Push徹底告別了。但是對(duì)于這種小型app,Local Push非常適合。
- APNS穩(wěn)定性及成功率并不是那么高。大型app可以采用兩種渠道提高通知用戶的成功率。比如后面會(huì)講到的voip Push的使用一般就是結(jié)合local Push使用。
- Local Push比APNS更加靈活。參數(shù)更加多樣。
- ......
如何選擇
因?yàn)閍pp不總是會(huì)運(yùn)行的,Local Push提供了另一種提醒用戶信息的方式。比如應(yīng)用在后臺(tái)沒(méi)有被殺死的時(shí)候,從服務(wù)端拉取數(shù)據(jù)更新,本地就可以根據(jù)服務(wù)端返回的信息,刪選出是否有用戶感興趣的部分,然后組織好消息,通過(guò)本地推送告訴用戶。當(dāng)然也可以用遠(yuǎn)程push。但是這個(gè)時(shí)候遠(yuǎn)程push并沒(méi)有本地push可靠。
Remote Push一般在應(yīng)用殺死的情況下才使用。用戶主動(dòng)去打開(kāi)app,之后請(qǐng)求數(shù)據(jù)。
簡(jiǎn)單總結(jié)一下,如果能夠直接拿到數(shù)據(jù)最好用本地push,如果拿不到,比如說(shuō)應(yīng)用被殺死采用遠(yuǎn)程push。
從用戶角度,這兩種Push沒(méi)有任何區(qū)別,具體來(lái)講我們可以控制Push的如下形式:
- 顯示提示框還是橫幅
- app icon上顯示的數(shù)字
- 顯示橫幅、數(shù)字、提示框所用的提示音
- 特別需要注意的幾點(diǎn):
- App在前臺(tái)運(yùn)行的時(shí)候,通知不會(huì)展示出來(lái)(在iOS10之后可以在userNotificationCenter:willPresentNotification:
withCompletionHandler:進(jìn)行處理,滿足可以在前臺(tái)顯示通知) - 點(diǎn)擊通知,默認(rèn)會(huì)自動(dòng)打開(kāi)推送通知的App
- 不管App是否打開(kāi),通知都可以發(fā)出
- 可以取消本地push
- 可以設(shè)定本地push的自定義處理action(控制點(diǎn)擊Push是否打開(kāi)App)
上面這幾點(diǎn)注意事項(xiàng)可以在在UILocalNotification Class Reference找到(最好的資料還是官方文檔)
本地推送(Local Push)
Local Push是不需要走APNS,讓客戶端更加靈活的控制。常見(jiàn)的比如一款鬧鐘App。app本地處理好推送邏輯,然后把推送邏輯交給系統(tǒng)。即使當(dāng)app不在前臺(tái)的時(shí)候也可以由系統(tǒng)發(fā)出通知告知用戶。
App在啟動(dòng)的時(shí)候就需要對(duì)本地和遠(yuǎn)程推送進(jìn)行設(shè)置,也就是不能晚于application:didFinishLaunchingWithOptions:調(diào)用。官方文檔上說(shuō)其實(shí)也可以,只要在處理只通知之前配置過(guò)就行。
配置推送
使用Push第一步需要向用戶申請(qǐng)權(quán)限
iOS10之后直接用下面這代碼
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert + UNAuthorizationOptionSound)
completionHandler:^(BOOL granted, NSError * _Nullable error) {
// Enable or disable features based on authorization.
}];
注意:因?yàn)橄到y(tǒng)會(huì)保存用戶的授權(quán)狀態(tài),所以下次啟動(dòng)的時(shí)候雖然調(diào)用了這代碼,依然不會(huì)再次提示用戶。
通知類別和通知Action
通知類別:類別定義了app是如何展示通知的。將類別和自定義的action關(guān)聯(lián)起來(lái),并且設(shè)置具體的選項(xiàng)(option)。讓推送更加靈活
Action(UNNotificationAction)具體來(lái)講長(zhǎng)什么樣:一般推送就一個(gè)橫幅,下面沒(méi)有按鈕。Action其實(shí)就是橫幅下面的按鈕。截圖如下

Actionable notifications的界面提供了用戶可以點(diǎn)擊的按鈕,Actionable notifications讓用戶可以快速的執(zhí)行相關(guān)的操作,不用強(qiáng)迫用戶打開(kāi)你的app(通過(guò)option設(shè)置是否打開(kāi)app)。當(dāng)用戶點(diǎn)擊的時(shí)候,會(huì)把相關(guān)的事件立即轉(zhuǎn)發(fā)到app中處理。
在啟動(dòng)的時(shí)候。app可以注冊(cè)一個(gè)或多個(gè)的通知類別,這些類別決定了app會(huì)發(fā)出哪中類型的通知。 通過(guò)設(shè)置UNNotificationCategoryOptions決定。
總的來(lái)講這里有如下幾個(gè)重要的類:
- UNUserNotificationCenter :iOS10之后新增加的通知框架中??梢院?jiǎn)單理解為這就是一個(gè)單例的Service?;舅械耐ㄖO(shè)置都是通過(guò)去設(shè)置的。設(shè)置通知的入口 Manages the notification-related activities for your app or app extension.大致來(lái)講有如下幾個(gè)作用:
- 請(qǐng)求通知權(quán)限
- 聲明app支持的通知類型及自定義action。
- 安排發(fā)出通知
- 管理在通知中心顯示的具體通知。
- 獲取通知相關(guān)的設(shè)置
- UNNotificationCategory :設(shè)置類別的名稱和各個(gè)可選項(xiàng)。類別的名稱就是這個(gè)通知的唯一標(biāo)識(shí),系統(tǒng)會(huì)用來(lái)查找和顯示相關(guān)通知。Defines the types of notifications your app supports and the custom actions displayed for each type.
- UNNotificationAction:定義響應(yīng)通知的具體任務(wù)。比如設(shè)置當(dāng)點(diǎn)擊action的時(shí)候是否打開(kāi)app,處理通知是否在非鎖屏狀態(tài)。Defines a task to perform in response to a delivered notification.
創(chuàng)建及注冊(cè)類別
前面提到過(guò)每個(gè)通知類別可以包含最多四個(gè)自定義action。如果類別包含了自定義的action,系統(tǒng)會(huì)在通知界面上添加按鈕,每個(gè)都會(huì)以設(shè)置的action的title為按鈕的title。如果用戶點(diǎn)擊了其中任何一個(gè)action。系統(tǒng)將會(huì)發(fā)送相關(guān)的action標(biāo)識(shí)給app,app可以使用這個(gè)標(biāo)識(shí)來(lái)標(biāo)識(shí)后面執(zhí)行的任務(wù)。
// Create the custom actions for expired timer notifications.
UNNotificationAction* snoozeAction = [UNNotificationAction
actionWithIdentifier:@"SNOOZE_ACTION"
title:@"Snooze"
options:UNNotificationActionOptionNone];
UNNotificationAction* stopAction = [UNNotificationAction
actionWithIdentifier:@"STOP_ACTION"
title:@"Stop"
options:UNNotificationActionOptionForeground];
// Create the category with the custom actions.
UNNotificationCategory* expiredCategory = [UNNotificationCategory
categoryWithIdentifier:@"TIMER_EXPIRED"
actions:@[snoozeAction, stopAction]
intentIdentifiers:@[]
options:UNNotificationCategoryOptionNone];
// Register the notification categories.
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
[center setNotificationCategories:[NSSet setWithObjects:generalCategory, expiredCategory,
nil]];
有時(shí)候雖然為類別設(shè)置了四個(gè)action,但是系統(tǒng)在某些環(huán)境下只有顯示前兩個(gè)。比如系統(tǒng)會(huì)在以橫幅顯示通知的時(shí)候只會(huì)顯示前兩個(gè)action。所以初始化action的時(shí)候,最好把優(yōu)先級(jí)高的設(shè)置在前面。如果使用UNTextInputNotificationAction , 系統(tǒng)會(huì)提供用戶一個(gè)輸入框最為這個(gè)通知的響應(yīng)。文本輸入框這種通知響應(yīng)方式對(duì)IM相關(guān)的app非常有用。截圖如下:

配置推送的聲音
本地推送和遠(yuǎn)程推送可以自定義提示音。因?yàn)槭峭ㄟ^(guò)系統(tǒng)聲音渠道播放的,所以自定義聲音必須遵循如下幾種數(shù)據(jù)格式:
- Linear PCM
- MA4 (IMA/ADPCM)
- μLaw
- aLaw
具體來(lái)講:聲音文件可以放到app bundle里面,或者如果是從網(wǎng)上下載的話就放在你pp沙盒目錄下的Library/Sounds。自定義聲音必須少于30秒。如果大于30秒將會(huì)用默認(rèn)的系統(tǒng)的提示音取代。
需要注意的幾點(diǎn)
- 通知類別UNNotificationCategoryOptions參數(shù)含義。
typedef NS_OPTIONS(NSUInteger, UNNotificationCategoryOptions) {
// 用戶點(diǎn)擊系統(tǒng)取消Action是否響應(yīng)到通知代理
UNNotificationCategoryOptionCustomDismissAction = (1 << 0),
// 通知在CarPlay(CarPlay 是美國(guó)蘋(píng)果公司發(fā)布的車載系統(tǒng))
UNNotificationCategoryOptionAllowInCarPlay = (1 << 1),
// 通知預(yù)覽關(guān)閉情況下是否顯示通知的title
UNNotificationCategoryOptionHiddenPreviewsShowTitle __IOS_AVAILABLE(11.0) __WATCHOS_PROHIBITED = (1 << 2),
// 通知預(yù)覽關(guān)閉情況下是否顯示通知的子title UNNotificationCategoryOptionHiddenPreviewsShowSubtitle __IOS_AVAILABLE(11.0) __WATCHOS_PROHIBITED = (1 << 3),
}
這里簡(jiǎn)單說(shuō)一下什么是通知預(yù)覽。在設(shè)置里面

關(guān)閉之后設(shè)置UNNotificationCategoryOptions為UNNotificationCategoryOptionHiddenPreviewsShowTitle | UNNotificationCategoryOptionHiddenPreviewsShowSubtitle。效果如下:

顯示的內(nèi)容就用數(shù)字代替了。
- 通知Action參數(shù)UNNotificationActionOptions含義
typedef NS_OPTIONS(NSUInteger, UNNotificationActionOptions) {
//執(zhí)行代理是否需要在非鎖屏狀態(tài)下
UNNotificationActionOptionAuthenticationRequired = (1 << 0),
//決定按鈕顯示是否為紅色。(紅色代表消極?)
UNNotificationActionOptionDestructive = (1 << 1),
//決定點(diǎn)擊action是否打開(kāi)app
UNNotificationActionOptionForeground = (1 << 2),
}
特別注意當(dāng)設(shè)置為UNNotificationActionOptionAuthenticationRequired的時(shí)候,即使點(diǎn)擊了action代理方法userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler也不會(huì)走。如果設(shè)置為UNNotificationActionOptionNone。則即使在鎖屏情況下也會(huì)走代理方法
創(chuàng)建本地推送
在app運(yùn)行的時(shí)候(無(wú)論是前臺(tái)還是后臺(tái)),設(shè)置好本地推送,然后系統(tǒng)會(huì)在合適的時(shí)間發(fā)出推送。
- 如果app沒(méi)有在運(yùn)行或者在后臺(tái),系統(tǒng)將直接向用戶展示通知,如果app提供了app通知擴(kuò)展,系統(tǒng)使用用戶自定義的界面提示用戶。
- 如果app在前臺(tái),系統(tǒng)會(huì)給app在內(nèi)部處理通知的機(jī)會(huì)。
前面講過(guò)配置本地推送,總結(jié)一下有如下幾個(gè)步驟:
- 設(shè)置內(nèi)容:創(chuàng)建并設(shè)置好UNMutableNotificationContent。
- 設(shè)置觸發(fā)器:創(chuàng)建通知觸發(fā)器UNCalendarNotificationTrigger, UNTimeIntervalNotificationTrigger, UNLocationNotificationTrigger其中一種。設(shè)置好觸發(fā)通知的條件。
- 連接內(nèi)容和觸發(fā)器:創(chuàng)建UNNotificationRequest ,設(shè)置content和trigger。
- 添加通知:調(diào)用`addNotificationRequest:withCompletionHandler:計(jì)劃通知。
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
content.title = [NSString localizedUserNotificationStringForKey:@"Wake up!" arguments:nil];
content.body = [NSString localizedUserNotificationStringForKey:@"Rise and shine! It's morning time!"
arguments:nil];
NSDateComponents* date = [[NSDateComponents alloc] init];
date.second = 10;
UNCalendarNotificationTrigger* trigger = [UNCalendarNotificationTrigger
triggerWithDateMatchingComponents:date repeats:NO];
// Create the request object.
UNNotificationRequest* request = [UNNotificationRequest
requestWithIdentifier:@"MorningAlarm" content:content trigger:trigger];
這里提供的唯一標(biāo)識(shí)是為了后面查找或者取消通知。
前面提到的通知類別,這個(gè)時(shí)候就可以排上用場(chǎng)了。在UNMutableNotificationContent設(shè)置好之前已經(jīng)注冊(cè)了的通知類別categoryIdentifier。注意必須在安排全這個(gè)通知請(qǐng)求之前就設(shè)置好這個(gè)值。
UNNotificationContent *content = [[UNNotificationContent alloc] init];
// Configure the content. . .
// Assign the category (and the associated actions).
content.categoryIdentifier = @"TIMER_EXPIRED";
前面提到過(guò)可以自定義通知的聲音。這里同樣是在UNMutableNotificationContent對(duì)象上設(shè)置。使用UNNotificationSound對(duì)象,這個(gè)對(duì)象決定是使用自定義的聲音還是系統(tǒng)默認(rèn)聲音。注意自定義的音頻文件必須在設(shè)備上存在。存儲(chǔ)在app的main bundle里面,或者下載并存儲(chǔ)到app沙盒路徑下得Library/Sounds子目錄。
content.sound = [UNNotificationSound soundNamed:@"MySound.aiff"];
發(fā)出或者安排通知:系統(tǒng)是異步的安排本地通知。當(dāng)安排完成或者出錯(cuò)之后通過(guò)回調(diào)block告知。
// Create the request object.
UNNotificationRequest* request = [UNNotificationRequest
requestWithIdentifier:@"MorningAlarm" content:content trigger:trigger];
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
if (error != nil) {
NSLog(@"%@", error.localizedDescription);
}
}];
安排過(guò)的通知會(huì)一直存活到系統(tǒng)取消或者app處理過(guò)。當(dāng)通知發(fā)出過(guò)了系統(tǒng)會(huì)自己取消掉通知,除非這個(gè)通知設(shè)定為重復(fù)通知。取消通知可以通過(guò)removePendingNotificationRequestsWithIdentifiers:實(shí)現(xiàn)。上面提到過(guò),取消是通過(guò)之前設(shè)置的標(biāo)識(shí)取消的。
為了響應(yīng)用戶對(duì)通知的處理。需要實(shí)現(xiàn)那UNUserNotificationCenter的代理UNUserNotificationCenterDelegate。當(dāng)自定義了action的時(shí)候代理必須實(shí)現(xiàn)。
// The method will be called on the delegate only if the application is in the foreground. If the method is not implemented or the handler is not called in a timely manner then the notification will not be presented. The application can choose to have the notification presented as a sound, badge, alert and/or in the notification list. This decision should be based on whether the information in the notification is otherwise visible to the user.
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler __IOS_AVAILABLE(10.0) __TVOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0);
// The method will be called on the delegate when the user responded to the notification by opening the application, dismissing the notification or choosing a UNNotificationAction. The delegate must be set before the application returns from application:didFinishLaunchingWithOptions:.
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0) __TVOS_PROHIBITED;
以下幾點(diǎn)值得注意下:
- 當(dāng)app在前臺(tái)的時(shí)候,app可以靜音掉通知(也就是不處理)或者告訴系統(tǒng)自己展示其他頁(yè)面。再次提醒,系統(tǒng)會(huì)默認(rèn)靜音掉所有通知當(dāng)app在前臺(tái)的時(shí)候。系統(tǒng)或直接把通知給app,讓app自己使用通知傳遞的數(shù)據(jù)去做app自定義的任務(wù)。如果該需要系統(tǒng)繼續(xù)顯示通知界面,需要為UNUserNotificationCenter提供一個(gè)代理對(duì)象,實(shí)現(xiàn)userNotificationCenter:willPresentNotification:withCompletionHandler: 方法,在這個(gè)方法里面應(yīng)該處理通知數(shù)據(jù)。完成之后,執(zhí)行app想讓系統(tǒng)去使用的通知發(fā)出選項(xiàng)。如果不做任何配置,系統(tǒng)將會(huì)靜音掉這個(gè)通知。
- 當(dāng)app在后臺(tái)或者已經(jīng)停止運(yùn)行。系統(tǒng)不會(huì)調(diào)用userNotificationCenter:willPresentNotification:withCompletionHandler:,在這種情況下,系統(tǒng)會(huì)自己提示用戶根據(jù)通知的設(shè)置。app可以使用getDeliveredNotificationsWithCompletionHandler: 來(lái)查看已經(jīng)發(fā)送過(guò)得通知。
- 當(dāng)用戶通過(guò)界面選擇了自定義的action,系統(tǒng)將會(huì)通知app,用戶當(dāng)前所做的選擇。同樣是通過(guò)代理告訴app。在代理userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler: 必須實(shí)現(xiàn)那能夠處理所有自定義的action。app如果沒(méi)有運(yùn)行,而用戶點(diǎn)擊了action。系統(tǒng)將會(huì)以后臺(tái)模式打開(kāi)app用于處理用戶的選擇。千萬(wàn)不要在這段時(shí)間處理一些無(wú)關(guān)的任務(wù)。
除了自定義的,還有系統(tǒng)的action。用戶可能沒(méi)有選擇自定義的action而是取消掉通知頁(yè)面或者打開(kāi)app。和自定義action一樣,系統(tǒng)的action也有對(duì)應(yīng)的唯一標(biāo)識(shí):
- UNNotificationDismissActionIdentifier :用戶沒(méi)有選擇action,直接取消了通知界面
- UNNotificationDefaultActionIdentifier :用戶沒(méi)有選擇action,直接打開(kāi)了app
// The user dismissed the notification without taking action.
}
else if ([response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier]) {
// The user launched the app.
}
if ([response.notification.request.content.categoryIdentifier isEqualToString:@"TIMER_EXPIRED"]) {
// Handle the actions for the expired timer.
if ([response.actionIdentifier isEqualToString:@"SNOOZE_ACTION"])
{
// Invalidate the old timer and create a new one. . .
}
else if ([response.actionIdentifier isEqualToString:@"STOP_ACTION"])
{
// Invalidate the timer. . .
}
}
注意事項(xiàng)
- (針對(duì)本地推送)應(yīng)用在前臺(tái)并不是不能彈出系統(tǒng)通知框。上面提到如果該需要系統(tǒng)繼續(xù)顯示通知界面,需要為UNUserNotificationCenter提供一個(gè)代理對(duì)象,實(shí)現(xiàn)userNotificationCenter:willPresentNotification:withCompletionHandler: 方法,在這個(gè)方法里面應(yīng)該處理通知數(shù)據(jù)。完成之后,執(zhí)行app想讓系統(tǒng)去使用的通知發(fā)出選項(xiàng)。如果不做任何配置,系統(tǒng)將會(huì)靜音掉這個(gè)通知。
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler{
//1. 處理通知
//2. 處理完成后條用 completionHandler ,用于指示在前臺(tái)顯示通知的形式
completionHandler(UNNotificationPresentationOptionAlert);
}
- UNNotificationTrigger:系統(tǒng)提供了基于時(shí)間和基于地理位置的Trigger。如果想設(shè)置為時(shí)間Trigger為Repeat,至少需要60s,如果用UNCalendarNotificationTrigger,雖然可以設(shè)置小于60s的時(shí)間間隔但是并不是按照設(shè)置的時(shí)間執(zhí)行
- 重復(fù)的通知只會(huì)在通知欄顯示一個(gè)
遠(yuǎn)程推送(Remote Push)
遠(yuǎn)程推送稍微大點(diǎn)的app都有有這功能。為了接受遠(yuǎn)程推送需要有如下幾步:
- 開(kāi)啟遠(yuǎn)程推送
- 注冊(cè)APNS服務(wù),接收蘋(píng)果返回的device token
- 把收到的device token發(fā)送給服務(wù)端
- 處理接收到的遠(yuǎn)程推送
配置推送
開(kāi)啟遠(yuǎn)程推送直接可以在xcode中操作。之前需要申請(qǐng)推送證書(shū)。如果沒(méi)有必須得entitlements,那么app在被app store審核的時(shí)候會(huì)被拒。
每一次app啟動(dòng),都會(huì)在APNS注冊(cè)。大致流程如下:
- app請(qǐng)求APNS注冊(cè)
- 注冊(cè)成功之后,APNS會(huì)把對(duì)應(yīng)的token返回給app
- 系統(tǒng)通過(guò)代理告訴app接收到的token
- app把這個(gè)token發(fā)送給服務(wù)端。
token其實(shí)就是當(dāng)前這個(gè)app在APNS中的唯一標(biāo)識(shí)。服務(wù)端拿著這個(gè)token,向APNS服務(wù)發(fā)送推送請(qǐng)求
絕對(duì)不要在app里面緩存token,應(yīng)該直接從系統(tǒng)獲取。在某些情況下APNS為了保證token的唯一性會(huì)更新app的token。比如用戶從備份中恢復(fù)系統(tǒng)、用戶在全新的設(shè)備上安裝了app,用戶重新安裝了操作系統(tǒng)。當(dāng)token在APNS沒(méi)有改變的時(shí)候,去獲取,APNS會(huì)快速的返回(也就是APNS或者系統(tǒng)其實(shí)做了緩存的,只是不要在app內(nèi)部去做緩存)。
調(diào)用registerForRemoteNotifications注冊(cè)APNS。在app啟動(dòng)的時(shí)候都會(huì)調(diào)用這個(gè)方法。系統(tǒng)會(huì)異步的回調(diào)回來(lái)。
Token長(zhǎng)度是不確定的,所以不要寫(xiě)死token的長(zhǎng)度。在注冊(cè)成功之后,只要當(dāng)token改變之后才APP才和APNS再次交互。否則調(diào)用registerForRemoteNotifications的時(shí)候,application:didRegisterForRemoteNotificationsWithDeviceToken: 會(huì)立即返回已經(jīng)存在的token。
如果token在app運(yùn)行的時(shí)候 改變。app會(huì)直接調(diào)用application:didRegisterForRemoteNotificationsWithDeviceToken:。
- (void)applicationDidFinishLaunching:(UIApplication *)app {
// Configure the user interactions first.
[self configureUserInteractions];
// Register for remote notifications.
[[UIApplication sharedApplication] registerForRemoteNotifications];
}
// Handle remote notification registration.
- (void)application:(UIApplication *)app
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken {
// Forward the token to your provider, using a custom method.
[self enableRemoteNotificationFeatures];
[self forwardTokenToServer:devTokenBytes];
}
- (void)application:(UIApplication *)app
didFailToRegisterForRemoteNotificationsWithError:(NSError *)err {
// The token is not currently available.
NSLog(@"Remote notification support is unavailable due to error: %@", err);
[self disableRemoteNotificationFeatures];
}
修改系統(tǒng)推送的展示方式
iOS10之后可以通過(guò) notification service app extension 修改通知的展現(xiàn)方式。(原理就是在之前推送的基礎(chǔ)上增加一層notification service app extension ,用于過(guò)濾服務(wù)端推送過(guò)來(lái)的數(shù)據(jù),根據(jù)數(shù)據(jù)進(jìn)行自定義)
分如下幾個(gè)步驟:
- 解密從服務(wù)端傳過(guò)來(lái)的加密數(shù)據(jù)
- 下載多媒體資源,并且作為通知附件
- 改變通知的title和文本
- 增加線程標(biāo)識(shí)用于修改通知的userinfo參數(shù)
如何實(shí)現(xiàn)那notification service app extension網(wǎng)上很多,這里不累贅了。可以看看iOS10推送必看UNNotificationServiceExtension
一張圖解釋

注意事項(xiàng)
- 同宿主target一樣, app extension的target也需要在 notification service app extension的target中打開(kāi)推送的capability才能正常收到推送。
該特性是在iOS7添加的,一句話來(lái)講就是:應(yīng)用在后臺(tái)收到通知后能夠運(yùn)行一段代碼,可用于從服務(wù)器獲取內(nèi)容更新。
靜默推送和一般推送的區(qū)別:
iOS7之前

iOS7之后

如果只攜帶content-available: 1 不攜帶任何badge,sound 和消息內(nèi)容等參數(shù),則可以不打擾用戶的情況下進(jìn)行內(nèi)容更新等操作即為“Silent Remote Notifications”
具體設(shè)置如下:

當(dāng)啟用Backgroud Modes -> Remote notifications 后,notification 處理函數(shù)一律切換到下面函數(shù),后臺(tái)推送代碼也在此函數(shù)中調(diào)用
/*! This delegate method offers an opportunity for applications with the "remote-notification" background mode to fetch appropriate new data in response to an incoming remote notification. You should call the fetchCompletionHandler as soon as you're finished performing that operation, so the system can accurately estimate its power and data cost.
This method will be invoked even if the application was launched or resumed because of the remote notification. The respective delegate methods will be invoked first. Note that this behavior is in contrast to application:didReceiveRemoteNotification:, which is not called in those cases, and which will not be invoked if this method is implemented. !*/
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler NS_AVAILABLE_IOS(7_0);
這里提到了application:didReceiveRemoteNotification,看了一下還有這些知識(shí)點(diǎn)。
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo NS_DEPRECATED_IOS(3_0, 10_0, "Use UserNotifications Framework's -[UNUserNotificationCenterDelegate willPresentNotification:withCompletionHandler:] or -[UNUserNotificationCenterDelegate didReceiveNotificationResponse:withCompletionHandler:] for user visible notifications and -[UIApplicationDelegate application:didReceiveRemoteNotification:fetchCompletionHandler:] for silent remote notifications");
一定要注意這幾個(gè)方法是屬于不同類的
- application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler :是屬于AppDelegate,用于靜默推送,用戶不可見(jiàn)。
- 其余兩個(gè)是是屬于UNUserNotificationCenter,用于用戶可見(jiàn)的推送,比如有提示橫幅。
注意事項(xiàng)
Silent Remote Notifications是在 Apple 的限制下有一定的頻率控制,但具體頻率不詳。所以并不是所有的 “Silent Remote Notifications” 都能按照預(yù)期到達(dá)客戶端觸發(fā)函數(shù)。
Background下提供給應(yīng)用的運(yùn)行時(shí)間窗是有限制的,如果需要下載較大的文件請(qǐng)參考 Apple 的 NSURLSession 的介紹。
Background Remote Notification 的前提是要求客戶端處于Background 或 Suspended 狀態(tài),如果用戶通過(guò) App Switcher 將應(yīng)用從后臺(tái) Kill 掉應(yīng)用將不會(huì)喚醒應(yīng)用處理 background 代碼。(這一點(diǎn)通過(guò)voip Push可以實(shí)現(xiàn))
-
靜默推送格式:
- 增加content-available字段,并設(shè)成1
- alert字段必須為空,否則收到的就不是靜默推送
- sound字段設(shè)不設(shè)不影響靜默推送的接收。
-
badge字段設(shè)不設(shè)不影響靜默推送的接收。
再次強(qiáng)調(diào):靜默推送只能在應(yīng)用在前臺(tái)和應(yīng)用在后臺(tái)掛起時(shí)收到,也就是說(shuō),如果應(yīng)用未啟動(dòng)或進(jìn)程被殺掉,靜默推送是喚醒不了設(shè)備的。
-
推送回調(diào)方法
- willPresentNotification:withCompletionHandler 用于前臺(tái)運(yùn)行
- didReceiveNotificationResponse:withCompletionHandler 用于后臺(tái)及程序退出
- didReceiveRemoteNotification:fetchCompletionHandler用于靜默推送
- (void)application:(UIApplication *)application didReceiveRemoteNotification(會(huì)處理所有推送,iOS10之前)
Voip推送(Voip Push)
Voip Push相對(duì)于Silent Push又更近一步。解決了Silent Push使用場(chǎng)景必須是app必須存活的問(wèn)題。Voip Push可以在應(yīng)用被殺死的情況下,也可以喚醒a(bǔ)pp。
基本介紹
蘋(píng)果在iOS8引入PushKit framework,app可以通過(guò)遠(yuǎn)程推送就可以喚醒,不過(guò)這個(gè)特性只有在voip類應(yīng)用起作用。在之前voip類app需要在后臺(tái)保持長(zhǎng)連接,才能實(shí)時(shí)收到voip,這樣非常耗電,于是在iOS8之后引入了Push Kit解決這個(gè)問(wèn)題。
之前有人分析過(guò)what's up 在iOS8之后的變化,原文入下:
每次用戶有新的離線消息,普通文本或者是voip call,app都會(huì)先被后臺(tái)喚醒,再?gòu)膕erver拉取離線消息,最后生成local push。等用戶點(diǎn)擊local push啟動(dòng)app的時(shí)候,沒(méi)有啟動(dòng)頁(yè)面,沒(méi)有connecting和loading,所有的數(shù)據(jù)已經(jīng)準(zhǔn)備就緒,就好像WhatsApp一直在后臺(tái)運(yùn)行一樣。
voip 推送和傳統(tǒng)的推送的區(qū)別如下:

通過(guò)上圖可以知道voip push有著自己完整的一套邏輯,與此同時(shí)同樣需要申請(qǐng)相關(guān)證書(shū)??梢园裿oip push 、 遠(yuǎn)程push 與本地 push三者視為同一級(jí)別。
下圖是voip push 對(duì)應(yīng)的證書(shū)

代碼來(lái)講實(shí)現(xiàn)下面兩個(gè)代理就行了
- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)pushCredentials forType:(PKPushType)type {
NSData *voipToken = pushCredentials.token;
NSLog(@"voip token:%@", voipToken);
}
- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type {
//用戶處理
// 呼出系統(tǒng)接聽(tīng)界面
// 或者生成本地推送
}
注意事項(xiàng)
- voip Push和remote Push 在token獲取、服務(wù)端交互是大致相同的。
- voip Push和silent Push最大的不同在于,silent Push必須是app沒(méi)有被殺死才能在后臺(tái)喚醒應(yīng)用,而voip Push是可以在app殺死之后,喚醒應(yīng)用。
- voip Push一般使用方式是
- 服務(wù)端發(fā)出推送,app收到推送
- app解析數(shù)據(jù),根據(jù)數(shù)據(jù)先處理好業(yè)務(wù)邏輯。比如更新數(shù)據(jù)
- 通過(guò)本地推送告訴用戶
- 用戶打開(kāi)app,顯示之前已經(jīng)處理好的內(nèi)容
蘋(píng)果 Push的優(yōu)化記錄
去了解一項(xiàng)技術(shù)的進(jìn)化史,最好的方式就是去看官方的文檔更新記錄。蘋(píng)果 Push的優(yōu)化記錄可以看到蘋(píng)果本地推送和遠(yuǎn)程推送文檔更新記錄。
寫(xiě)在最后
花了將近兩天的時(shí)間總結(jié)Push這塊知識(shí)。過(guò)程中甚至感受到了蘋(píng)果為提升用戶體驗(yàn)而做的努力。從最開(kāi)始的只要應(yīng)用不在前臺(tái)就無(wú)法喚醒的普通Push,到iOS7的新增的只能在后臺(tái)喚醒a(bǔ)pp靜默Push,再到后面iOS8新增的即使app沒(méi)有運(yùn)行也可以喚醒a(bǔ)pp的voip Push。Push技術(shù)的演變也是蘋(píng)果對(duì)用戶體驗(yàn)提升的見(jiàn)證。
回到作為技術(shù)人的角度,最近大半年都過(guò)得太過(guò)于浮躁。之前的炒得風(fēng)風(fēng)火火的數(shù)字貨幣,ICO項(xiàng)目等讓自己太過(guò)于浮躁。直到前不久認(rèn)識(shí)了一個(gè)技術(shù)大牛,才讓自己認(rèn)識(shí)到做技術(shù)還是需要沉淀,還是要形成知識(shí)體系,靜下心來(lái)好好打磨。
內(nèi)心激蕩著那句對(duì)我說(shuō)的話:“多看看計(jì)算機(jī)原理、底層相關(guān)的書(shū)。遇到問(wèn)題多想幾個(gè)為什么”。雖然這句話有裝逼嫌疑,但是不得不說(shuō)是走向技術(shù)大牛的最佳途徑。
最近打算出一系列關(guān)于計(jì)算機(jī)底層、原理的文章。敬請(qǐng)期待!?。?!
擴(kuò)展閱讀
How to use PushKit for VoIP Push
Background Execution
Local and Remote Notification Programming Guide
iOS10最新實(shí)現(xiàn)遠(yuǎn)程通知的開(kāi)發(fā)教程詳解
iOS 靜默推送實(shí)現(xiàn)(推送背景為個(gè)推)
iOS10推送必看UNNotificationServiceExtension
iOS開(kāi)發(fā)-本地通知與遠(yuǎn)程通知
iOS Push的門(mén)道
