1. 程式人生 > >iOS開發系列--通知與訊息機制

iOS開發系列--通知與訊息機制

概述

在多數移動應用中任何時候都只能有一個應用程式處於活躍狀態,如果其他應用此刻發生了一些使用者感興趣的那麼通過通知機制就可以告訴使用者此時發生的事情。iOS中通知機制又叫訊息機制,其包括兩類:一類是本地通知;另一類是推送通知,也叫遠端通知。兩種通知在iOS中的表現一致,可以通過橫幅或者彈出提醒兩種形式告訴使用者,並且點選通知可以會開啟應用程式,但是實現原理卻完全不同。今天就和大家一塊去看一下如何在iOS中實現這兩種機制,並且在文章後面會補充通知中心的內容避免初學者對兩種概念的混淆。

本地通知

本地通知是由本地應用觸發的,它是基於時間行為的一種通知形式,例如鬧鐘定時、待辦事項提醒,又或者一個應用在一段時候後不使用通常會提示使用者使用此應用等都是本地通知。建立一個本地通知通常分為以下幾個步驟:

  1. 建立UILocalNotification。
  2. 設定處理通知的時間fireDate。
  3. 配置通知的內容:通知主體、通知聲音、圖示數字等。
  4. 配置通知傳遞的自定義資料引數userInfo(這一步可選)。
  5. 呼叫通知,可以使用scheduleLocalNotification:按計劃排程一個通知,也可以使用presentLocalNotificationNow立即呼叫通知。

下面就以一個程式更新後用戶長期沒有使用的提醒為例對本地通知做一個簡單的瞭解。在這個過程中並沒有牽扯太多的介面操作,所有的邏輯都在AppDelegate中:進入應用後如果沒有註冊通知,需要首先註冊通知請求使用者允許通知;一旦呼叫完註冊方法,無論使用者是否選擇允許通知此刻都會呼叫應用程式的- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings

代理方法,在這個方法中根據使用者的選擇:如果是允許通知則會按照前面的步驟建立通知並在一定時間後執行。

AppDelegate.m

//
//  AppDelegate.m
//  LocalNotification
//
//  Created by Kenshin Cui on 14/03/28.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "AppDelegate.h"
#import "KCMainViewController.h"

@interface AppDelegate ()

@end

@implementation AppDelegate

#pragma mark - 應用代理方法 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { _window=[[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds]; _window.backgroundColor =[UIColor colorWithRed:249/255.0 green:249/255.0 blue:249/255.0 alpha:1]; //設定全域性導航條風格和顏色 [[UINavigationBar appearance] setBarTintColor:[UIColor colorWithRed:23/255.0 green:180/255.0 blue:237/255.0 alpha:1]]; [[UINavigationBar appearance] setBarStyle:UIBarStyleBlack]; KCMainViewController *mainController=[[KCMainViewController alloc]init]; _window.rootViewController=mainController; [_window makeKeyAndVisible]; //如果已經獲得傳送通知的授權則建立本地通知,否則請求授權(注意:如果不請求授權在設定中是沒有對應的通知設定項的,也就是說如果從來沒有傳送過請求,即使通過設定也打不開訊息允許設定) if ([[UIApplication sharedApplication]currentUserNotificationSettings].types!=UIUserNotificationTypeNone) { [self addLocalNotification]; }else{ [[UIApplication sharedApplication]registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]]; } return YES; } #pragma mark 呼叫過使用者註冊通知方法之後執行(也就是呼叫完registerUserNotificationSettings:方法之後執行) -(void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings{ if (notificationSettings.types!=UIUserNotificationTypeNone) { [self addLocalNotification]; } } #pragma mark 進入前臺後設置訊息資訊 -(void)applicationWillEnterForeground:(UIApplication *)application{ [[UIApplication sharedApplication]setApplicationIconBadgeNumber:0];//進入前臺取消應用訊息圖示 } #pragma mark - 私有方法 #pragma mark 新增本地通知 -(void)addLocalNotification{ //定義本地通知物件 UILocalNotification *notification=[[UILocalNotification alloc]init]; //設定呼叫時間 notification.fireDate=[NSDate dateWithTimeIntervalSinceNow:10.0];//通知觸發的時間,10s以後 notification.repeatInterval=2;//通知重複次數 //notification.repeatCalendar=[NSCalendar currentCalendar];//當前日曆,使用前最好設定時區等資訊以便能夠自動同步時間 //設定通知屬性 [email protected]"最近添加了諸多有趣的特性,是否立即體驗?"; //通知主體 notification.applicationIconBadgeNumber=1;//應用程式圖示右上角顯示的訊息數 [email protected]"開啟應用"; //待機介面的滑動動作提示 [email protected]"Default";//通過點選通知開啟應用時的啟動圖片,這裡使用程式啟動圖片 //notification.soundName=UILocalNotificationDefaultSoundName;//收到通知時播放的聲音,預設訊息聲音 "msg.caf";//通知聲音(需要真機才能聽到聲音) //設定使用者資訊 [email protected]{@"id":@1,@"user":@"Kenshin Cui"};//繫結到通知上的其他附加資訊 //呼叫通知 [[UIApplication sharedApplication] scheduleLocalNotification:notification]; } #pragma mark 移除本地通知,在不需要此通知時記得移除 -(void)removeNotification{ [[UIApplication sharedApplication] cancelAllLocalNotifications]; } @end

