唯一的设备ID

    苹果对用户隐私方面的权限管理非常严格,不允许调用私有API获取用户硬件的相关的ID,其中包括手机号、UDID、IMIE、序列号、MAC地址等,这些能解析设备唯一性的信息都不能获取,否则无法上架App Store.本文将讨论如何获取设备的相关ID,同时不违背苹果对于隐私管控的规定。

1. UDID与设备ID
   UDID的全称是Unique Device Identifier,它是iOS设备的唯一标识码,由40位十六进制的字母和数字组成。在iOS5以下,苹果的开发者们通常使用UDID作为设备的唯一标识,我们可以通过调用私有API的方式获得UDID,代码如下:
     NSString *udid = [[UIDevice currentDevice]uniqueIdentifier];

除了通过调用私有的API获取UDID之外,还可以通过Safari浏览器安装描述文件来获取UDID,访问http:www.exchen.net/udid,然后点击“获取UDID”按钮,会出现描述文件的安装提示,安装好描述文件以后,页面上会显示UDID。

 关于如何通过safari浏览器获取UDID,详情可以参阅https://www.exchen.net/通过-safari-浏览器获取-udid.html

2. IDFA

  对于一个设备上的所有应用,获取的IDFA都是一样的。由于苹果取消了UDID的获取方法,IDFA就成了一种标识和追踪用户的常用方法。一台新的iOS设备在激活之后,IDFA是默认开启的,用户也可以关闭或者重置,如果重置,应用再次获取IDFA就会有变化,但是用户一般是不知道这个开关和设置的。IDFA的获取代码如下:

#import <AdSupport/AdSupport.h>

NSString *strIDFA = [ASIdentifierManager sharedManager]advertisingIdentifier]UUIDString];

NSLog(@"IDFA: %@",strIDFA);

输出结果如下:
2020-04-20 15:33:46.831800+0800 DeviceUDID[1461:137700] IDFA: 3D42C428-513E-44EA-AB82-BC49DD641286

如果在 “设置” -> “隐私” -> “广告” 中开启“限制广告追踪”,就会关闭IDFA,如图所示,此时获取的IDFA的值就会变成00000000-0000-0000-0000-000000000000。如果点击“还原广告标识符”,那么再次获得的IDFA就和上次是不一样的。

3.IDFV

IDFV是供应商标识符,当同一个证书签发的多个应用安装在同一个设备上时,这些应用获取的IDFV都是一样的。不论卸载了几个应用,只要设备上还存在该证书签发的应用,重新安装的证书签发的其他应用,IDFV就不会变。如果将该证书签发的应用全部卸载,重新安装的应用所获取的IDFV就会变化。获取IDFV的代码如下:

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

NSLog(@"strIDFV:%@",strIDFV);

运行后输出的结果:

2020-04-20 17:29:45.531076+0800 DeviceUDID[2356:199871] strIDFV: 0E07AAE2-3EAA-4011-846F-2793D10C7C1F

4.OpenUDID

OpenUDID是一种开源的ID生成算法,下载地址为:https://github.com/ylechelle/OpenUDID,使用OpenUDID的代码如下:

#import "OpenUDID.h"

NSString *strOpenUDID = [OpenUDID value];

NSLog(@"strOpenUDID:%@",strOpenUDID);

打印数据:

2020-04-21 09:02:11.556993+0800 DeviceUDID[17351:44308] strOpenUDID: 25f81fe2fae5cb67ef817322baf9a7803f53f2d7

由于高版本的Xcode默认启用ARC,而早期的OpenUDID出现的时候还没有ARC模式,所以编译时要注意,在Build Phases里的Compile Sources中,给OpenUUID.m添加-fno-objc-arc标记来关闭ARC,如图所示。

OpenUUID的核心原理是将生成的ID保存到剪切板中,同时在沙盒的Preferences目录下也保存一份,当应用被卸载时,沙盒目录会清空,但是剪切板中的数据还存在,所以从剪切板中获取数据OpenUUID就可以了。

下面我们来看一下OpenUUID内部实现的代码:

5. simulateIDFA

2016年,有米公司开源了一个获取ID的方法simulateIDFA,该方法可以在短时间内识别一台设备的唯一性,主要用于进行广告检测。由于一些设备的IDFA被关闭,所以simulateIDFA就可以成为一种“曲线救国”的方法。下载地址为: https://github.com/youmi/SimulateIDFA,调用方法也很简单,只需要调用createSimulateIDFA函数即可,代码如下:

#import "SimulateIDFA.h"
NSString *simulateIDFA = [SimulateIDFA createSimulateIDFA];

