iOS-DeviceToken变化之谜

作者: jins_1990 | 来源:发表于2016-05-30 21:49 被阅读11773次

    本人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
    
    

    相关文章

      网友评论

      • Arvin_雾里看花:我试了下,ios9之后也不会变化啊,卸载重装
      • 72f49dc6e2ea:有个问题想问一下啊 既然uuid不会变,为什么要存到keychain里面,每次要用的话直接去获取,再和devicetoken一起传到服务器不就行了
        314ff6ceae4b:UUID是没有currentUUID之类的获取当前UUID的方法的,它只有创建方法[NSUUID UUID],也即一获取就是一个新的UUID
      • 半尺尘:写得很好,赞~👍
      • 少年不知代码贵:写的很详细,也遇到过这种问题,9.1之后卸载确实会发生变化
      • 安心做个笨男孩:你好,我想问下,不用极光推送等,后台服务器那和苹果如何对接的?
        jins_1990:@安心做个笨男孩 我们公司是用java写的 具体的我就不知道了
      • mieGod:作者你好,
        文中提及的问题是一个用户收到多个重复的消息,是因为一个用户对应多个device token造成的,那为什么后台要保存多个 device token呢,如果device token有变化,后台直接替换掉之前的不就可以了吗,这个问题跟前端没有什么关系吧?
        PokerFace_u:@jins_1990 说的在理!
        jins_1990:@mieGod 保存多个token是因为,如果同一用户多台手机登录,怎么确保每台手机都能收到推送
      • a群:真机测试 iOS 9.4.3 和 iOS 10.0.1 deviceToken 在反复卸载demo时候 并没有变化...
        _moses:@jins_1990 刚才在别的地方看到,这TM是苹果iOS9的BUG,不知道真的假的,反正我的10没问题:joy:
        _moses: 我也是10.0.1,卸载了几十次了,一直都没变过,不知道更新系统会不会变。
        jins_1990:最近没关注过这个问题,今天试了一下,iOS 10.2.1 ,不删除demo,每次注册的 token没有发生变化,删除demo之后注册的token 发生变化.这篇文章当时的版本号忘记了,不好意思.
      • 不余先生:你好,请问在UuidObject.m文件下的[KeyChainStore save:KEY_USERNAME_PASSWORD data:strUUID];中这段代码的KEY_USERNAME_PASSWORD是保存用户的用户名或者是密码是吗?填nil可以吗?
        天山雪莲_38324:也就是说KEY_USERNAME_PASSWORD可以写成一个宏定义,定义为App的名字,对吗?
        不余先生:@贱哥_1990 谢谢
        jins_1990:@a1203302261 这个其实就是一个键值对的key名而已,用app的名字也行,就是查询有没有这个key对应的值,如果没有值,生成一个uuid,和这个key配成键值对,存到keychain内部就OK.我这写的有点不明其意,我的问题,哈哈
      • 883c7bb8a6ff:你们如何根据同一个设备、登录用户的不同,精准推送的?
        abf319bdd291:1.每个设备都会生成一个唯一号,启动的时候唯一号和设备的devicetoken绑定,确保收到推送消息【群推】。
        2.用户登录时,用户账号和唯一号绑定;
        用户登录期间,用此唯一号推送;
        用户退出,用户和唯一号解绑。【单推】
        麦子maizi:@贱哥_1990 他的意思应该是同一个手机,不同用户登陆的情况,你的DeviceToken是不变的,我切换用户还是会收到前一个用户的apns,这里要怎么处理呢?
        jins_1990:@whyou 每个用户都绑定了deviceToken啊,退出登陆的时候就把deviceToken同时清除,这样,如果同一个用户用多个设备登陆,那么该用户下会有个多个deviceToken,而且每一个deviceToken都是有效的,也会做到多台设备同时推送
      • AppleIdGX:所以,当设备被擦除后(重新安装,证书变化,系统更新等),Device_token都可能会发生变化
      • AppleIdGX:友盟论坛里面的回答:

        ios9以前的系统里面:一个设备的token是唯一的。除了升级系统等少量情况,基本不变。 而且在token变了以后,老的token,就被认为是无效了。 苹果不会对这部分无效的token推送。
        ios9的系统:一个app每一次重新安装多会产生新的token。 而且老的token不会无效,还可以正常推送。 这个问题,我们在ios9刚发布的时候,我们就向苹果反馈过这个问题,也得到过他们反馈,应该是个bug。但是他们一直也没有修复。 所以这个重担就落在我们头上。

        处理办法:
        我们目前是根据OpenUDID(实时过滤)和IDFA(按天过滤)双重过滤(部分app没有采集IDFA,就只按按照OpenUDID),对于同一个OpenUDID或者IDFA只采用最新的devietoken做为设备的有效devicetoken,老的我们这边认为是非法的。 由于某些原因OpenUDID也可能会变,所以就会有极少量的设备可能会存在发送两次以上的情况(尤其是测试设备)。
      • 4e528bb5d122:你好,我是菜鸟,有个问题,不能直接用NSUserDefault储存吗?
        487cae2f38eb:@射手豆豆 后台只是判断下是不是需要替换数据库里的信息吧,如果每次都重新获取 数据库每次岂不是都要替换一次 我是这么理解的
        哄哄的薇薇:@释放想象力 为什么一定要存储uuid呢,每次用的时候去获取不就可以吗?
        释放想象力:@夜风相随 不能,如果你这样做,那么应用删除了存储的内容也会消失!
      • WHZ闹哪样:有这么一个场景,app上用户是登录状态,然后我升级系统,升级完成后用户应该也还是登录状态,此时devicetoken应该是改变的,但是app服务器要推送的话还是把原来的devicetoken传给苹果的服务器,这样会不会造成接收不到推送消息
        Leehf:升级系统不是会重启嘛
        jins_1990:@WHZ闹哪样 哈哈 具体这种特殊情况,我真的没有测试过呢
      • WHZ闹哪样:有个问题,既然可以获取并存储UUID,为什么不直接使用UUID作为设备唯一标识符?这样不是少了重新绑定devicetoken这一步吗
        jins_1990:@WHZ闹哪样 这个出现的问题时间有点久远,当时也没仔细查出来原因,后来修复之后就不了了之了
        WHZ闹哪样:@贱哥_1990 现在我明白了,又有一个新的问题,你说同一台机器token会发生变化,之前的token就失效了,但是你在文中说过重复收到信息可能是因为一个用户用同一台手机绑定了多个token号(因为token可能会变化),假如之前的token号失效的话照理来说应该只能收到一条消息啊.
        jins_1990:@WHZ闹哪样 UUID就是作为唯一标识符啊,但是我们后台需要那个devicetoken去推送,但是devicetoken是变化的,而且变化之后之前的token就失效了
      • 小草先生:按deviceToken 去推送 有几百万用户 后台不是需要推送几百万次?
        jins_1990:@小草先生 好像真的是的,我们的应用没有用jpush等第三方平台,是自己后台发出的,是会一条一条发出的,不过我们目前用户不多 :pensive:
      • a24b6718d698:有个地方不理解啊,这个token最原本是存在系统哪里的啊,删除应用 ,下载新的应用,本身的token就是最新的啊,那还有必要上传服务器验正嘛?这里的替换就不是没意思?我还没懂,求指教
        jins_1990:@沐逸 恩 因为旧的可能就失效了 会推送失败 ,所以只有最新的token才能推送成功,不过是保证一个设备一个token,可能会存在一个用户多台设备登陆,那么就可能对应多个设备,多个token.做到同时推送多台设备,就可以根据这些最新的token推送了 不会发生推送失败之类的
        a24b6718d698:@贱哥_1990 懂啦 最后还是得服务器是根据新的token 给APNS 而不是我给他一个Token 他什么不做 老得新的都传给APNS,是这样的意思吧?
        jins_1990:@沐逸 token苹果根据appID和设备编码生成的,我们拿到后是需要传给后端,后端如果推送信息,会把信息和token发送给APNs,苹果去推送.这篇讲的内容是根据我们公司需要每个用户绑定一个唯一的token,不会发生推送到错误设备上的情况
      • zp4133:写的很好,详细易懂
      • Yaanco:666
      • BlueEagleBoy:写的很好 多多分享
      • 阿拉当:有用

      本文标题:iOS-DeviceToken变化之谜

      本文链接:https://www.haomeiwen.com/subject/vddcdttx.html