iOS獲取設(shè)備的唯一標(biāo)識的方法總結(jié) 2020年可用方案 用不了的我就不寫出來了哈.
一.UUID(Universally Unique Identifier)
UUID是Universally Unique Identifier的縮寫,中文意思是通用唯一識別碼。它是讓分布式系統(tǒng)中的所有元素,都能有唯一的辨識資訊,而不需要透過中央控制端來做辨識資訊的指定。這樣,每個人都可以建立不與其它人沖突的 UUID。在此情況下,就不需考慮數(shù)據(jù)庫建立時的名稱重復(fù)問題。蘋果公司建議使用UUID為應(yīng)用生成唯一標(biāo)識字符串。
獲得的UUID值系統(tǒng)沒有存儲, 而且每次調(diào)用得到UUID,系統(tǒng)都會返回一個新的唯一標(biāo)示符。如果你希望存儲這個標(biāo)示符,那么需要自己將其存儲到NSUserDefaults, Keychain, Pasteboard或其它地方。
CFUUID
從iOS2.0開始,CFUUID就已經(jīng)出現(xiàn)了。它是CoreFoundatio包的一部分,因此API屬于C語言風(fēng)格。CFUUIDCreate 方法用來創(chuàng)建CFUUIDRef,并且可以獲得一個相應(yīng)的NSString,如下代碼:
CFUUIDRef cfuuid = CFUUIDCreate(kCFAllocatorDefault);
NSString *cfuuidString = (NSString*)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault,cfuuid));
獲得的這個CFUUID值系統(tǒng)并沒有存儲。每次調(diào)用CFUUIDCreate,系統(tǒng)都會返回一個新的唯一標(biāo)示符。如果你希望存儲這個標(biāo)示符,那么需要自己將其存儲到NSUserDefaults, Keychain, Pasteboard或其它地方。
NSUUID
NSUUID在iOS 6中才出現(xiàn),這跟CFUUID幾乎完全一樣,只不過它是Objective-C接口。+ (id)UUID 是一個類方法,調(diào)用該方法可以獲得一個UUID。通過下面的代碼可以獲得一個UUID字符串:
NSString *uuid = [[NSUUID UUID] UUIDString];
跟CFUUID一樣,這個值系統(tǒng)也不會存儲,每次調(diào)用的時候都會獲得一個新的唯一標(biāo)示符。如果要存儲的話,你需要自己存儲。在我讀取NSUUID時,注意到獲取到的這個值跟CFUUID完全一樣(不過也可能不一樣)
二.廣告標(biāo)示符(IDFA-identifierForIdentifier)
廣告標(biāo)示符,在同一個設(shè)備上的所有App都會取到相同的值,是蘋果專門給各廣告提供商用來追蹤用戶而設(shè)的。但好在Apple默認(rèn)是允許追蹤的,而且一般用戶都不知道有這么個設(shè)置,所以基本上用來監(jiān)測推廣效果,是戳戳有余了。
它是iOS 6中另外一個新的方法,提供了一個方法advertisingIdentifier,通過調(diào)用該方法會返回一個NSUUID實例,最后可以獲得一個UUID,由系統(tǒng)存儲著的。
#import <AdSupport/AdSupport.h>
NSString *adId = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
不過即使這是由系統(tǒng)存儲的,但是有幾種情況下,會重新生成廣告標(biāo)示符。
(1)如果用戶完全重置系統(tǒng)((設(shè)置程序 -> 通用 -> 還原 -> 還原位置與隱私) ,這個廣告標(biāo)示符會重新生成。
(2)另外如果用戶明確的還原廣告(設(shè)置程序-> 通用 -> 關(guān)于本機(jī) -> 廣告 -> 還原廣告標(biāo)示符) ,那么廣告標(biāo)示符也會重新生成。
關(guān)于廣告標(biāo)示符的還原,有一點需要注意:如果程序在后臺運行,此時用戶“還原廣告標(biāo)示符”,然后再回到程序中,此時獲取廣 告標(biāo)示符并不會立即獲得還原后的標(biāo)示符。必須要終止程序,然后再重新啟動程序,才能獲得還原后的廣告標(biāo)示符。
所以IDFA也不可以作為獲取唯一標(biāo)識的方法,來識別用戶。
三.Vendor標(biāo)示符 (IDFV-identifierForVendor)
Vendor標(biāo)示符,是給Vendor標(biāo)識用戶用的,每個設(shè)備在所屬同一個Vender的應(yīng)用里,都有相同的值。其中的Vender是指應(yīng)用提供商,但準(zhǔn)確點說,是通過BundleID的反轉(zhuǎn)的前兩部分進(jìn)行匹配,如果相同就是同一個Vender,例如對于com.taobao.app1, com.taobao.app2 這兩個BundleID來說,就屬于同一個Vender,共享同一個IDFV的值。和IDFA不同的是,IDFV的值是一定能取到的,所以非常適合于作為內(nèi)部用戶行為分析的主id,來標(biāo)識用戶,替代OpenUDID。
它是iOS 6中新增的,跟advertisingIdentifier一樣,該方法返回的是一個 NSUUID對象,可以獲得一個UUID。如果滿足條件“相同的一個程序里面-相同的vendor-相同的設(shè)備”,那么獲取到的這個屬性值就不會變。如果是“相同的程序-相同的設(shè)備-不同的vendor,或者是相同的程序-不同的設(shè)備-無論是否相同的vendor”這樣的情況,那么這個值是不會相同的。
NSString *strIDFV = [[[UIDevice currentDevice]identifierForVendor]UUIDString];
但是如果用戶將屬于此Vender的所有App卸載,則IDFV的值會被重置,即再重裝此Vender的App,IDFV的值和之前不同。
四.推送token+bundle_id
推送token+bundle_id的方法:
1、應(yīng)用中增加推送用來獲取token
2、獲取應(yīng)用bundle_id
3、根據(jù)token+bundle_id進(jìn)行散列運算
apple push token保證設(shè)備唯一,但必須有網(wǎng)絡(luò)情況下才能工作,該方法并不是依賴于設(shè)備本身,而是依賴于apple push機(jī)制,所以當(dāng)蘋果push做出改變時, 你獲取所謂的唯一標(biāo)識也就隨之失效了。所以此方法還是不可取的。
如何正確的獲取設(shè)備的唯一標(biāo)識
我用的方法是將獲取的UUID永久存儲在設(shè)備的KeyChain中, 這個方法在應(yīng)用第一次啟動時, 將獲取的UUID存儲進(jìn)KeyChain中, 每次取的時候, 檢查本地鑰匙串中有沒有, 如果沒有則需要將獲取的UUID存儲進(jìn)去。當(dāng)你重啟設(shè)備, 卸載應(yīng)用再次安裝,都不影響, 只是當(dāng)設(shè)備刷機(jī)時, KeyChain會清空, 才會消失, 才會失效。
不只是這一種方法, 你也可以保存除UUID之外,其他合適的標(biāo)識, 但利用KeyChain去存儲標(biāo)識的方式應(yīng)該是最接近的。
利用keyChain和UUID永久獲得設(shè)備的唯一標(biāo)識
開發(fā)者可以在應(yīng)用第一次啟動時調(diào)用一次,然后將該串存儲起來,以便以后替代UDID來使用。但是,如果用戶刪除該應(yīng)用再次安裝時,又會生成新的字符串,所以不能保證唯一識別該設(shè)備。這就需要各路高手想出各種解決方案。所以,之前很多應(yīng)用就采用MAC Address。但是現(xiàn)在如果用戶升級到iOS7(及其以后的蘋果系統(tǒng))后,他們機(jī)子的MAC Address就是一樣的,沒辦法做區(qū)分,只能棄用此方法,重新使用UUID來標(biāo)識。如果使用UUID,就要考慮應(yīng)用被刪除后再重新安裝時的處理。
什么是鑰匙串?
在應(yīng)用間利用KeyChain共享數(shù)據(jù)
我們可以把KeyChain理解為一個Dictionary,所有數(shù)據(jù)都以key-value的形式存儲,可以對這個Dictionary進(jìn)行add、update、get、delete這四個操作。對于每一個應(yīng)用來說,KeyChain都有兩個訪問區(qū),私有區(qū)和公共區(qū)。私有區(qū)是一個sandbox,本程序存儲的任何數(shù)據(jù)都對其他程序不可見。而要想在將存儲的內(nèi)容放在公共區(qū),需要先聲明公共區(qū)的名稱,官方文檔管這個名稱叫“keychain access group”,聲明的方法是新建一個plist文件,名字隨便起,內(nèi)容如下

"yourAppID.com.yourCompany.whatever"就是你要起的公共區(qū)名稱,除了whatever字段可以隨便定之外,其他的都必須如實填寫。這個文件的路徑要配置在 Project->build setting->Code Signing Entitlements里,否則公共區(qū)無效,配置好后,須用你正式的證書簽名編譯才可通過,否則xcode會彈框告訴你code signing有問題。所以,蘋果限制了你只能同公司的產(chǎn)品共享KeyChain數(shù)據(jù),別的公司訪問不了你公司產(chǎn)品的KeyChain。
保存私密信息
iOS的Keychain服務(wù)提供了一種安全的保存私密信息(密碼,序列號,證書等)的方式,每個ios程序都有一個獨立的keychain存儲。相對于NSUserDefaults、文件保存等一般方式,keychain保存更為安全,而且keychain里保存的信息不會因App被刪除而丟失,所以在重裝App后,keychain里的數(shù)據(jù)還能使用。
項目配置
在項目的 Build Phases中添加Security.framework庫
在項目的 Signing & Capabilities中添加Keychain Sharing,加入Keychain Groups分組。每個app都設(shè)置相同的Keychain Groups值

實現(xiàn)代碼
介紹下我使用的方法以及封裝的工具類, 在應(yīng)用里使用使用keyChain,我們需要導(dǎo)入Security.framework。下面介紹下, 我在其他庫基礎(chǔ)上封裝的一個獲取唯一標(biāo)識的工具類:
#import <Foundation/Foundation.h>
#import <Security/Security.h>
@interface BGKeychainTool : NSObject
/**
本方法是得到 UUID 后存入系統(tǒng)中的 keychain 的方法
不用添加 plist 文件
程序刪除后重裝,仍可以得到相同的唯一標(biāo)示
但是當(dāng)系統(tǒng)升級或者刷機(jī)后,系統(tǒng)中的鑰匙串會被清空,此時本方法失效
*/
+(NSString*)getDeviceIDInKeychain;
@end
.m 文件實現(xiàn)
#import "BGKeychainTool.h"
@implementation BGKeychainTool
+(NSString *)getDeviceIDInKeychain {
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];//獲取app版本信息
NSString *key = [NSString stringWithFormat:@"%@.uniqueid",[infoDictionary objectForKey:@"CFBundleIdentifier"]];
NSString *getUDIDInKeychain = (NSString *)[BGKeychainTool load:key];
NSLog(@"從keychain中獲取到的 UDID_INSTEAD %@",getUDIDInKeychain);
if (!getUDIDInKeychain || [getUDIDInKeychain isEqualToString:@""] || [getUDIDInKeychain isKindOfClass:[NSNull class]]) {
getUDIDInKeychain = [UIDevice currentDevice].identifierForVendor.UUIDString;
if(getUDIDInKeychain.length == 0 || [getUDIDInKeychain isEqualToString:@"00000000-0000-0000-0000-000000000000"]) {
CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault);
CFStringRef uuidString = CFUUIDCreateString(kCFAllocatorDefault, uuidRef);
getUDIDInKeychain = (NSString *)CFBridgingRelease(CFStringCreateCopy(NULL, uuidString));
CFRelease(uuidRef);
CFRelease(uuidString);
NSLog(@"\n \n \n _____重新存儲 UUID _____\n \n \n %@",getUDIDInKeychain);
}
[BGKeychainTool save:key data:getUDIDInKeychain];
}
NSLog(@"最終 ———— UDID_INSTEAD %@",getUDIDInKeychain);
return getUDIDInKeychain;
}
下面有幾個私有方法
通過 key 獲取數(shù)據(jù)
+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service {
return [NSMutableDictionary dictionaryWithObjectsAndKeys:
(id)kSecClassGenericPassword,(id)kSecClass,
service, (id)kSecAttrService,
service, (id)kSecAttrAccount,
(id)kSecAttrAccessibleAfterFirstUnlock,(id)kSecAttrAccessible,
nil];
}
保存數(shù)據(jù)至鑰匙串中
+(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);
}
從鑰匙串中加載數(shù)據(jù)
+ (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;
}
刪除數(shù)據(jù)
+ (void)delete:(NSString *)service {
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
SecItemDelete((CFDictionaryRef)keychainQuery);
}
首次編譯

再次編譯