NSLog(@"simulateIDFA: %@",simulateIDFA);
2020-04-21 10:03:46.321838+0800 DeviceUDID[17819:78121] simulateIDFA: 7AA3216A-905E-1F0B-9EE0-8CF164A6ABCA

createSimulateIDFA函数获取ID的原理:首先获取两组信息,一组是不稳定的信息,包括系统启用时间、国家代码、本地语言、设备名称,另一组是稳定的信息,包括系统版本、机型、运营商名称、内存、coreServices文件创建时间、硬盘使用空间;然后将这两组获取到的信息都格式化为字符串后放到fingerPrintUnstablePart和fingerPrintStablePart变量中;接着对这两组信息进行MD5加密,最后将加密后的两组值通过combineTwoFingerPrint函数转化为与IDFA一样的格式。SimulateIDFA的核心代码如下:
6. Mac地址
       除了使用UDID作为iOS设备的唯一标识符,还可以使用MAC地址。调用sysctl和ioctl可以获取MAC地址,直到iOS7的出现。不知出于什么原因,苹果对于sysctl和ioctl进行了技术处理,让MAC地址返回02:00:00:00:00:00。官方文档上这样写的:
“Two low-level networking APIs that used to return a MAC address now return thefixed value 02:00:00:00:00:00. The APIs in question are sysctl(NET_RT_IFLIST) and ioctl(SIOCGIFCONF). Developers using the value of the MAC address should migrate toidentifiers such as -[UIDevice identifierForVendor].This change affects all apps running on iOS 7” 
       原文的意思是苹果对于sysctl和ioctl进行了技术处理,Mac地址返回的都是 02:00:00:00:00:00,有需要使用MAC地址的开发者请使用其他方法,如[UIDevice identifierForVendor].这个改动会影响iOS7及以上系统。
       由于iOS7无法获得MAC地址,所以不得不另谋出路。AppStore上有一款应用叫做Fing,该应用是一款网络扫描器,能扫描到局域网的主机和MAC地址,其中包含本机的MAC地址。于是对该应用进行分析,发现Fing是从ARP列表中获取的MAC地址,这是一个很好的思路,下面我们来实现一下,其功能包括本机的MAC地址,获取路由器的IP地址,获取路由的MAC地址.

7. ID的持久存储
         
众所周知,每个iOS应用在系统上都对应一个沙盒,它只能在自己的沙盒中存储数据,如果应用被卸载,那么其沙盒目录也会被删除,就像微信被卸载了,那么本地的聊天记录肯定也就没有了。由于UDID不能获取,所以应用开发者只能自己生成一个ID,想要这个ID不变,就必须持久化的数据存储。在iOS系统上有两个地方可以做到应用被卸载后,数据不会被清理,一个是Keychain钥匙串,还有一个是剪切板。而之前我们所介绍的OpenUUID就是利用剪切板存储的。

    1. Keychain存储

  苹果封装了一个读写Keychain的对象keychainItemWrapper,将该对象的源代码文件添加到工程中。需要注意的是,在Build Phases里,在Compile Sources里的KeychainItemWrapper.m中,添加Compiler Flags为-fno-objc-arc来关闭ARC,如图所示

                                          KeychainItemWrapper.m关闭ARC模式

这里定义了一个函数getIDForKeychain,通过这个函数能获取和生成ID,代码如下:
NSString *keychainID = [self getIDForKeychain];

下面来看看getIDForKeychain函数的过程,其代码如下:
 

//获取和生成一个ID并用keychain进行存储

-(NSString *)getIDForKeychain{

    NSString *strBundleSeedID = [self bundleSeedID];

    NSDictionary *infoDic = [[NSBundle mainBundle]infoDictionary];

    NSString *strAppname = [infoDic objectForKey:@"CFBundleIdentifier"];

    NSString *strGroup = [NSString stringWithFormat:@"%@.%@",strBundleSeedID,strAppname];

    NSLog(@"strGroup:%@",strGroup);

    KeychainItemWrapper *keychainItem = [[KeychainItemWrapper alloc]initWithIdentifier:@"super" accessGroup:strGroup];

    NSString *strValue = [keychainItem objectForKey:(NSString *)CFBridgingRelease(kSecValueData)];

    if ([strValue isEqualToString:@""]||strValue==nil) {

        //随机生成ID

        CFUUIDRef uuid = CFUUIDCreate(NULL);

        strValue = (NSString *)CFBridgingRelease(CFUUIDCreateString(NULL, uuid));

        strValue = [self md5:strValue];//MD5

        [keychainItem setObject:strValue forKey:(NSString *)CFBridgingRelease(kSecValueData)];

    }

    return strValue;

}
首先,要获取bundleSeedID,也就是证书前缀,将证书的前缀和应用的CFBundleIdentifier格式化为一个group.这个group很重要,因为应用之间读取keychain也是互相隔离的,必须要在同一个访问组才能读取。然后调用[keychainItem objectForkey]获取对应的keychain中的值。如果获取的数据为空,说明是第一次运行,需要生成ID,其方法是调用CFUUIDCreateString函数生成一个随机的UUID串,然后再进行一次MD5加密,接着调用[keychainItem setObject]将ID写入keychain,最后返回ID。

