iOS推送分為遠(yuǎn)程推送 和 本地推送兩種,本地推送此文章先不說,之后會有新的文章更新。
遠(yuǎn)程推送將一些重要的信息推送到用戶的相關(guān)APP上,不管你的APP是否運(yùn)行,或者在后臺掛起 或者 完全殺死,都能收到推送。并且在iOS10 之后,蘋果允許在收到推送內(nèi)容后有30s 的時間對推送的內(nèi)容進(jìn)行修改,或者下載一些圖片(這個在文章的后面會提到),推送會展示一個彈框(alert),聲音(sound),APP icon 上的紅色標(biāo)志(badge)。
遠(yuǎn)程推送的原理
蘋果推送服務(wù)通知是由自己專門的推送服務(wù)器APNS(Apple push Notification service)來完成的,其過程是apns 接收到我們自己的應(yīng)用服務(wù)器發(fā)出的被推送消息,將這條消息推送到指定的iOS設(shè)備上,然后再由iOS設(shè)備通知到我們的應(yīng)用程序,我們將會以通知或者聲音的形式受到推送回來的消息。
我們的APP在啟動的時候,會攜帶設(shè)備號和應(yīng)用id 想APNs 注冊推送服務(wù),成功后APNs 會返回一個標(biāo)識devicetoken,然后我們會將收到的devicetoken發(fā)送到我們自己的服務(wù)器,當(dāng)我們需要推送消息時,我們的服務(wù)器會將推送內(nèi)容payLoad(之前是不超過256字節(jié),現(xiàn)在應(yīng)該比較寬松了,具體未定,json 格式)和devicetoken發(fā)送給APNs服務(wù)器。APNs服務(wù)器將新消息推送到iOS設(shè)備上(在有網(wǎng)的情況下設(shè)備會與APNs服務(wù)器建立一個長連接tcp)。
devicetoken 在以下情況下會發(fā)生改變:
1、同一款設(shè)備上重新安裝同一款應(yīng)用
2、不同設(shè)備上安裝同一款應(yīng)用
3、設(shè)備重新升級了系統(tǒng),同一個APP對應(yīng)的devicetoken也會發(fā)生改變
-
APP注冊推送服務(wù)過程
注冊流程圖.png
上述圖片完成了如下流程:
1、安裝了推送功能APP的設(shè)備,攜帶者設(shè)備號和APPid 連接APNs服務(wù)器。
2、連接成功后,APNs 通過打包和加密等處理生成devicetoken,返回給注冊的設(shè)備。
3、APP拿到devicetoken后,將它發(fā)送給我們自己的服務(wù)器。
4、完成需要被推送的設(shè)備在蘋果服務(wù)器和我們自己服務(wù)器之前的注冊。
-
推送過程
推送過程.png
上述圖片完成如下流程:
1、首先,我們的設(shè)備安裝了具有推送功能的APP后(APP在啟動的時候會在lunch方法里注冊推送),在有網(wǎng)絡(luò)的情況下會連接到apns 推送服務(wù)器,在連接的過程中apns 會解密設(shè)備的devicetoken進(jìn)行驗(yàn)證,驗(yàn)證成功后,建立tcp 連接。
2、Provider(我們自己的應(yīng)用服務(wù)器)將 要被推送的消息結(jié)合接收消息的iOS設(shè)備的devicetoken發(fā)送給apns服務(wù)器
3、apns 收到Provider 發(fā)送過來的推送消息,解密devicetoken進(jìn)行驗(yàn)證,驗(yàn)證成功后將消息發(fā)送給指定的設(shè)備
4、iOS設(shè)備收到蘋果推送的消息后,通知我們的APP并顯示和提示用戶(alert,sound,badge)。
比較直觀的流程圖:

信息結(jié)構(gòu)圖:

