本人iOS菜鸟一枚,最近遇到了关于deviceToken的一些问题,将解决问题的思路记录下来,也算是一种知识的沉淀吧,或许能帮助到同样遇到问题的你,那就再好不过了.
闲话不说,直接进入主题.
一.介绍一下问题的背景
最近在搞远程推送的时候,忽然发现,有时候,当某一台机器需要推送一条信息的时候,这台机器可能会收到同样的信息若干条.就去找问题所在.然而更换了证书,或者配置文件之后,故障依然存在.我就认为这不是我的问题,是后台服务器的问题(后台的兄弟们,无辜躺枪),就去了解了一下后台推送的相关流程.之前只是了解一下苹果远程推送的原理,不是很了解我们后台服务器端需要做些什么事情.
简述一下我目前的理解.当app启动时,我们在appDelegate里面注册远程通知,然后苹果服务器返回一个deviceToken给我们,在appDelegate的其中一个代理方法
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken
在该方法里,我们获取到的NSData类型的devToken就是苹果服务器根据我们这一台设备的UDID和app的bundleID混编而成的deviceToken,我们需要将这个deviceToken传送给我们的服务器端,或者登陆用户的时候作为参数传给服务器.这样一个用户对象就绑定了一个deviceToken.当需要给这个用户推送消息的时候,我们自己的后台服务器,就会找这个用户对应的deviceToken和要发送的推送内容,直接发送到苹果的apns服务器,然后由苹果的apns服务器将消息推送到该deviceToekn对应的手机上.
后来我就让后台的兄弟查到该用户竟然对应了多个deviceToken,不过当这一账号同时在多个设备上登陆的时候,可能会绑定多个deviceToken的,但问题是测试机一共就两个,不会存在绑定八九个deviceToken的.我就在想这个deviceToken会不会发生变化.
二.问题所在
在网上搜索到一些知识,也查询了一下官方文档里面对deviceToken的解释,deviceToken会发生变化,但是仅仅在用户在新的设备上登陆或者更新设备操作系统的时候会发生变化.更重要的是,我的上司领导也很肯定的告诉我,同一台设备,同一款软件,而且还是在没有修改软件的bundleID的情况下,deviceToken是不会发生变化的.
后来测试时候就无意中发现,每当我将运行在真机上的demo卸载再重新运行的时候,deviceToken竟然会是发生变化的,而且还是无规律的发生变化.这让我发现了故障的所在.竟然卸载重装会让deviceToken发生变化.后来我分别用iOS7.0系统和iOS8.0的真机测试,发现在这两款系统上,卸载重装,苹果返回的deviceToken不会发生变化.而只有iOS9.0以后的系统版本会发生变化.而且如果每次启动都请求注册的话,只要你没有卸载重装,那么返回的deviceToken是不会发生变化的.只有当你卸载重装的时候才会发生变化.
三.解决方案
先说我尝试过的一种解决方案吧,我将第一次安装软件时候所获得的deviceToken,存储在钥匙串(keychain)内.以后不论什么时候卸载重装软件,只有软件一启动,那么就从keychain内读取保存的deviceToken.然后利用这个deviceToken去进行推送服务.但是我自己在用网络上的那个可以模拟推送的mac小demo(名字叫做PushMeBaby)时,发现如果卸载重装软件后,keychain内保存的旧的deviceToken竟然是无效的,而新获得的deviceToken才是有效的.这让我感到很无奈,保存在keychain的方法没有奏效.
后来考虑到在传递给自己后台服务器时候,怎么才可能保证用一个用户下,一个设备下仅仅保存一个deviceToken,每当这个设备的deviceToken发生变化的时候,就替换该设备对应的deviceToken.
最终的解决方案就是,获取设备的UUID(被苹果禁用的是UDID) + keychain + DeviceToken来解决这个问题.
当软件第一次安装时候,获取设备的UUID 存储到keychain中,那么只要你不刷机,那么这个保存在keychain中的UUID一直存在,即使你升级操作系统也会存在(我正好升级试了一下),这样我们就能保证设备编码的唯一性,在向我们自己的后台服务器传参数时,将这个UUID和获得的deviceToken一起传递过去,让后台做个校验,查询该用户属性下的UUID设备对应的deviceToken是否发生变化,如果发生变化,就替换.这样保证了该用户在这一台设备上绑定了一个deviceToken,这样推送的时候就不会造成可能会推送多条信息的bug.
四.下面附上封装的UUID和keychain的代码,稍微修改一下即可使用.(注意:必须需要导入Security.framework框架)
// UuidObject.h
#import@interface UuidObject : NSObject
+ (NSString *)getUUID;
@end
// UuidObject.m
#import "UuidObject.h"
#import "KeyChainStore.h"
@implementation UuidObject
+(NSString *)getUUID
{
NSString * strUUID = (NSString *)[KeyChainStore load:@"your_app_bundleID"];
//首次执行该方法时,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;
}
@end
//KeyChainStore.h
#import@interface KeyChainStore : NSObject
+ (void)save:(NSString *)service data:(id)data;
+ (id)load:(NSString *)service;
+ (void)deleteKeyData:(NSString *)service;
@end
//KeyChainStore.m
#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
网友评论
文中提及的问题是一个用户收到多个重复的消息,是因为一个用户对应多个device token造成的,那为什么后台要保存多个 device token呢,如果device token有变化,后台直接替换掉之前的不就可以了吗,这个问题跟前端没有什么关系吧?
2.用户登录时,用户账号和唯一号绑定;
用户登录期间,用此唯一号推送;
用户退出,用户和唯一号解绑。【单推】
ios9以前的系统里面:一个设备的token是唯一的。除了升级系统等少量情况,基本不变。 而且在token变了以后,老的token,就被认为是无效了。 苹果不会对这部分无效的token推送。
ios9的系统:一个app每一次重新安装多会产生新的token。 而且老的token不会无效,还可以正常推送。 这个问题,我们在ios9刚发布的时候,我们就向苹果反馈过这个问题,也得到过他们反馈,应该是个bug。但是他们一直也没有修复。 所以这个重担就落在我们头上。
处理办法:
我们目前是根据OpenUDID(实时过滤)和IDFA(按天过滤)双重过滤(部分app没有采集IDFA,就只按按照OpenUDID),对于同一个OpenUDID或者IDFA只采用最新的devietoken做为设备的有效devicetoken,老的我们这边认为是非法的。 由于某些原因OpenUDID也可能会变,所以就会有极少量的设备可能会存在发送两次以上的情况(尤其是测试设备)。