獲取iOS設備唯一標示UUID
在開發過程中,我們經常會被要求獲取每個設備的唯一標示,以便后臺做相應的處理。我們來看看有哪些方法來獲取設備的唯一標示,然后再分析下這些方法的利弊。
具體可以分為如下幾種:
- UDID
- IDFA
- IDFV
- MAC
- keychain
下面我們來具體分析下每種獲取方法的利弊
1、UDID
什么是UDID
UDID 「Unique Device Identifier Description」是由子母和數字組成的40個字符串的序號,用來區別每一個唯一的iOS設備,包括 iPhones, iPads, 以及 iPod touches,這些編碼看起來是隨機的,實際上是跟硬件設備特點相聯系的,另外你可以到iTunes,pp助手或itools等軟件查看你的udid(設備標識)
如下圖所示:
image
UDID是用來干什么的?
UDID可以關聯其它各種數據到相關設備上。例如,連接到開發者賬號,可以允許在發布前讓設備安裝或測試應用;也可以讓開發者獲得iOS測試版進行體驗。蘋果用UDID連接到蘋果的ID,這些設備可以自動下載和安裝從App Store購買的應用、保存從iTunes購買的音樂、幫助蘋果發送推送通知、即時消息。 在iOS 應用早期,UDID被第三方應用開發者和網絡廣告商用來收集用戶數據,可以用來關聯地址、記錄應用使用習慣……以便推送精準廣告。
為什么蘋果反對開發人員使用UDID?
iOS 2.0版本以后UIDevice提供一個獲取設備唯一標識符的方法uniqueIdentifier,通過該方法我們可以獲取設備的序列號,這個也是目前為止唯一可以確認唯一的標示符。 許多開發者把UDID跟用戶的真實姓名、密碼、住址、其它數據關聯起來;網絡窺探者會從多個應用收集這些數據,然后順藤摸瓜得到這個人的許多隱私數據。同時大部分應用確實在頻繁傳輸UDID和私人信息。 為了避免集體訴訟,蘋果最終決定在iOS 5 的時候,將這一慣例廢除,開發者被引導生成一個唯一的標識符,只能檢測應用程序,其他的信息不提供。 現在應用試圖獲取UDID已被禁止且不允許上架。
所以這個方法作廢
2、IDFA
-
全名:advertisingIdentifier
-
獲取代碼:
#import <AdSupport/AdSupport.h>
NSString *adId = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
-
來源:iOS6.0及以后
-
說明:直譯就是廣告id, 在同一個設備上的所有App都會取到相同的值,是蘋果專門給各廣告提供商用來追蹤用戶而設的,用戶可以在 設置|隱私|廣告追蹤 里重置此id的值,或限制此id的使用,故此id有可能會取不到值,但好在Apple默認是允許追蹤的,而且一般用戶都不知道有這么個設置,所以基本上用來監測推廣效果,是戳戳有余了。
-
注意:由于idfa會出現取不到的情況,故絕不可以作為業務分析的主id,來識別用戶。
3、IDFV
- 全名:identifierForVendor
- 獲取代碼:
NSString *idfv = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
-
來源:iOS6.0及以后
-
說明:顧名思義,是給Vendor標識用戶用的,每個設備在所屬同一個Vender的應用里,都有相同的值。其中的Vender是指應用提供商,但準確點說,是通過BundleID的反轉的前兩部分進行匹配,如果相同就是同一個Vender,例如對于com.taobao.app1, com.taobao.app2 這兩個BundleID來說,就屬于同一個Vender,共享同一個idfv的值。和idfa不同的是,idfv的值是一定能取到的,所以非常適合于作為內部用戶行為分析的主id,來標識用戶,替代OpenUDID。
-
注意:如果用戶將屬于此Vender的所有App卸載,則idfv的值會被重置,即再重裝此Vender的App,idfv的值和之前不同。
4、MAC地址
使用WiFi的mac地址來取代已經廢棄了的uniqueIdentifier方法。具體可見:
http://stackoverflow.com/questions/677530/how-can-i-programmatically-get-the-mac-address-of-an-iphone
然而在iOS 7中蘋果再一次無情的封殺mac地址,使用之前的方法獲取到的mac地址全部都變成了02:00:00:00:00:00。
5、Keychain
Paste_Image.png
我們可以獲取到UUID,然后把UUID保存到KeyChain里面。
這樣以后即使APP刪了再裝回來,也可以從KeyChain中讀取回來。使用group還可以可以保證同一個開發商的所有程序針對同一臺設備能夠獲取到相同的不變的UDID。
但是刷機或重裝系統后uuid還是會改變。
把下面兩個類文件放到你的項目中
KeychainItemWrapper.h文件
********************************
#import <UIKit/UIKit.h>
@interface KeychainItemWrapper : NSObject
{
NSMutableDictionary *keychainItemData; // The actual keychain item data backing store.
NSMutableDictionary *genericPasswordQuery; // A placeholder for the generic keychain item query used to locate the item.
}
@property (nonatomic, retain) NSMutableDictionary *keychainItemData;
@property (nonatomic, retain) NSMutableDictionary *genericPasswordQuery;
// Designated initializer.
- (id)initWithAccount:(NSString *)account service:(NSString *)service accessGroup:(NSString *) accessGroup;
- (id)initWithIdentifier: (NSString *)identifier accessGroup:(NSString *) accessGroup;
- (void)setObject:(id)inObject forKey:(id)key;
- (id)objectForKey:(id)key;
// Initializes and resets the default generic keychain item data.
- (void)resetKeychainItem;
@end
KeychainItemWrapper.h文件
********************************
#import "KeychainItemWrapper.h"
#import <Security/Security.h>
/*
These are the default constants and their respective types,
available for the kSecClassGenericPassword Keychain Item class:
kSecAttrAccessGroup - CFStringRef
kSecAttrCreationDate - CFDateRef
kSecAttrModificationDate - CFDateRef
kSecAttrDescription - CFStringRef
kSecAttrComment - CFStringRef
kSecAttrCreator - CFNumberRef
kSecAttrType - CFNumberRef
kSecAttrLabel - CFStringRef
kSecAttrIsInvisible - CFBooleanRef
kSecAttrIsNegative - CFBooleanRef
kSecAttrAccount - CFStringRef
kSecAttrService - CFStringRef
kSecAttrGeneric - CFDataRef
See the header file Security/SecItem.h for more details.
*/
@interface KeychainItemWrapper (PrivateMethods)
/*
The decision behind the following two methods (secItemFormatToDictionary and dictionaryToSecItemFormat) was
to encapsulate the transition between what the detail view controller was expecting (NSString *) and what the
Keychain API expects as a validly constructed container class.
*/
- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert;
- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert;
// Updates the item in the keychain, or adds it if it doesn't exist.
- (void)writeToKeychain;
@end
@implementation KeychainItemWrapper
@synthesize keychainItemData, genericPasswordQuery;
- (id)initWithAccount:(NSString *)account service:(NSString *)service accessGroup:(NSString *) accessGroup;
{
if (self = [super init])
{
NSAssert(account != nil || service != nil, @"Both account and service are nil. Must specifiy at least one.");
// Begin Keychain search setup. The genericPasswordQuery the attributes kSecAttrAccount and
// kSecAttrService are used as unique identifiers differentiating keychain items from one another
genericPasswordQuery = [[NSMutableDictionary alloc] init];
[genericPasswordQuery setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
[genericPasswordQuery setObject:account forKey:(id)kSecAttrAccount];
[genericPasswordQuery setObject:service forKey:(id)kSecAttrService];
// The keychain access group attribute determines if this item can be shared
// amongst multiple apps whose code signing entitlements contain the same keychain access group.
if (accessGroup != nil)
{
#if TARGET_IPHONE_SIMULATOR
// Ignore the access group if running on the iPhone simulator.
//
// Apps that are built for the simulator aren't signed, so there's no keychain access group
// for the simulator to check. This means that all apps can see all keychain items when run
// on the simulator.
//
// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
// simulator will return -25243 (errSecNoAccessForItem).
#else
[genericPasswordQuery setObject:accessGroup forKey:(id)kSecAttrAccessGroup];
#endif
}
// Use the proper search constants, return only the attributes of the first match.
[genericPasswordQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
[genericPasswordQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];
NSDictionary *tempQuery = [NSDictionary dictionaryWithDictionary:genericPasswordQuery];
NSMutableDictionary *outDictionary = nil;
if (! SecItemCopyMatching((CFDictionaryRef)tempQuery, (CFTypeRef *)&outDictionary) == noErr)
{
// Stick these default values into keychain item if nothing found.
[self resetKeychainItem];
//Adding the account and service identifiers to the keychain
[keychainItemData setObject:account forKey:(id)kSecAttrAccount];
[keychainItemData setObject:service forKey:(id)kSecAttrService];
if (accessGroup != nil)
{
#if TARGET_IPHONE_SIMULATOR
// Ignore the access group if running on the iPhone simulator.
//
// Apps that are built for the simulator aren't signed, so there's no keychain access group
// for the simulator to check. This means that all apps can see all keychain items when run
// on the simulator.
//
// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
// simulator will return -25243 (errSecNoAccessForItem).
#else
[keychainItemData setObject:accessGroup forKey:(id)kSecAttrAccessGroup];
#endif
}
}
else
{
// load the saved data from Keychain.
self.keychainItemData = [self secItemFormatToDictionary:outDictionary];
}
[outDictionary release];
}
return self;
}
- (id)initWithIdentifier: (NSString *)identifier accessGroup:(NSString *) accessGroup;
{
if (self = [super init])
{
// Begin Keychain search setup. The genericPasswordQuery leverages the special user
// defined attribute kSecAttrGeneric to distinguish itself between other generic Keychain
// items which may be included by the same application.
genericPasswordQuery = [[NSMutableDictionary alloc] init];
[genericPasswordQuery setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
[genericPasswordQuery setObject:identifier forKey:(id)kSecAttrGeneric];
// The keychain access group attribute determines if this item can be shared
// amongst multiple apps whose code signing entitlements contain the same keychain access group.
if (accessGroup != nil)
{
#if TARGET_IPHONE_SIMULATOR
// Ignore the access group if running on the iPhone simulator.
//
// Apps that are built for the simulator aren't signed, so there's no keychain access group
// for the simulator to check. This means that all apps can see all keychain items when run
// on the simulator.
//
// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
// simulator will return -25243 (errSecNoAccessForItem).
#else
[genericPasswordQuery setObject:accessGroup forKey:(id)kSecAttrAccessGroup];
#endif
}
// Use the proper search constants, return only the attributes of the first match.
[genericPasswordQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
[genericPasswordQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];
NSDictionary *tempQuery = [NSDictionary dictionaryWithDictionary:genericPasswordQuery];
NSMutableDictionary *outDictionary = nil;
if (! SecItemCopyMatching((CFDictionaryRef)tempQuery, (CFTypeRef *)&outDictionary) == noErr)
{
// Stick these default values into keychain item if nothing found.
[self resetKeychainItem];
// Add the generic attribute and the keychain access group.
[keychainItemData setObject:identifier forKey:(id)kSecAttrGeneric];
if (accessGroup != nil)
{
#if TARGET_IPHONE_SIMULATOR
// Ignore the access group if running on the iPhone simulator.
//
// Apps that are built for the simulator aren't signed, so there's no keychain access group
// for the simulator to check. This means that all apps can see all keychain items when run
// on the simulator.
//
// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
// simulator will return -25243 (errSecNoAccessForItem).
#else
[keychainItemData setObject:accessGroup forKey:(id)kSecAttrAccessGroup];
#endif
}
}
else
{
// load the saved data from Keychain.
self.keychainItemData = [self secItemFormatToDictionary:outDictionary];
}
[outDictionary release];
}
return self;
}
- (void)dealloc
{
[keychainItemData release];
[genericPasswordQuery release];
[super dealloc];
}
- (void)setObject:(id)inObject forKey:(id)key
{
if (inObject == nil) return;
id currentObject = [keychainItemData objectForKey:key];
if (![currentObject isEqual:inObject])
{
[keychainItemData setObject:inObject forKey:key];
[self writeToKeychain];
}
}
- (id)objectForKey:(id)key
{
return [keychainItemData objectForKey:key];
}
- (void)resetKeychainItem
{
OSStatus junk = noErr;
if (!keychainItemData)
{
self.keychainItemData = [[NSMutableDictionary alloc] init];
}
else if (keychainItemData)
{
NSMutableDictionary *tempDictionary = [self dictionaryToSecItemFormat:keychainItemData];
junk = SecItemDelete((CFDictionaryRef)tempDictionary);
NSAssert( junk == noErr || junk == errSecItemNotFound, @"Problem deleting current dictionary." );
}
// Default attributes for keychain item.
[keychainItemData setObject:@"" forKey:(id)kSecAttrAccount];
[keychainItemData setObject:@"" forKey:(id)kSecAttrLabel];
[keychainItemData setObject:@"" forKey:(id)kSecAttrDescription];
// Default data for keychain item.
[keychainItemData setObject:@"" forKey:(id)kSecValueData];
}
- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert
{
// The assumption is that this method will be called with a properly populated dictionary
// containing all the right key/value pairs for a SecItem.
// Create a dictionary to return populated with the attributes and data.
NSMutableDictionary *returnDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];
// Add the Generic Password keychain item class attribute.
[returnDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
// Convert the NSString to NSData to meet the requirements for the value type kSecValueData.
// This is where to store sensitive data that should be encrypted.
NSString *passwordString = [dictionaryToConvert objectForKey:(id)kSecValueData];
[returnDictionary setObject:[passwordString dataUsingEncoding:NSUTF8StringEncoding] forKey:(id)kSecValueData];
return returnDictionary;
}
- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert
{
// The assumption is that this method will be called with a properly populated dictionary
// containing all the right key/value pairs for the UI element.
// Create a dictionary to return populated with the attributes and data.
NSMutableDictionary *returnDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];
// Add the proper search key and class attribute.
[returnDictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
[returnDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
// Acquire the password data from the attributes.
NSData *passwordData = NULL;
if (SecItemCopyMatching((CFDictionaryRef)returnDictionary, (CFTypeRef *)&passwordData) == noErr)
{
// Remove the search, class, and identifier key/value, we don't need them anymore.
[returnDictionary removeObjectForKey:(id)kSecReturnData];
// Add the password to the dictionary, converting from NSData to NSString.
NSString *password = [[[NSString alloc] initWithBytes:[passwordData bytes] length:[passwordData length]
encoding:NSUTF8StringEncoding] autorelease];
[returnDictionary setObject:password forKey:(id)kSecValueData];
}
else
{
// Don't do anything if nothing is found.
NSAssert(NO, @"Serious error, no matching item found in the keychain.\n");
}
[passwordData release];
return returnDictionary;
}
- (void)writeToKeychain
{
NSDictionary *attributes = NULL;
NSMutableDictionary *updateItem = NULL;
OSStatus result;
if (SecItemCopyMatching((CFDictionaryRef)genericPasswordQuery, (CFTypeRef *)&attributes) == noErr)
{
// First we need the attributes from the Keychain.
updateItem = [NSMutableDictionary dictionaryWithDictionary:attributes];
// Second we need to add the appropriate search key/values.
[updateItem setObject:[genericPasswordQuery objectForKey:(id)kSecClass] forKey:(id)kSecClass];
// Lastly, we need to set up the updated attribute list being careful to remove the class.
NSMutableDictionary *tempCheck = [self dictionaryToSecItemFormat:keychainItemData];
[tempCheck removeObjectForKey:(id)kSecClass];
#if TARGET_IPHONE_SIMULATOR
// Remove the access group if running on the iPhone simulator.
//
// Apps that are built for the simulator aren't signed, so there's no keychain access group
// for the simulator to check. This means that all apps can see all keychain items when run
// on the simulator.
//
// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
// simulator will return -25243 (errSecNoAccessForItem).
//
// The access group attribute will be included in items returned by SecItemCopyMatching,
// which is why we need to remove it before updating the item.
[tempCheck removeObjectForKey:(id)kSecAttrAccessGroup];
#endif
// An implicit assumption is that you can only update a single item at a time.
result = SecItemUpdate((CFDictionaryRef)updateItem, (CFDictionaryRef)tempCheck);
NSAssert( result == noErr, @"Couldn't update the Keychain Item." );
}
else
{
// No previous item found; add the new one.
result = SecItemAdd((CFDictionaryRef)[self dictionaryToSecItemFormat:keychainItemData], NULL);
NSAssert( result == noErr, @"Couldn't add the Keychain Item." );
}
}
@end
我們在寫一個工具類用來保存UUID到keychain和從keychain中讀取UUID.
實現代碼
AppUntils.m文件
*********************
#import <Security/Security.h>
#import "KeychainItemWrapper.h"
#pragma mark - 保存和讀取UUID
+(void)saveUUIDToKeyChain{
KeychainItemWrapper *keychainItem = [[KeychainItemWrapper alloc] initWithAccount:@"Identfier" service:@"AppName" accessGroup:nil];
NSString *string = [keychainItem objectForKey: (__bridge id)kSecAttrGeneric];
if([string isEqualToString:@""] || !string){
[keychainItem setObject:[self getUUIDString] forKey:(__bridge id)kSecAttrGeneric];
}
}
+(NSString *)readUUIDFromKeyChain{
KeychainItemWrapper *keychainItemm = [[KeychainItemWrapper alloc] initWithAccount:@"Identfier" service:@"AppName" accessGroup:nil];
NSString *UUID = [keychainItemm objectForKey: (__bridge id)kSecAttrGeneric];
return UUID;
}
+ (NSString *)getUUIDString
{
CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault);
CFStringRef strRef = CFUUIDCreateString(kCFAllocatorDefault , uuidRef);
NSString *uuidString = [(__bridge NSString*)strRef stringByReplacingOccurrencesOfString:@"-" withString:@""];
CFRelease(strRef);
CFRelease(uuidRef);
return uuidString;
}
寫入UUID到keychain
我們最好在程序啟動之后把UUID寫入到keychain,代碼如下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[AppUtils saveUUIDToKeyChain];
}
讀取UUID
在需要讀取的地方直接調用AppUtils的類方法 readUUIDFromKeyChain 即可。
注意
1.設置非ARC編譯環境
因為KeychainItemWrapper.m文件是在非ARC環境下運行的,所以需要設置非arc編譯環境,
如圖所示:
image
2. 讓同一開發商的所有APP在同一臺設備上獲取到UUID相同
在每個APP的項目里面做如下設置
2.1、設置accessgroup
keychainItemWrapper *keychainItem = [[KeychainItemWrapper alloc] initWithAccount:@"Identfier" service:@"AppName" accessGroup:@"YOUR_BUNDLE_SEED.com.yourcompany.userinfo"];
此處設置accessGroup為YOUR_BUNDLE_SEED.com.yourcompany.userinfo
2.2、創建plist文件
然后在項目相同的目錄下創建KeychainAccessGroups.plist文件。
該文件的結構是一個字典,其中中最頂層的節點必須是一個鍵為“keychain-access-groups”的Array,并且該Array中每一項都是一個描述分組的NSString。YOUR_BUNDLE_SEED.com.yourcompany.userinfo就是要設置的組名。
如圖:
image
2.3、 設置code signing
接著在Target--->Build Settings---->Code Signing欄下的Code Signing Entitlements右側添加KeychainAccessGroups.plist
如圖:
image
這樣就可以保證每個app都是從keychain中讀取出來同一個UUID
來自:http://www.jianshu.com/p/2741f0124cd3