上圖顯示的這個消息體就是Provider(我們服務(wù)器)發(fā)送給apns服務(wù)器的消息結(jié)構(gòu),APNs 驗(yàn)證這個結(jié)構(gòu)正確并提取其中的信息后,再將消息推送給指定的iOS設(shè)備。這個結(jié)構(gòu)體包括五個部分,第一個部分是命令標(biāo)識符,第二個部分是devicetoken的長度,第三部分是devicetoken字符串,第四部分是推送消息體(payload)的長度,最后一部分也就是真正的消息內(nèi)容了,里面包含了推送的基本信息,比如消息內(nèi)容,應(yīng)用icon右上角顯示多少數(shù)字以及推送消息到達(dá)時所播放的聲音等。
payload的結(jié)構(gòu):
{
“aps”:{
“alert”:“CSDN給您發(fā)送了新消息”,
“badge”:1,
“sound”:“default”
},
}
這其實(shí)就是個json結(jié)構(gòu)體,alert標(biāo)簽的內(nèi)容就是會顯示在用戶手機(jī)上的推送信息,badge 顯示的數(shù)量是會在APP icon右上角顯示的數(shù)量,提示有多少條未讀消息等,sound就是當(dāng)推送信息送達(dá)時手機(jī)播放的聲音,沒有特殊要求就是default 系統(tǒng)默認(rèn)聲音。
使用自己服務(wù)器完成推送
1、iOS端代碼
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
/*
注冊推送服務(wù)
申請APP需要接受來自服務(wù)商提供推送消息
launchOptions:保存了app啟動的原因信息,如果app是因?yàn)辄c(diǎn)擊通知欄啟動的,可以在launchOptions獲取到通知的具體內(nèi)容
*/
//判斷是不是點(diǎn)擊通知欄啟動
NSDictionary *remoteNotification = [launchOptions objectForKey:@"UIApplicationLaunchOptionsRemoteNotificationKey"];
if (remoteNotification != nil) {
// 點(diǎn)擊通知欄消息啟動
self.isLaunchedByNotification = YES;
}else{
// 不是點(diǎn)擊通知欄消息啟動
self.isLaunchedByNotification = NO;
}
//iOS10以后的注冊方法
if ([[UIDevice currentDevice].systemVersion floatValue] >= 10.0) {
//來自UserNotification框架
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
// center.delegate = self;
// 請求授權(quán)
[center requestAuthorizationWithOptions:UNAuthorizationOptionBadge |UNAuthorizationOptionSound | UNAuthorizationOptionAlert completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted) {//授權(quán)成功
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
NSLog(@"=======%@", settings);
}];
} else {
//點(diǎn)擊不允許,注冊失敗
}
}];
}
} else if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {
[[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert categories:nil]];
} else {
[[UIApplication sharedApplication]registerForRemoteNotificationTypes:UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert];
}
//最后一定要調(diào)用這個方法,不然收不到apns返回的devicetoken
[[UIApplication sharedApplication]registerForRemoteNotifications];
發(fā)起申請后 會有兩個回調(diào)方法,一個是注冊成功,返回devicetoken,另一個是注冊失敗的回調(diào)方法
//成功
- (void)application:(UIApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
NSLog(@"deviceToken = %@",deviceToken);
//我們會在收到此方法后,將收到的devicetoken 發(fā)送給我們自己的服務(wù)器,我們的服務(wù)器在將消息發(fā)送給apns 時會用到,用于apns 的驗(yàn)證和識別
}
//注冊apns失敗回調(diào)
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
//Optional
HLLog(@"did Fail To Register For Remote Notifications With Error: %@", error);
}
以上是我們本地代碼完成了在apns 和 Provider 之間的注冊,以下的方法是在apns 成功推送到我們設(shè)備后的回調(diào)方法。
//iOS10 在前臺收到通知
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
UNNotificationRequest *request = notification.request; //收到推送的請求
UNNotificationContent *content = request.content;//收到推送的消息內(nèi)容
NSDictionary *userInfo = content.userInfo;
NSNumber *badge = content.badge;//推送的角標(biāo)
NSString *body = content.body;//推送消息體
UNNotificationSound *sound = content.sound;//聲音
NSString *subtitle = content.subtitle;//推送消息的副標(biāo)題
NSString *title = content.title;//推送消息的標(biāo)題
if ([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
//UNPushNotificationTrigger 觸發(fā)器,專門用于遠(yuǎn)程推送,其他一般是本地通知要用到的
} else {
//本地通知
}
UNNotificationPresentationOptions options = UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert;
completionHandler(options);
}
//ios10 點(diǎn)擊推送消息
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
UNNotificationRequest *request = response.notification.request;
UNNotificationContent *content = request.content;
NSDictionary *userInfo = content.userInfo;
NSNumber *badge = content.badge;
NSString *body = content.body;
NSString *subtitle = content.subtitle;
NSString *title = content.title;
if ([request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
//遠(yuǎn)程推送
} else {
//本地通知
}
completionHandler();
}
/*
iOS7及以上,推送字段必須包含content-available = 1 并且Background Modes 中勾選Remote notifications,才能調(diào)用此方法.
如果滿足上述條件,那么收到推送消息時,應(yīng)用在前臺和后臺不殺死的情況下還有點(diǎn)擊通知欄消息都會調(diào)用此方法,可以在此方法內(nèi)做一些后臺操作,如下載數(shù)據(jù) 更新UI等
*/
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
NSDictionary *aps = userInfo[@"aps"];
NSString * storeid = aps[@"order_id"];
if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
}else if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground){
HLLog(@"程序在后臺 或從后臺點(diǎn)擊通知欄 運(yùn)行 該方法");
}else{
HLLog(@"后臺處于前臺的過度");
}
completionHandler(UIBackgroundFetchResultNewData);
}
現(xiàn)在看一下服務(wù)端的代碼,大致了解一下,這段也是摘自網(wǎng)上的相關(guān)文章的。
在此之前我們是需要將推送證書導(dǎo)出成p12文件交給服務(wù)器端,不會導(dǎo)出的自行百度。
java端代碼
import javapns.back.PushNotificationManager;
import javapns.back.SSLConnectionHelper;
import javapns.data.Device;
import javapns.data.PayLoad;
public class pushService {
public static void main(String[] args) {
try {
//這個token 就是客戶端從APNs服務(wù)器獲取到的devicetoken
String deviceToken = "eab6df47eb4f81e0aaa93bb208cffd7dc3884fd346ea0743fcf93288018cfcb6";
//被推送的iphone應(yīng)用程序標(biāo)示符
PayLoad payLoad = new PayLoad();
payLoad.addAlert("測試我的push消息");
payLoad.addBadge(1);
payLoad.addSound("default");
PushNotificationManager pushManager = PushNotificationManager.getInstance();
pushManager.addDevice("iphone", deviceToken);
//測試推送服務(wù)器地址:gateway.sandbox.push.apple.com /2195
//產(chǎn)品推送服務(wù)器地址:gateway.push.apple.com / 2195
String host="gateway.sandbox.push.apple.com"; //測試用的蘋果推送服務(wù)器
int port = 2195;
String certificatePath = "/Users/hsw/Desktop/PushTest/PushTest.p12"; //剛才在mac系統(tǒng)下導(dǎo)出的證書
String certificatePassword= "123456";
pushManager.initializeConnection(host, port, certificatePath,certificatePassword, SSLConnectionHelper.KEYSTORE_TYPE_PKCS12);
//Send Push
Device client = pushManager.getDevice("iphone");
pushManager.sendNotification(client, payLoad); //推送消息
pushManager.stopConnection();
pushManager.removeDevice("iphone");
}
catch (Exception e) {
e.printStackTrace();
System.out.println("push faild!");
return;
}
System.out.println("push succeed!");
}
}
遠(yuǎn)程推送涉及到的方法
1、 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
2、 - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo;
3、 - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler;
4、 - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler;
5、 - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler;
1、會在APP啟動完成后調(diào)用,lunchOptions保存了app啟動的原因信息,如果app是因?yàn)辄c(diǎn)擊通知欄啟動的,可以在lunchOptions 獲取到通知的具體內(nèi)容(上文已經(jīng)提到如何獲取)
2、會在接收到通知的時候調(diào)用,在最新的iOS10中已經(jīng)廢棄,建議不再使用。
3、在iOS7之后新增的方法,可以說是2的升級版本,如果APP最低支持iOS7的話可以不用添加2,其中completionHandler這個block可以填寫的參數(shù)UIBackgroundFetchResult是一個枚舉值。主要是用來在后臺狀態(tài)下進(jìn)行一些操作的,比如請求數(shù)據(jù),操作完成之后,必須通知系統(tǒng)獲取完成,可供選擇的結(jié)果:
typedef NS_ENUM(NSUInteger, UIBackgroundFetchResult) {
// 獲取到了新數(shù)據(jù)(此時系統(tǒng)將對現(xiàn)在的UI狀態(tài)截圖并更新APP Switcher中你的應(yīng)用截屏)
UIBackgroundFetchResultNewData,
UIBackgroundFetchResultNoData,//沒有新數(shù)據(jù)
UIBackgroundFetchResultFailed//獲取失敗
}
以上操作的前提是已經(jīng)在Background Modes 里面勾選了Remote notifications(推送喚醒)且推送的消息中包含content-available = 1字段。
4、是iOS10新增的 UNUserNotificationCenterDelegate 代理方法,在ios10的環(huán)境下,點(diǎn)擊通知欄都會調(diào)用這個方法。
5、也是iOS10新增的代理方法,在iOS10 以前,如果應(yīng)用處于前臺狀態(tài),接收到推送,通知欄是不會有任何提示的,如果開發(fā)者需要展示通知,需要在3方法中提取到通知內(nèi)容展示。在iOS10 中,如果開發(fā)者需要前臺展示通知,可以再在這個方法completionHandler傳入相應(yīng)的參數(shù)。
typedef NS_OPTIONS(NSUInteger, UNNotificationPresentationOptions) {
UNNotificationPresentationOptionBadge = (1 << 0),
UNNotificationPresentationOptionSound = (1 << 1),
UNNotificationPresentationOptionAlert = (1 << 2),
}
- 當(dāng)程序處于關(guān)閉狀態(tài)的時候收到推送消息,點(diǎn)擊應(yīng)用程序圖標(biāo)無法獲取推送消息,iOS10環(huán)境下,點(diǎn)擊通知欄會調(diào)用1,4,非iOS10的情況下 會調(diào)用1,3
- 當(dāng)程序處于前臺狀態(tài)下收到推送消息,iOS10的環(huán)境下如果推送的消息包含content-available字段的話,執(zhí)行方法3,5,否則只執(zhí)行5,非iOS10的情況會執(zhí)行3
- 當(dāng)程序處于后臺收到推送消息,如果已經(jīng)在Background Modes 里面勾選了Remote notifications(推送喚醒)且推送消息中包含content-available 字段的話,都會執(zhí)行3,點(diǎn)擊通知欄iOS10 執(zhí)行4,非iOS執(zhí)行3.

