最近在做远程推送的时候,发现同一台设备,会收到同一条消息若干条,通过了解推送原理,得知我们在AppDelegate里面注册远程通知的时候,苹果服务器会给我们返回一个deviceToken。
-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
一般用户在登录成功后会将苹果服务器返回的deviceToken传到我们自己的服务器上面,以便我们给指定的用户推送消息。
在iOS7.0和iOS8.0的系统中,卸载重装应用,deviceToken不会发生变化,但是iOS9.0(包括)以后卸载重装应用,deviceToken会发生变化。此时需要保证上传到服务器里面的deviceToken是唯一的,就是一个用户下,一个设备仅仅保存一个deviceToken。每当这个设备的deviceToken发生变化时,就替换此设备对应的deviceToken。
采用UUID+keychain+DeviceToken来解决此问题。当软件第一次安装时候,获取设备的UUID存储到keychain中,那么只要你不刷机,保存在keychain中的UUID一直存在,即使升级操作系统也会存在。这样就能保证deviceToken的唯一性。向我们的服务器上传deviceToken时,将UUID和deviceToken一起上传。后台在保存deviceToken时候通过UUID校验,如果deviceToken变化了,就替换原来的deviceToken。
获取到UUID后,如果用NSUserDefaults存储,当程序被卸载后重装时,再获得的UUID和之前就不同了。使用keychain存储可以保证程序卸载重装时,UUID不变。但当刷机后,UUID还是会改变的。但这仍是目前为止最佳的解决办法了,如果有更好的解决办法,欢迎留言。
Target - Capabilities - Keychain Sharing 改为ON
注意:Bundle Identifier、Keychain Sharing的Keychain Groups、Entitlements文件的Keychain Access Groups的第一个元素,它们要保持上图所示的一致性。
#import <Foundation/Foundation.h>
@interface UuidObject : NSObject
+(NSString *)getUUID;
@end
#import "UuidObject.h"
#import "KeyChainStore.h"
@implementation UuidObject
+(NSString *)getUUID
{
NSString * strUUID = (NSString *)[KeyChainStore load:@"com.cloud.app"];
//首次执行该方法时,uuid为空
if ([strUUID isEqualToString:@""] || !strUUID)
{
//生成一个uuid的方法
CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault);
strUUID = (NSString *)CFBridgingRelease(CFUUIDCreateString (kCFAllocatorDefault,uuidRef));
//将该uuid保存到keychain
[KeyChainStore save:KEY_USERNAME_PASSWORD data:strUUID];
}
return strUUID;
}
#import <Foundation/Foundation.h>
@interface KeyChainStore : NSObject
+ (void)save:(NSString *)service data:(id)data;
+ (id)load:(NSString *)service;
+ (void)deleteKeyData:(NSString *)service;
@end
#import "KeyChainStore.h"
@implementation KeyChainStore
+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service {
return [NSMutableDictionary dictionaryWithObjectsAndKeys:
(id)kSecClassGenericPassword,(id)kSecClass,
service, (id)kSecAttrService,
service, (id)kSecAttrAccount,
(id)kSecAttrAccessibleAfterFirstUnlock,(id)kSecAttrAccessible,
nil];
}
+ (void)save:(NSString *)service data:(id)data {
//Get search dictionary
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
//Delete old item before add new item
SecItemDelete((CFDictionaryRef)keychainQuery);
//Add new object to search dictionary(Attention:the data format)
[keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
//Add item to keychain with the search dictionary
SecItemAdd((CFDictionaryRef)keychainQuery, NULL);
}
+ (id)load:(NSString *)service {
id ret = nil;
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
//Configure the search setting
//Since in our simple case we are expecting only a single attribute to be returned (the password) we can set the attribute kSecReturnData to kCFBooleanTrue
[keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
[keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
CFDataRef keyData = NULL;
if (SecItemCopyMatching((CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr) {
@try {
ret = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)keyData];
} @catch (NSException *e) {
NSLog(@"Unarchive of %@ failed: %@", service, e);
} @finally {
}
}
if (keyData)
CFRelease(keyData);
return ret;
}
+ (void)deleteKeyData:(NSString *)service {
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
SecItemDelete((CFDictionaryRef)keychainQuery);
}
@end
在pch文件里面添加如下宏定义#define KEY_USERNAME_PASSWORD @"com.cloud.app.usernamepassword"。
在viewController里面添加头文件,调用:
NSString *uuid = [UuidObject getUUID];
NSLog(@"UUID = %@",uuid);