iOS开发之 -- 获取设备的唯一标示符

各种获取设备唯一标识的方法介绍

一.UDID(Unique Device Identifier)

UDID的全称是Unique Device Identifier,它就是苹果iOS设备的唯一识别码,它由40位16进制数的字母和数字组成(越狱的设备通过某些工具可以改变设备的UDID)。移动网络可利用UDID来识别移动设备,但是,从IOS5.0(2011年8月份)开始,苹果宣布将不再支持用uniqueIdentifier方法获取设备的UDID,iOS5以下是可以用的。苹果从iOS5开始就移除了通过代码访问UDID的权限。从2013年5月1日起,试图访问UIDIDs的程序将不再被审核通过,替代的方案是开发者应该使用“在iOS 6中介绍的Vendor或Advertising标示符”。所以UDID是绝对是不能再使用了。

//UUID , 已废除
NSString *udid = [[UIDevice currentDevice] uniqueIdentifier];

为什么苹果反对开发人员使用UDID?
iOS
2.0版本以后UIDevice提供一个获取设备唯一标识符的方法uniqueIdentifier,通过该方法我们可以获取设备的序列号,这个也是目前为止唯一可以确认唯一的标示符。

许多开发者把UDID跟用户的真实姓名、密码、住址、其它数据关联起来;网络窥探者会从多个应用收集这些数据,然后顺藤摸瓜得到这个人的许多隐私数据。同时大部分应用确实在频繁传输UDID和私人信息。
为了避免集体诉讼,苹果最终决定在iOS 5
的时候,将这一惯例废除,开发者被引导生成一个唯一的标识符,只能检测应用程序,其他的信息不提供。现在应用试图获取UDID已被禁止且不允许上架。

二.UUID(Universally Unique Identifier)

UUID是Universally Unique Identifier的缩写,中文意思是通用唯一识别码。它是让分布式系统中的所有元素,都能有唯一的辨识资讯,而不需要透过*控制端来做辨识资讯的指定。这样,每个人都可以建立不与其它人冲突的 UUID。在此情况下,就不需考虑数据库建立时的名称重复问题。苹果公司建议使用UUID为应用生成唯一标识字符串。
获得的UUID值系统没有存储, 而且每次调用得到UUID,系统都会返回一个新的唯一标示符。如果你希望存储这个标示符,那么需要自己将其存储到NSUserDefaults, Keychain, Pasteboard或其它地方。

1.CFUUID

从iOS2.0开始,CFUUID就已经出现了。它是CoreFoundatio包的一部分,因此API属于C语言风格。CFUUIDCreate 方法用来创建CFUUIDRef,并且可以获得一个相应的NSString,如下代码:

CFUUIDRef cfuuid = CFUUIDCreate(kCFAllocatorDefault);NSString *cfuuidString = (NSString*)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, cfuuid));

2.NSUUID

NSUUID在iOS 6中才出现,这跟CFUUID几乎完全一样,只不过它是Objective-C接口。+ (id)UUID 是一个类方法,调用该方法可以获得一个UUID。通过下面的代码可以获得一个UUID字符串:

NSString *uuid = [[NSUUID UUID] UUIDString];

跟CFUUID一样,这个值系统也不会存储,每次调用的时候都会获得一个新的唯一标示符。如果要存储的话,你需要自己存储。在我读取NSUUID时,注意到获取到的这个值跟CFUUID完全一样(不过也可能不一样)

三.open UDID

在iOS 5发布时,uniqueIdentifier被弃用了,这引起了广大开发者需要寻找一个可以替代UDID,并且不受苹果控制的方案。由此OpenUDID成为了当时使用最广泛的开源UDID替代方案。OpenUDID在工程中实现起来非常简单,并且还支持一系列的广告提供商。

OpenUDID利用了一个非常巧妙的方法在不同程序间存储标示符 — 在粘贴板中用了一个特殊的名称来存储标示符。通过这种方法,别的程序(同样使用了OpenUDID)知道去什么地方获取已经生成的标示符(而不用再生成一个新的)。而且根据贡献者的代码和方法,和一些开发者的经验,如果把使用了OpenUDID方案的应用全部都删除,再重新获取OpenUDID,此时的OpenUDID就跟以前的不一样。可见,这种方法还是不保险。
但是OpenUDID库早已经弃用了, 在其官方的博客中也指明了, 停止维护OpenUDID的原因是为了更好的向苹果的举措靠拢, 还指明了MAC Address不是一个好的选择。