請求獲得使用者允許通知的效果:

 LocalNotification_Request

應用退出到後彈出通知的效果:

 LocalNotification_Notification

鎖屏狀態下的通知效果(從這個介面可以看到alertAction配置為“開啟應用”):

LocalNotification_Lock

注意:

  • 在使用通知之前必須註冊通知型別,如果使用者不允許應用程式傳送通知,則以後就無法傳送通知,除非使用者手動到iOS設定中開啟通知。
  • 本地通知是有作業系統統一排程的,只有在應用退出到後臺或者關閉才能收到通知。(注意:這一點對於後面的推送通知也是完全適用的。 )
  • 通知的聲音是由iOS系統播放的,格式必須是Linear PCM、MA4(IMA/ADPCM)、µLaw、aLaw中的一種,並且播放時間必須在30s內,否則將被系統聲音替換,同時自定義聲音檔案必須放到main boundle中。
  • 本地通知的數量是有限制的,最近的本地通知最多隻能有64個,超過這個數量將被系統忽略。
  • 如果想要移除本地通知可以呼叫UIApplication的cancelLocalNotification:cancelAllLocalNotifications移除指定通知或所有通知。

從上面的程式可以看到userInfo這個屬性我們設定了引數,那麼這個引數如何接收呢?

在iOS中如果點選一個彈出通知(或者鎖屏介面滑動檢視通知),預設會自動開啟當前應用。由於通知由系統排程那麼此時進入應用有兩種情況:如果應用程式已經完全退出那麼此時會呼叫- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法;如果此時應用程式還在執行(無論是在前臺還是在後臺)則會呼叫-(void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification方法接收訊息引數。當然如果是後者自然不必多說,因為引數中已經可以拿到notification物件,只要讀取userInfo屬性即可。如果是前者的話則可以訪問launchOptions中鍵為UIApplicationLaunchOptionsLocalNotificationKey的物件,這個物件就是傳送的通知,由此物件再去訪問userInfo。為了演示這個過程在下面的程式中將userInfo的內容寫入檔案以便模擬關閉程式後再通過點選通知開啟應用獲取userInfo的過程。

AppDelegate.m

//
//  AppDelegate.m
//  LocalNotification
//
//  Created by Kenshin Cui on 14/03/28.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "AppDelegate.h"
#import "KCMainViewController.h"

@interface AppDelegate ()

@end

@implementation AppDelegate

#pragma mark - 應用代理方法
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    _window=[[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    
    _window.backgroundColor =[UIColor colorWithRed:249/255.0 green:249/255.0 blue:249/255.0 alpha:1];
    
    //設定全域性導航條風格和顏色
    [[UINavigationBar appearance] setBarTintColor:[UIColor colorWithRed:23/255.0 green:180/255.0 blue:237/255.0 alpha:1]];
    [[UINavigationBar appearance] setBarStyle:UIBarStyleBlack];
    
    KCMainViewController *mainController=[[KCMainViewController alloc]init];
    _window.rootViewController=mainController;
    
    [_window makeKeyAndVisible];

    //新增通知
    [self addLocalNotification];

    //接收通知引數
    UILocalNotification *notification=[launchOptions valueForKey:UIApplicationLaunchOptionsLocalNotificationKey];
    NSDictionary *userInfo= notification.userInfo;
    
    [userInfo writeToFile:@"/Users/kenshincui/Desktop/didFinishLaunchingWithOptions.txt" atomically:YES];
    NSLog(@"didFinishLaunchingWithOptions:The userInfo is %@.",userInfo);
    
    return YES;
}

#pragma mark 接收本地通知時觸發
-(void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification{
    NSDictionary *userInfo=notification.userInfo;
    [userInfo writeToFile:@"/Users/kenshincui/Desktop/didReceiveLocalNotification.txt" atomically:YES];
    NSLog(@"didReceiveLocalNotification:The userInfo is %@",userInfo);
}

#pragma mark 呼叫過使用者註冊通知方法之後執行(也就是呼叫完registerUserNotificationSettings:方法之後執行)
-(void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings{
    if (notificationSettings.types!=UIUserNotificationTypeNone) {
        [self addLocalNotification];
    }
}

#pragma mark 進入前臺後設置訊息資訊
-(void)applicationWillEnterForeground:(UIApplication *)application{
    [[UIApplication sharedApplication]setApplicationIconBadgeNumber:0];//進入前臺取消應用訊息圖示
}

#pragma mark - 私有方法
#pragma mark 新增本地通知
-(void)addLocalNotification{
    
    //定義本地通知物件
    UILocalNotification *notification=[[UILocalNotification alloc]init];
    //設定呼叫時間
    notification.fireDate=[NSDate dateWithTimeIntervalSinceNow:10.0];//通知觸發的時間,10s以後
    notification.repeatInterval=2;//通知重複次數
    //notification.repeatCalendar=[NSCalendar currentCalendar];//當前日曆,使用前最好設定時區等資訊以便能夠自動同步時間
    
    //設定通知屬性
    [email protected]"最近添加了諸多有趣的特性,是否立即體驗?"; //通知主體
    notification.applicationIconBadgeNumber=1;//應用程式圖示右上角顯示的訊息數
    [email protected]"開啟應用"; //待機介面的滑動動作提示
    [email protected]"Default";//通過點選通知開啟應用時的啟動圖片
    //notification.soundName=UILocalNotificationDefaultSoundName;//收到通知時播放的聲音,預設訊息聲音
    [email protected]"msg.caf";//通知聲音(需要真機)
    
    //設定使用者資訊
    [email protected]{@"id":@1,@"user":@"Kenshin Cui"};//繫結到通知上的其他額外資訊
    
    //呼叫通知
    [[UIApplication sharedApplication] scheduleLocalNotification:notification];
}
@end

上面的程式可以分為兩種情況去執行:一種是啟動程式關閉程式,等到接收到通知之後點選通知重新進入程式;另一種是啟動程式後,進入後臺(其實在前臺也可以,但是為了明顯的體驗這個過程建議進入後臺),接收到通知後點擊通知進入應用。另種情況會分別按照前面說的情況呼叫不同的方法接收到userInfo寫入本地檔案系統。有了userInfo一般來說就可以根據這個資訊進行一些處理,例如可以根據不同的引數資訊導航到不同的介面,假設是更新的通知則可以導航到更新內容介面等。

推送通知

和本地通知不同,推送通知是由應用服務提供商發起的,通過蘋果的APNs(Apple Push Notification Server)傳送到應用客戶端。下面是蘋果官方關於推送通知的過程示意圖:

PushNotification_FlowChart

推送通知的過程可以分為以下幾步:

  1. 應用服務提供商從伺服器端把要傳送的訊息和裝置令牌(device token)傳送給蘋果的訊息推送伺服器APNs。
  2. APNs根據裝置令牌在已註冊的裝置(iPhone、iPad、iTouch、mac等)查詢對應的裝置,將訊息傳送給相應的裝置。
  3. 客戶端裝置接將接收到的訊息傳遞給相應的應用程式,應用程式根據使用者設定彈出通知訊息。

當然,這只是一個簡單的流程,有了這個流程我們還無從下手編寫程式,將上面的流程細化可以得到如下流程圖(圖片來自網際網路),在這個過程中會也會提到如何在程式中完成這些步驟:

PushNotification_FlowChartDetail

1.應用程式註冊APNs推送訊息。

說明:

a.只有註冊過的應用才有可能接收到訊息,程式中通常通過UIApplication的registerUserNotificationSettings:方法註冊,iOS8中通知註冊的方法發生了改變,如果是iOS7及之前版本的iOS請參考其他程式碼。

b.註冊之前有兩個前提條件必須準備好:開發配置檔案(provisioning profile,也就是.mobileprovision字尾的檔案)的App ID不能使用通配ID必須使用指定APP ID並且生成配置檔案中選擇Push Notifications服務,一般的開發配置檔案無法完成註冊;應用程式的Bundle Identifier必須和生成配置檔案使用的APP ID完全一致。

2.iOS從APNs接收device token,在應用程式獲取device token。

說明:

a.在UIApplication的-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken代理方法中獲取令牌,此方法發生在註冊之後。

b.如果無法正確獲得device token可以在UIApplication的-(void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error代理方法中檢視詳細錯誤資訊,此方法發生在獲取device token失敗之後。

c.必須真機除錯,模擬器無法獲取device token。

3.iOS應用將device token傳送給應用程式提供商,告訴伺服器端當前裝置允許接收訊息。

說明:

a.device token的生成演算法只有Apple掌握,為了確保演算法發生變化後仍然能夠正常接收伺服器端傳送的通知,每次應用程式啟動都重新獲得device token(注意:device token的獲取不會造成效能問題,蘋果官方已經做過優化)。

b.通常可以建立一個網路連線傳送給應用程式提供商的伺服器端, 在這個過程中最好將上一次獲得的device token儲存起來,避免重複傳送,一旦發現device token發生了變化最好將原有的device token一塊傳送給伺服器端,伺服器端刪除原有令牌儲存新令牌避免伺服器端傳送無效訊息。

4.應用程式提供商在伺服器端根據前面傳送過來的device token組織資訊傳送給APNs。

說明:

a.傳送時指定device token和訊息內容,並且完全按照蘋果官方的訊息格式組織訊息內容,通常情況下可以藉助其他第三方訊息推送框架來完成。

5.APNs根據訊息中的device token查詢已註冊的裝置推送訊息。

說明:

a.正常情況下可以根據device token將訊息成功推送到客戶端裝置中,但是也不排除使用者解除安裝程式的情況,此時推送訊息失敗,APNs會將這個錯誤訊息通知伺服器端以避免資源浪費(伺服器端此時可以根據錯誤刪除已經儲存的device token,下次不再發送)。

下面將簡單演示一下推送通知的簡單流程:

首先,看一下iOS客戶端程式碼:

//
//  AppDelegate.m
//  pushnotification
//
//  Created by Kenshin Cui on 14/03/27.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "AppDelegate.h"
#import "KCMainViewController.h"

@interface AppDelegate ()

@end

@implementation AppDelegate

#pragma mark - 應用程式代理方法
#pragma mark 應用程式啟動之後
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    _window=[[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    
    _window.backgroundColor =[UIColor colorWithRed:249/255.0 green:249/255.0 blue:249/255.0 alpha:1];
    
    //設定全域性導航條風格和顏色
    [[UINavigationBar appearance] setBarTintColor:[UIColor colorWithRed:23/255.0 green:180/255.0 blue:237/255.0 alpha:1]];
    [[UINavigationBar appearance] setBarStyle:UIBarStyleBlack];
    
    KCMainViewController *mainController=[[KCMainViewController alloc]init];
    _window.rootViewController=mainController;
    
    [_window makeKeyAndVisible];
    
    //註冊推送通知(注意iOS8註冊方法發生了變化)
    [application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]];
    [application registerForRemoteNotifications];
    
    return YES;
}
#pragma mark 註冊推送通知之後
//在此接收裝置令牌
-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{
    [self addDeviceToken:deviceToken];
    NSLog(@"device token:%@",deviceToken);
}

#pragma mark 獲取device token失敗後
-(void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{
    NSLog(@"didFailToRegisterForRemoteNotificationsWithError:%@",error.localizedDescription);
    [self addDeviceToken:nil];
}

#pragma mark 接收到推送通知之後
-(void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo{
    NSLog(@"receiveRemoteNotification,userInfo is %@",userInfo);
}

#pragma mark - 私有方法
/**
 *  新增裝置令牌到伺服器端
 *
 *  @param deviceToken 裝置令牌
 */
-(void)addDeviceToken:(NSData *)deviceToken{
    NSString *[email protected]"DeviceToken";
    NSData *oldToken= [[NSUserDefaults standardUserDefaults]objectForKey:key];
    //如果偏好設定中的已儲存裝置令牌和新獲取的令牌不同則儲存新令牌並且傳送給伺服器端
    if (![oldToken isEqualToData:deviceToken]) {
        [[NSUserDefaults standardUserDefaults] setObject:deviceToken forKey:key];
        [self sendDeviceTokenWidthOldDeviceToken:oldToken newDeviceToken:deviceToken];
    }
}

-(void)sendDeviceTokenWidthOldDeviceToken:(NSData *)oldToken newDeviceToken:(NSData *)newToken{
    //注意一定確保真機可以正常訪問下面的地址
    NSString *[email protected]"http://192.168.1.101/RegisterDeviceToken.aspx";
    urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSURL *url=[NSURL URLWithString:urlStr];
    NSMutableURLRequest *requestM=[NSMutableURLRequest requestWithURL:url cachePolicy:0 timeoutInterval:10.0];
    [requestM setHTTPMethod:@"POST"];
    NSString *bodyStr=[NSString stringWithFormat:@"oldToken=%@&newToken=%@",oldToken,newToken];
    NSData *body=[bodyStr dataUsingEncoding:NSUTF8StringEncoding];
    [requestM setHTTPBody:body];
    NSURLSession *session=[NSURLSession sharedSession];
    NSURLSessionDataTask *dataTask= [session dataTaskWithRequest:requestM completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (error) {
            NSLog(@"Send failure,error is :%@",error.localizedDescription);
        }else{
            NSLog(@"Send Success!");
        }
        
    }];
    [dataTask resume];
}
@end

iOS客戶端程式碼的程式碼比較簡單,註冊推送通知,獲取device token儲存到偏好設定中,並且如果新獲取的device token不同於偏好設定中儲存的資料則傳送給伺服器端,更新伺服器端device token列表。

其次,由於device token需要傳送給伺服器端,這裡使用一個Web應用作為伺服器端接收device token,這裡使用了ASP.NET程式來處理令牌接收註冊工作,當然你使用其他技術同樣沒有問題。下面是對應的後臺程式碼:

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using CMJ.Framework.Data;

namespace WebServer
{
    public partial class RegisterDeviceToken : System.Web.UI.Page
    {
        private string _appID = @"com.cmjstudio.pushnotification";
        private SqlHelper _helper = new SqlHelper();
        protected void Page_Load(object sender, EventArgs e)
        {
            try
            {
                string oldToken = Request["oldToken"] + "";
                string newToken = Request["newToken"] + "";
                string sql = "";
                //如果傳遞舊的裝置令牌則刪除舊令牌新增新令牌
                if (oldToken != "")
                {
                    sql = string.Format("DELETE FROM dbo.Device WHERE AppID='{0}' AND DeviceToken='{1}';", _appID, oldToken);
                }
                sql += string.Format(@"IF NOT EXISTS (SELECT ID FROM dbo.Device WHERE AppID='{0}' AND DeviceToken='{1}')
                                        INSERT INTO dbo.Device ( AppID, DeviceToken ) VALUES ( N'{0}', N'{1}');", _appID, newToken);
                _helper.ExecuteNonQuery(sql);
                Response.Write("註冊成功!");
            }
            catch(Exception ex)
            {
                Response.Write("註冊失敗,錯誤詳情:"+ex.ToString());
            }
        }
    }
}

這個過程主要就是儲存device token到資料庫中,當然如果同時傳遞舊的裝置令牌還需要先刪除就的裝置令牌,這裡簡單的在資料庫中建立了一張Device表來儲存裝置令牌,其中記錄了應用程式Id和裝置令牌。

第三步就是伺服器端傳送訊息,如果要給APNs傳送訊息就必須按照Apple的標準訊息格式組織訊息內容。但是好在目前已經有很多開源的第三方類庫供我們使用,具體訊息如何包裝完全不用自己組織,這裡使用一個開源的類庫Push Sharp來給APNs傳送訊息 ,除了可以給Apple裝置推送訊息,Push Sharp還支援Android、Windows Phone等多種裝置,更多詳細內容大家可以參照官方說明。前面說過如果要開發訊息推送應用不能使用一般的開發配置檔案,這裡還需要注意:如果伺服器端要給APNs傳送訊息其祕鑰也必須是通過APNs Development iOS型別的證書來匯出的,一般的iOS Development 型別的證書匯出的祕鑰無法用作伺服器端傳送祕鑰。下面通過在一個簡單的WinForm程式中呼叫Push Sharp給APNs傳送訊息,這裡讀取之前Device表中的所有裝置令牌迴圈傳送訊息:

using System;
using System.IO;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using PushSharp;
using PushSharp.Apple;
using CMJ.Framework.Data;
using CMJ.Framework.Logging;
using CMJ.Framework.Windows.Forms;

namespace PushNotificationServer
{
    public partial class frmMain : PersonalizeForm
    {
        private string _appID = @"com.cmjstudio.pushnotification";
        private SqlHelper _helper = new SqlHelper();
        public frmMain()
        {
            InitializeComponent();
        }

        private void btnClose_Click(object sender, EventArgs e)
        {
            this.Close();
        }

        private void btnSend_Click(object sender, EventArgs e)
        {
            List<string> deviceTokens = GetDeviceToken();
            SendMessage(deviceTokens, tbMessage.Text);
        }

        #region 傳送訊息
        /// <summary>
        /// 取得所有裝置令牌
        /// </summary>
        /// <returns>裝置令牌</returns>
        private List<string> GetDeviceToken()
        {
            List<string> deviceTokens = new List<string>();
            string sql = string.Format("SELECT DeviceToken FROM dbo.Device WHERE AppID='{0}'",_appID);
            DataTable dt = _helper.GetDataTable(sql);
            if(dt.Rows.Count>0)
            {
                foreach(DataRow dr in dt.Rows)
                {
                    deviceTokens.Add((dr["DeviceToken"]+"").TrimStart('<').TrimEnd('>').Replace(" ",""));
                }
            }
            return deviceTokens;
        }
        
        /// <summary>
        /// 傳送訊息
        /// </summary>
        /// <param name="deviceToken">裝置令牌</param>
        /// <param name="message">訊息內容</param>
        private void SendMessage(List<string> deviceToken, string message)
        {
            //建立推送物件
            var pusher = new PushBroker();
            pusher.OnNotificationSent += pusher_OnNotificationSent;//傳送成功事件
            pusher.OnNotificationFailed += pusher_OnNotificationFailed;//傳送失敗事件
            pusher.OnChannelCreated += pusher_OnChannelCreated;
            pusher.OnChannelDestroyed += pusher_OnChannelDestroyed;
            pusher.OnChannelException += pusher_OnChannelException;
            pusher.OnDeviceSubscriptionChanged += pusher_OnDeviceSubscriptionChanged;
            pusher.OnDeviceSubscriptionExpired += pusher_OnDeviceSubscriptionExpired;
            pusher.OnNotificationRequeue += pusher_OnNotificationRequeue;
            pusher.OnServiceException += pusher_OnServiceException;
            //註冊推送服務
            byte[] certificateData = File.ReadAllBytes(@"E:\KenshinCui_Push.p12");
            pusher.RegisterAppleService(new ApplePushChannelSettings(certificateData, "123"));
            foreach (string token in deviceToken)
            {
                //給指定裝置傳送訊息
                pusher.QueueNotification(new AppleNotification()
                    .ForDeviceToken(token)
                    .WithAlert(message) 
                    .WithBadge(1)
                    .WithSound("default"));
            }
        }

        void pusher_OnServiceException(object sender, Exception error)
        {
            Console.WriteLine("訊息傳送失敗,錯誤詳情:" + error.ToString());
            PersonalizeMessageBox.Show(this, "訊息傳送失敗,錯誤詳情:" + error.ToString(), "系統提示");
        }

        void pusher_OnNotificationRequeue(object sender, PushSharp.Core.NotificationRequeueEventArgs e)
        {
            Console.WriteLine("pusher_OnNotificationRequeue");
        }

        void pusher_OnDeviceSubscriptionExpired(object sender, string expiredSubscriptionId, DateTime expirationDateUtc, PushSharp.Core.INotification notification)
        {
            Console.WriteLine("pusher_OnDeviceSubscriptionChanged");
        }

        void pusher_OnDeviceSubscriptionChanged(object sender, string oldSubscriptionId, string newSubscriptionId, PushSharp.Core.INotification notification)
        {
            Console.WriteLine("pusher_OnDeviceSubscriptionChanged");
        }

        void pusher_OnChannelException(object sender, PushSharp.Core.IPushChannel pushChannel, Exception error)
        {
            Console.WriteLine("訊息傳送失敗,錯誤詳情:" + error.ToString());
            PersonalizeMessageBox.Show(this, "訊息傳送失敗,錯誤詳情:" + error.ToString(), "系統提示");
        }

        void pusher_OnChannelDestroyed(object sender)
        {
            Console.WriteLine("pusher_OnChannelDestroyed");
        }

        void pusher_OnChannelCreated(object sender, PushSharp.Core.IPushChannel pushChannel)
        {
            Console.WriteLine("pusher_OnChannelCreated");
        }

        void pusher_OnNotificationFailed(object sender, PushSharp.Core.INotification notification, Exception error)
        {
            Console.WriteLine("訊息傳送失敗,錯誤詳情:" + error.ToString());
            PersonalizeMessageBox.Show(this, "訊息傳送失敗,錯誤詳情:"+error.ToString(), "系統提示");
        }

        void pusher_OnNotificationSent(object sender, PushSharp.Core.INotification notification)
        {
            Console.WriteLine("訊息傳送成功!");
            PersonalizeMessageBox.Show(this, "訊息傳送成功!", "系統提示");
        }
        #endregion
    }
}

伺服器端訊息傳送應用執行效果:

PushNotification_ServerClient

iOS客戶端接收的訊息的效果:

PushNotification_iOSApp

到目前為止通過伺服器端應用可以順利傳送訊息給APNs並且iOS應用已經成功接收推送訊息。

補充--iOS開發證書、祕鑰

iOS開發過程中如果需要進行真機除錯、釋出需要註冊申請很多證書,對於初學者往往迷惑不解,再加上今天的文章中會牽扯到一些特殊配置,這裡就簡單的對iOS開發的常用證書和祕鑰等做一說明。

證書

iOS常用的證書包括開發證書和釋出證書,無論是真機除錯還是最終釋出應用到App Store這兩個證書都是必須的,它是iOS開發的基本證書。

a.開發證書:開發證書又分為普通開發證書和推送證書,如果僅僅是一般的應用則前者即可滿足,但是如果開發推送應用則必須使用推送證書。

b.釋出證書:釋出證書又可以分為普通釋出證書、推送證書、Pass Type ID證書、站點發布證書、VoIP服務證書、蘋果支付證書。同樣的,對於需要使用特殊服務的應用則必須選擇對應的證書。

應用標識

App ID,應用程式的唯一標識,對應iOS應用的Bundle Identifier,App ID在蘋果開發者中心中分為通配應用ID和明確的應用ID,前者一般用於普通應用開發,一個ID可以適用於多個不同標識的應用;但是對於使用訊息推送、Passbook、站點發布、iCloud等服務的應用必須配置明確的應用ID。

裝置標識

UDID,用於標識每一臺硬體裝置的標示符。注意它不是device token,device token是根據UDID使用一個只有Apple自己才知道的演算法生成的一組標示符。

配置簡介

Provisioning Profiles,平時又稱為PP檔案。將UDID、App ID、開發證書打包在一起的配置檔案,同樣分為開發和釋出兩類配置檔案。

祕鑰

在申請開發證書時必須要首先提交一個祕鑰請求檔案,對於生成祕鑰請求檔案的mac,如果要做開發則只需要下載證書和配置簡介即可開發。但是如果要想在其他機器上做開發則必須將證書中的祕鑰匯出(匯出之後是一個.p12檔案),然後匯入其他機器。同時對於類似於推送伺服器端應用如果要給APNs傳送訊息,同樣需要使用.p12祕鑰檔案,並且這個祕鑰檔案需要是推送證書匯出的對應祕鑰。

補充--通知中心

對於很多初學者往往會把iOS中的本地通知、推送通知和iOS通知中心的概念弄混。其實二者之間並沒有任何關係,事實上它們都不屬於一個框架,前者屬於UIKit框架,後者屬於Foundation框架。

通知中心實際上是iOS程式內部之間的一種訊息廣播機制,主要為了解決應用程式內部不同物件之間解耦而設計。它是基於觀察者模式設計的,不能跨應用程式程序通訊,當通知中心接收到訊息之後會根據內部的訊息轉發表,將訊息傳送給訂閱者。下面是一個簡單的流程示意圖:

image

瞭解通知中心需要熟悉NSNotificationCenter和NSNotification兩個類:

NSNotificationCenter:是通知系統的中心,用於註冊和傳送通知,下表列出常用的方法。

方法 說明
- (void)addObserver:(id)observer selector:(SEL)aSelector