原文地址:http://www.cocoachina.com/ios/20150318/11364.html
概述
在多數移動應用中任何時候都只能有一個應用程序處於活躍狀態,如果其他應用此刻發生了一些用戶感興趣的那麼通過通知機制就可以告訴用戶此時發生的事情。iOS中通知機制又叫消息機制,其包括兩類:一類是本地通知;另一類是推送通知,也叫遠程通知。兩種通知在iOS中的表現一致,可以通過橫幅或者彈出提醒兩種形式告訴用戶,並且點擊通知可以會打開應用程序,但是實現原理卻完全不同。今天就和大家一塊去看一下如何在iOS中實現這兩種機制,並且在文章後面會補充通知中心的內容避免初學者對兩種概念的混淆。
本文包括下面內容
-
本地通知
-
推送通知
-
補充--iOS開發證書、祕鑰
-
補充--通知中心
本地通知
本地通知是由本地應用觸發的,它是基於時間行爲的一種通知形式,例如鬧鐘定時、待辦事項提醒,又或者一個應用在一段時候後不使用通常會提示用戶使用此應用等都是本地通知。創建一個本地通知通常分爲以下幾個步驟:
-
創建UILocalNotification。
-
設置處理通知的時間fireDate。
-
配置通知的內容:通知主體、通知聲音、圖標數字等。
-
配置通知傳遞的自定義數據參數userInfo(這一步可選)。
-
調用通知,可以使用scheduleLocalNotification:按計劃調度一個通知,也可以使用presentLocalNotificationNow立即調用通知。
下面就以一個程序更新後用戶長期沒有使用的提醒爲例對本地通知做一個簡單的瞭解。在這個過程中並沒有牽扯太多的界面操作,所有的邏輯都在AppDelegate中:進入應用後如果沒有註冊通知,需要首先註冊通知請求用戶允許通知;一旦調用完註冊方法,無論用戶是否選擇允許通知此刻都會調用應用程序的- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings代理方法,在這個方法中根據用戶的選擇:如果是允許通知則會按照前面的步驟創建通知並在一定時間後執行。
AppDelegate.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
// // 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];//當前日曆,使用前最好設置時區等信息以便能夠自動同步時間 //設置通知屬性 notification.alertBody=@ "最近添加了諸多有趣的特性,是否立即體驗?" ; //通知主體 notification.applicationIconBadgeNumber=1; //應用程序圖標右上角顯示的消息數 notification.alertAction=@ "打開應用" ; //待機界面的滑動動作提示 notification.alertLaunchImage=@ "Default" ; //通過點擊通知打開應用時的啓動圖片,這裏使用程序啓動圖片 //notification.soundName=UILocalNotificationDefaultSoundName;//收到通知時播放的聲音,默認消息聲音 notification.soundName=@ "msg.caf" ; //通知聲音(需要真機才能聽到聲音) //設置用戶信息 notification.userInfo=@{@ "id" :@1,@ "user" :@ "Kenshin Cui" }; //綁定到通知上的其他附加信息 //調用通知 [[UIApplication sharedApplication] scheduleLocalNotification:notification]; } #pragma mark 移除本地通知,在不需要此通知時記得移除 -(void)removeNotification{ [[UIApplication sharedApplication] cancelAllLocalNotifications]; } @end |
請求獲得用戶允許通知的效果:
應用退出到後彈出通知的效果:
鎖屏狀態下的通知效果(從這個界面可以看到alertAction配置爲“打開應用”):
注意:
-
在使用通知之前必須註冊通知類型,如果用戶不允許應用程序發送通知,則以後就無法發送通知,除非用戶手動到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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
// // 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];//當前日曆,使用前最好設置時區等信息以便能夠自動同步時間 //設置通知屬性 notification.alertBody=@ "最近添加了諸多有趣的特性,是否立即體驗?" ; //通知主體 notification.applicationIconBadgeNumber=1; //應用程序圖標右上角顯示的消息數 notification.alertAction=@ "打開應用" ; //待機界面的滑動動作提示 notification.alertLaunchImage=@ "Default" ; //通過點擊通知打開應用時的啓動圖片 //notification.soundName=UILocalNotificationDefaultSoundName;//收到通知時播放的聲音,默認消息聲音 notification.soundName=@ "msg.caf" ; //通知聲音(需要真機) //設置用戶信息 notification.userInfo=@{@ "id" :@1,@ "user" :@ "Kenshin Cui" }; //綁定到通知上的其他額外信息 //調用通知 [[UIApplication sharedApplication] scheduleLocalNotification:notification]; } @end |
上面的程序可以分爲兩種情況去運行:一種是啓動程序關閉程序,等到接收到通知之後點擊通知重新進入程序;另一種是啓動程序後,進入後臺(其實在前臺也可以,但是爲了明顯的體驗這個過程建議進入後臺),接收到通知後點擊通知進入應用。另種情況會分別按照前面說的情況調用不同的方法接收到userInfo寫入本地文件系統。有了userInfo一般來說就可以根據這個信息進行一些處理,例如可以根據不同的參數信息導航到不同的界面,假設是更新的通知則可以導航到更新內容界面等。
推送通知
和本地通知不同,推送通知是由應用服務提供商發起的,通過蘋果的APNs(Apple Push Notification Server)發送到應用客戶端。下面是蘋果官方關於推送通知的過程示意圖:
推送通知的過程可以分爲以下幾步:
-
應用服務提供商從服務器端把要發送的消息和設備令牌(device token)發送給蘋果的消息推送服務器APNs。
-
APNs根據設備令牌在已註冊的設備(iPhone、iPad、iTouch、mac等)查找對應的設備,將消息發送給相應的設備。
-
客戶端設備接將接收到的消息傳遞給相應的應用程序,應用程序根據用戶設置彈出通知消息。
當然,這只是一個簡單的流程,有了這個流程我們還無從下手編寫程序,將上面的流程細化可以得到如下流程圖(圖片來自互聯網),在這個過程中會也會提到如何在程序中完成這些步驟:
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客戶端代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
// // 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 *key=@ "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{ //注意一定確保真機可以正常訪問下面的地址 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程序來處理令牌接收註冊工作,當然你使用其他技術同樣沒有問題。下面是對應的後臺代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
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表中的所有設備令牌循環發送消息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
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 deviceTokens = GetDeviceToken(); SendMessage(deviceTokens, tbMessage.Text); } #region 發送消息 /// /// 取得所有設備令牌 /// /// 設備令牌 private List GetDeviceToken() { List deviceTokens = new List(); 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( '' ).Replace( " " , "" )); } } return deviceTokens; } /// /// 發送消息 /// /// 設備令牌 /// 消息內容 private void SendMessage(List 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 } } |
服務器端消息發送應用運行效果:
iOS客戶端接收的消息的效果:
到目前爲止通過服務器端應用可以順利發送消息給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程序內部之間的一種消息廣播機制,主要爲了解決應用程序內部不同對象之間解耦而設計。它是基於觀察者模式設計的,不能跨應用程序進程通信,當通知中心接收到消息之後會根據內部的消息轉發表,將消息發送給訂閱者。下面是一個簡單的流程示意圖:
瞭解通知中心需要熟悉NSNotificationCenter和NSNotification兩個類:
NSNotificationCenter:是通知系統的中心,用於註冊和發送通知,下表列出常用的方法。
NSNotification:代表通知內容的載體,主要有三個屬性:name代表通知名稱,object代表通知的發送者,userInfo代表通知的附加信息。
雖然前面的文章中從未提到過通知中心,但是其實通知中心我們並不陌生,前面文章中很多內容都是通過通知中心來進行應用中各個組件通信的,只是沒有單獨拿出來說而已。例如前面的文章中討論的應用程序生命週期問題,當應用程序啓動後、進入後臺、進入前臺、獲得焦點、失去焦點,窗口大小改變、隱藏等都會發送通知。這個通知可以通過前面NSNotificationCenter進行訂閱即可接收對應的消息,下面的示例演示瞭如何添加監聽獲得UIApplication的進入後臺和獲得焦點的通知:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
// // KCMainViewController.m // NotificationCenter // // Created by Kenshin Cui on 14/03/27. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "KCMainViewController.h" @interface KCMainViewController () @end @implementation KCMainViewController - (void)viewDidLoad { [ super viewDidLoad]; [self addObserverToNotificationCenter]; } #pragma mark 添加監聽 -(void)addObserverToNotificationCenter{ /*添加應用程序進入後臺監聽 * observer:監聽者 * selector:監聽方法(監聽者監聽到通知後執行的方法) * name:監聽的通知名稱(下面的UIApplicationDidEnterBackgroundNotification是一個常量) * object:通知的發送者(如果指定nil則監聽任何對象發送的通知) */ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:[UIApplication sharedApplication]]; /* 添加應用程序獲得焦點的通知監聽 * name:監聽的通知名稱 * object:通知的發送者(如果指定nil則監聽任何對象發送的通知) * queue:操作隊列,如果制定非主隊線程隊列則可以異步執行block * block:監聽到通知後執行的操作 */ NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init]; [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification object:[UIApplication sharedApplication] queue:operationQueue usingBlock:^(NSNotification *note) { NSLog(@ "Application become active." ); }]; } #pragma mark 應用程序啓動監聽方法 -(void)applicationEnterBackground{ NSLog(@ "Application enter background." ); } @end |
當然很多時候使用通知中心是爲了添加自定義通知,並獲得自定義通知消息。在前面的文章“iOS開發系列--視圖切換”中提到過如何進行多視圖之間參數傳遞,其實利用自定義通知也可以進行參數傳遞。通常一個應用登錄後會顯示用戶信息,而登錄信息可以通過登錄界面獲取。下面就以這樣一種場景爲例,在主界面中添加監聽,在登錄界面發送通知,一旦登錄成功將向通知中心發送成功登錄的通知,此時主界面中由於已經添加通知監聽所以會收到通知並更新UI界面。
主界面KCMainViewController.m:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
// // KCMainViewController.m // NotificationCenter // // Created by Kenshin Cui on 14/03/27 // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "KCMainViewController.h" #import "KCLoginViewController.h" #define UPDATE_LGOGIN_INFO_NOTIFICATION @"updateLoginInfo" @interface KCMainViewController (){ UILabel *_lbLoginInfo; UIButton *_btnLogin; } @end @implementation KCMainViewController - (void)viewDidLoad { [ super viewDidLoad]; [self setupUI]; } -(void)setupUI{ UILabel *label =[[UILabel alloc]initWithFrame:CGRectMake(0, 100,320 ,30)]; label.textAlignment=NSTextAlignmentCenter; label.textColor=[UIColor colorWithRed:23/255.0 green:180/255.0 blue:237/255.0 alpha:1]; _lbLoginInfo=label; [self.view addSubview:label]; UIButton *button=[UIButton buttonWithType:UIButtonTypeSystem]; button.frame=CGRectMake(60, 200, 200, 25); [button setTitle:@ "登錄" forState:UIControlStateNormal]; [button addTarget:self action:@selector(loginOut) forControlEvents:UIControlEventTouchUpInside]; _btnLogin=button; [self.view addSubview:button]; } -(void)loginOut{ //添加監聽 [self addObserverToNotification]; KCLoginViewController *loginController=[[KCLoginViewController alloc]init]; [self presentViewController:loginController animated:YES completion:nil]; } /** * 添加監聽 */ -(void)addObserverToNotification{ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateLoginInfo:) name:UPDATE_LGOGIN_INFO_NOTIFICATION object:nil]; } /** * 更新登錄信息,注意在這裏可以獲得通知對象並且讀取附加信息 */ -(void)updateLoginInfo:(NSNotification *)notification{ NSDictionary *userInfo=notification.userInfo; _lbLoginInfo.text=userInfo[@ "loginInfo" ]; _btnLogin.titleLabel.text=@ "註銷" ; } -(void)dealloc{ //移除監聽 [[NSNotificationCenter defaultCenter] removeObserver:self]; } @end |
登錄界面KCLoginViewController.m:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
// // KCLoginViewController.m // NotificationCenter // // Created by Kenshin Cui on 14/03/27. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "KCLoginViewController.h" #define UPDATE_LGOGIN_INFO_NOTIFICATION @"updateLoginInfo" @interface KCLoginViewController (){ UITextField *_txtUserName; UITextField *_txtPassword; } @end @implementation KCLoginViewController - (void)viewDidLoad { [ super viewDidLoad]; [self setupUI]; } /** * UI佈局 */ -(void)setupUI{ //用戶名 UILabel *lbUserName=[[UILabel alloc]initWithFrame:CGRectMake(50, 150, 100, 30)]; lbUserName.text=@ "用戶名:" ; [self.view addSubview:lbUserName]; _txtUserName=[[UITextField alloc]initWithFrame:CGRectMake(120, 150, 150, 30)]; _txtUserName.borderStyle=UITextBorderStyleRoundedRect; [self.view addSubview:_txtUserName]; //密碼 UILabel *lbPassword=[[UILabel alloc]initWithFrame:CGRectMake(50, 200, 100, 30)]; lbPassword.text=@ "密碼:" ; [self.view addSubview:lbPassword]; _txtPassword=[[UITextField alloc]initWithFrame:CGRectMake(120, 200, 150, 30)]; _txtPassword.secureTextEntry=YES; _txtPassword.borderStyle=UITextBorderStyleRoundedRect; [self.view addSubview:_txtPassword]; //登錄按鈕 UIButton *btnLogin=[UIButton buttonWithType:UIButtonTypeSystem]; btnLogin.frame=CGRectMake(70, 270, 80, 30); [btnLogin setTitle:@ "登錄" forState:UIControlStateNormal]; [self.view addSubview:btnLogin]; [btnLogin addTarget:self action:@selector(login) forControlEvents:UIControlEventTouchUpInside]; //取消登錄按鈕 UIButton *btnCancel=[UIButton buttonWithType:UIButtonTypeSystem]; btnCancel.frame=CGRectMake(170, 270, 80, 30); [btnCancel setTitle:@ "取消" forState:UIControlStateNormal]; [self.view addSubview:btnCancel]; [btnCancel addTarget:self action:@selector(cancel) forControlEvents:UIControlEventTouchUpInside]; } #pragma mark 登錄操作 -(void)login{ if ([_txtUserName.text isEqualToString:@ "kenshincui" ] && [_txtPassword.text isEqualToString:@ "123" ] ) { //發送通知 [self postNotification]; [self dismissViewControllerAnimated:YES completion:nil]; } else { //登錄失敗彈出提示信息 UIAlertView *alertView=[[UIAlertView alloc]initWithTitle:@ "系統信息" message:@ "用戶名或密碼錯誤,請重新輸入!" delegate:nil cancelButtonTitle:@ "取消" otherButtonTitles:nil]; [alertView show]; } } #pragma mark 點擊取消 -(void)cancel{ [self dismissViewControllerAnimated:YES completion:nil]; } /** * 添加通知,注意這裏設置了附加信息 */ -(void)postNotification{ NSDictionary *userInfo=@{@ "loginInfo" :[NSString stringWithFormat:@ "Hello,%@!" ,_txtUserName.text]}; NSLog(@ "%@" ,userInfo); NSNotification *notification=[NSNotification notificationWithName:UPDATE_LGOGIN_INFO_NOTIFICATION object:self userInfo:userInfo]; [[NSNotificationCenter defaultCenter] postNotification:notification]; //也可直接採用下面的方法 // [[NSNotificationCenter defaultCenter] postNotificationName:UPDATE_LGOGIN_INFO_NOTIFICATION object:self userInfo:userInfo]; } @end |
運行效果:
注意:
通過上面的介紹大家應該可以發現其實通知中心是一種低耦合設計,和前面文章中提到的代理模式有異曲同工之妙。相對於後者而言,通知中心可以將一個通知發送給多個監聽者,而每個對象的代理卻只能有一個。當然代理也有其優點,例如使用代理代碼分佈結構更加清晰,它不像通知一樣隨處都可以添加訂閱等,實際使用過程中需要根據實際情況而定。