四.MAC Address

1.这个MAC地址是指什么?有什么用?

MAC(Medium/Media Access Control)地址,用来表示互联网上每一个站点的标识符,采用十六进制数表示,共六个字节(48位)。其中,前三个字节是由IEEE的注册管理机构 RA负责给不同厂家分配的代码(高位24位),也称为“编制上唯一的标识符” (Organizationally Unique Identifier),后三个字节(低位24位)由各厂家自行指派给生产的适配器接口,称为扩展标识符(唯一性)。
MAC地址在网络上用来区分设备的唯一性,接入网络的设备都有一个MAC地址,他们肯定都是不同的,是唯一的。一部iPhone上可能有多个MAC地址,包括WIFI的、SIM的等,但是iTouch和iPad上就有一个WIFI的,因此只需获取WIFI的MAC地址就好了,也就是en0的地址。

形象的说,MAC地址就如同我们身份证上的身份证号码,具有全球唯一性。这样就可以非常好的标识设备唯一性,类似与苹果设备的UDID号,通常的用途有:
1)用于一些统计与分析目的,利用用户的操作习惯和数据更好的规划产品;
2)作为用户ID来唯一识别用户,可以用游客身份使用app又能在服务器端保存相应的信息,省去用户名、密码等注册过程。

2.如何使用Mac地址生成设备的唯一标识呢?

主要分三种:
1、直接使用“MAC Address”
2、使用“MD5(MAC Address)”
3、使用“MD5(Mac Address+bundle_id)”获得“机器+应用”的唯一标识(bundle_id 是应用的唯一标识)

iOS7之前,因为Mac地址是唯一的,
一般app开发者会采取第3种方式来识别安装对应app的设备。为什么会使用它?在iOS5之前,都是使用UDID的,后来被禁用。苹果推荐使用UUID

但是也有诸多问题,从而使用MAC地址。而MAC地址跟UDID一样,存在隐私问题,现在苹果新发布的iOS7上,如果请求Mac地址都会返回一个固定值,那么Mac
Address+bundle_id这个值大家的设备都变成一致的啦,跟UDID一样相当于被禁用, 所以Mac Address
是不能够被使用为获取设备唯一标识的。

五.广告标示符(IDFA-identifierForIdentifier)

广告标示符,在同一个设备上的所有App都会取到相同的值,是苹果专门给各广告提供商用来追踪用户而设的。但好在Apple默认是允许追踪的,而且一般用户都不知道有这么个设置,所以基本上用来监测推广效果,是戳戳有余了。
它是iOS 6中另外一个新的方法,提供了一个方法advertisingIdentifier,通过调用该方法会返回一个NSUUID实例,最后可以获得一个UUID,由系统存储着的。

#import <AdSupport/AdSupport.h>
NSString *adId = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];