bundleSeedID获取证书的前缀的函数如下,其实际原理也是从keychain中读取:

-(NSString *)bundleSeedID{

    NSDictionary *query = [NSDictionary dictionaryWithObjectsAndKeys:

                           (NSString *)CFBridgingRelease(kSecClassGenericPassword),kSecClass,

                           @"bundleSeedID",kSecAttrAccount,

                           @"",kSecAttrService,

                           (id)kCFBooleanTrue,kSecReturnAttributes,

                           nil];

    CFDictionaryRef result = nil;

    OSStatus status = SecItemCopyMatching((CFDictionaryRef)query, (CFTypeRef *)&result);

    if (status==errSecItemNotFound) {

        status = SecItemAdd((CFDictionaryRef)query, (CFTypeRef *)&result);

    }

    if (status!=errSecSuccess) {

        return nil;

    }

    NSString *accessGroup = [(__bridge NSDictionary *)result objectForKey:(NSString *)CFBridgingRelease(kSecAttrAccessGroup)];

    NSArray *compents = [accessGroup componentsSeparatedByString:@"."];

    NSString *bundleSeedID = [[compents objectEnumerator]nextObject];

    CFRelease(result);

    return bundleSeedID;

}

2. 剪贴板
  除了Keychain能够进行持久化的数据存储外,剪贴板也可以。我们将ID保存在剪贴板中,如果应用被卸载了,下次安装依然能够获取到,并且在系统升级更新时也保持不变。下面定义了一个getIDForPasteboard函数,通过这个函数获取和生成ID。

NSString * pasteboardID = [self getIDForPasteboard];
下面我们来看看getIDForPasteboard函数的实现过程,代码如下:

-(NSString *)getIDForPasteboard{

    NSString *pasteboardName = @"exchen.net";

    NSString *pasteboardType = @"id";

    NSString *strID;

    UIPasteboard *pasteboard = [UIPasteboard pasteboardWithName:pasteboardName create:YES];

    pasteboard.persistent = YES;

    //从剪贴板里读取ID

    id item = [pasteboard dataForPasteboardType:pasteboardType];

    if (item) {

        //如果读取的数据不为空,说明之前就写入了ID

        item = [NSKeyedUnarchiver unarchiveObjectWithData:item];

        if (item!=nil) {

            NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:item];

            strID = [dic objectForKey:pasteboardType];

        }

    }

    else{

        //随机生成ID

        CFUUIDRef uuid = CFUUIDCreate(NULL);

        strID = (NSString *)CFBridgingRelease(CFUUIDCreateString(NULL, uuid));

        strID = [self md5:strValue];//MD5

        

        //写入剪贴板

        NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:item];

        [dic setValue:strID forKey:pasteboardType];

        [pasteboard setData:[NSKeyedArchiver archivedDataWithRootObject:dic] forPasteboardType:pasteboardType];

    }

    return strID;

}
首先,定义剪贴板的名称和类型,调用[UIPasteboard pasteboardWithName]创建剪贴板,通过dataForPasteboardType获取相应类型的数据。如果能够读到内容,就调用[NSKeyedUnarchiver unarchiveObjectWithData:item]将数据解析出来,然后返回ID;如果读不到内容,就说明是第一次运行,需要生成ID。生成ID的方法是调用CFUUIDCreateString创建一个随机的UUID,接着对UUID进行MD加密。ID生成之后,调用setData向剪贴板中写入数据,最后返回ID。

8. DeviceToken

DeviceToken主要用于推送消息,也可以识别用户的设备,其获取方法是在AppDeleagte.m里添加didRegisterForRemoteNotificationsWithDeviceToken方法,具体代码如下:
 

-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{

    NSString *strDeviceToken = [[[[deviceToken description]stringByReplacingOccurrencesOfString:@"<" withString:@"."]stringByReplacingOccurrencesOfString:@">" withString:@""]stringByReplacingOccurrencesOfString:@" " withString:@""];

    NSLog(@"DeviceToken:%@",strDeviceToken);

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值