不过即使这是由系统存储的,但是有几种情况下,会重新生成广告标示符。如果用户完全重置系统((设置程序 -> 通用 -> 还原 -> 还原位置与隐私) ,这个广告标示符会重新生成。另外如果用户明确的还原广告(设置程序-> 通用 -> 关于本机 -> 广告 -> 还原广告标示符) ,那么广告标示符也会重新生成。
关于广告标示符的还原,有一点需要注意:如果程序在后台运行,此时用户“还原广告标示符”,然后再回到程序中,此时获取广 告标示符并不会立即获得还原后的标示符。必须要终止程序,然后再重新启动程序,才能获得还原后的广告标示符。
所以IDFA也不可以作为获取唯一标识的方法,来识别用户。

六.Vendor标示符 (IDFV-identifierForVendor)

Vendor标示符,是给Vendor标识用户用的,每个设备在所属同一个Vender的应用里,都有相同的值。其中的Vender是指应用提供商,但准确点说,是通过BundleID的反转的前两部分进行匹配,如果相同就是同一个Vender,例如对于com.taobao.app1, com.taobao.app2 这两个BundleID来说,就属于同一个Vender,共享同一个IDFV的值。和IDFA不同的是,IDFV的值是一定能取到的,所以非常适合于作为内部用户行为分析的主id,来标识用户,替代OpenUDID。
它是iOS 6中新增的,跟advertisingIdentifier一样,该方法返回的是一个
NSUUID对象,可以获得一个UUID。如果满足条件“相同的一个程序里面-相同的vendor-相同的设备”,那么获取到的这个属性值就不会变。如果是“相同的程序-相同的设备-不同的vendor,或者是相同的程序-不同的设备-无论是否相同的vendor”这样的情况,那么这个值是不会相同的。

    NSString *strIDFV = [[[UIDevice currentDevice] identifierForVendor] UUIDString];

但是如果用户将属于此Vender的所有App卸载,则IDFV的值会被重置,即再重装此Vender的App,IDFV的值和之前不同。

七.推送token+bundle_id

推送token+bundle_id的方法:
1、应用中增加推送用来获取token
2、获取应用bundle_id
3、根据token+bundle_id进行散列运算

apple push token保证设备唯一,但必须有网络情况下才能工作,该方法并不是依赖于设备本身,而是依赖于apple push机制,所以当苹果push做出改变时, 你获取所谓的唯一标识也就随之失效了。所以此方法还是不可取的。

八. NSUUID, CFUUID, IDFA, IDFV获取的标识对比

首次运行

NSUUID:9D820D3A-4429-4918-97F7-A69588B388A4
CFUUID:80F961D0-1E6A-4ECD-A0A9-F58ED858FE20
IDFA:687E6A90-50A3-4424-871C-BE255D050AFD
IDFV:8E740A99-283B-4F6A-87EF-443FB7778488

二次运行

NSUUID:23AB8D3D-4F1D-45E2-8BD7-83B451125326
CFUUID:14DDBFCF-67A6-46B7-BB48-4EF2ADC5429F
IDFA:687E6A90-50A3-4424-871C-BE255D050AFD
IDFV:8E740A99-283B-4F6A-87EF-443FB7778488

卸载后, 重新安装运行

NSUUID:BD934F9C-B7EC-4BD1-B65E-964C66537CAB
CFUUID:29654DE0-AC93-40F9-98AB-1E10A271AF8D
IDFA:687E6A90-50A3-4424-871C-BE255D050AFD
IDFV:8E740A99-283B-4F6A-87EF-443FB7778488

重启后运行

NSUUID:82711557-3A17-4B82-8F18-09AADF9DD37B
CFUUID:FFBC73EC-CFBE-414C-870E-77C0714E0347
IDFA:687E6A90-50A3-4424-871C-BE255D050AFD
IDFV:8E740A99-283B-4F6A-87EF-443FB7778488

总结

说了这么多, 才发现原来没有一种方法是可行的。没错, 其实自从苹果废除UDID后, 就不能达到获取设备真正的唯一标识了。因为这些方法中导致获取的唯一标示产生改变的原因, 或是重新调用方法, 或是重启设备, 或是卸载应用, 或是还原某些标识, 或者刷新系统…
所以, 不能达到从根本上获取唯一标识, 我们只能做到尽可能接近。下面是我用过的方法。

如何正确的获取设备的唯一标识(亲测有用)

我用的方法是将获取的UUID永久存储在设备的KeyChain中, 这个方法在应用第一次启动时, 将获取的UUID存储进KeyChain中, 每次取的时候, 检查本地钥匙串中有没有, 如果没有则需要将获取的UUID存储进去。当你重启设备, 卸载应用再次安装,都不影响, 只是当设备刷机时, KeyChain会清空, 才会消失, 才会失效。
不只是这一种方法, 你也可以保存除UUID之外,其他合适的标识, 但利用KeyChain去存储标识的方式应该是最接近的。

利用keyChain和UUID永久获得设备的唯一标识

开发者可以在应用第一次启动时调用一次,然后将该串存储起来,以便以后替代UDID来使用。但是,如果用户删除该应用再次安装时,又会生成新的字符串,所以不能保证唯一识别该设备。这就需要各路高手想出各种解决方案。所以,之前很多应用就采用MAC Address。但是现在如果用户升级到iOS7(及其以后的苹果系统)后,他们机子的MAC Address就是一样的,没办法做区分,只能弃用此方法,重新使用UUID来标识。如果使用UUID,就要考虑应用被删除后再重新安装时的处理。

具体代码如下:

1.打开工程,看一下自己的Bundle Id.这个Bundle Id 要和你用真机测试时的证书上面的Bundle Id相匹配

iOS开发之 -- 获取设备的唯一标示符

2.Target - Capabilities - Keychain Sharing - ON,打开钥匙串,由灰色状态变成蓝色状态

iOS开发之 -- 获取设备的唯一标示符

3,左侧目录会出现Entitlements文件,因为先前集成过极光推送,所有Entitlements文件已经存在了,如下图:

iOS开发之 -- 获取设备的唯一标示符

也就是说,Bundle Identifier、Keychain Sharing的Keychain Groups、Entitlements文件的Keychain Access Groups的第一个元素,它们要保持上图所示的一致性。

设置好了以后可以运行下程序,没问题可以进行下一步。

4,uuid+keychain,具体代码如下:

苹果官方也提供了一些demo,据说会崩溃,有兴趣的朋友可以尝试下:链接:https://developer.apple.com/library/ios/samplecode/GenericKeychain/Introduction/Intro.html

所以我也是参考网上的资料,修改的demo,只保存一个唯一的uuid的话,完全可以满足需求:

UUID.h

#import <Foundation/Foundation.h>

@interface UUID : NSObject

+(NSString *)getUUID;

@end

UUID.m

#import "UUID.h"
#import "KeyChainStore.h" @implementation UUID +(NSString *)getUUID { NSString * strUUID = (NSString *)[KeyChainStore load:@"com.company.app.usernamepassword"]; //首次执行该方法时,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 <Foundation/Foundation.h>

@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

在需要的地方导入两个头文件即可:

5.把一下内容添加到pch文件上:

 #ifndef PrefixHeader_pch

#define PrefixHeader_pch

#define  KEY_USERNAME_PASSWORD @"com.company.app.usernamepassword"

#define  KEY_USERNAME @"com.company.app.username"

#define  KEY_PASSWORD @"com.company.app.password"

#endif

6,在控制器里面执行如下代码:

 NSString *uuidstr = [UUID getUUID];

    NSLog(@"uuidstr is %@",[hHttpEngine stringtoGL:uuidstr]);

附一个转码的方法:

#pragma mark 过滤掉“—”
+ (NSString*)stringtoGL:(NSString *)str; + (NSString*)stringtoGL:(NSString *)str
{
NSCharacterSet *doNotWant = [NSCharacterSet characterSetWithCharactersInString:@"[]{}(#%-*+=_)\\|~(<>$%^&*)_+ "];
NSString * hmutStr = [[str componentsSeparatedByCharactersInSet: doNotWant]componentsJoinedByString: @""]; NSLog(@"humStr is %@",hmutStr); return hmutStr;
}

这样的话就可以直接传到后台,实现所需要的操作了!

控制台打印如下:

iOS开发之 -- 获取设备的唯一标示符

注:其实这样做的目的的话,是为了保证在一台设备上,一个手机号,只能注册一次这样的需求,如果调用苹果以前的方法,不存入到钥匙串里面的话,每次把应用卸载,重新安装的时候,不管获取到devicetoken或者uuid或者udid或者idfv的话,都会随着应用的重新安装,而发生改变,这样的话,如果用户重新安装应用,就会出现账号不匹配的情况!但是当系统升级或者刷机后,系统中的钥匙串会被清空,该方法会失效!

参考:http://blog.csdn.net/wangyanchang21/article/details/53068304

http://blog.sina.com.cn/s/blog_5971cdd00102vqgy.html

扩展:IOS在钥匙串里保存APP的账号密码: http://blog.csdn.net/jiang314/article/details/51984914

上一篇:【Python】【网络编程】


下一篇:敏捷转型历程 - Sprint3 一团糟的演示会