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。在此情況下,就不需考慮數據庫建立時的名稱重復問題。蘋果公司建議使用UUID為應用生成唯一標識字符串。
獲得的UUID值系統沒有存儲, 而且每次調用得到UUID,系統都會返回一個新的唯一標示符。如果你希望存儲這個標示符,那麼需要自己將其存儲到NSUserDefaults, Keychain, Pasteboard或其它地方。
從iOS2.0開始,CFUUID就已經出現了。它是CoreFoundatio包的一部分,因此API屬於C語言風格。CFUUIDCreate 方法用來創建CFUUIDRef,並且可以獲得一個相應的NSString,如下代碼:
CFUUIDRef cfuuid = CFUUIDCreate(kCFAllocatorDefault);NSString *cfuuidString = (NSString*)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, cfuuid));
獲得的這個CFUUID值系統並沒有存儲。每次調用CFUUIDCreate,系統都會返回一個新的唯一標示符。如果你希望存儲這個標示符,那麼需要自己將其存儲到NSUserDefaults, Keychain, Pasteboard或其它地方。
NSUUID在iOS 6中才出現,這跟CFUUID幾乎完全一樣,只不過它是Objective-C接口。+ (id)UUID 是一個類方法,調用該方法可以獲得一個UUID。通過下面的代碼可以獲得一個UUID字符串:
NSString *uuid = [[NSUUID UUID] UUIDString];
跟CFUUID一樣,這個值系統也不會存儲,每次調用的時候都會獲得一個新的唯一標示符。如果要存儲的話,你需要自己存儲。在我讀取NSUUID時,注意到獲取到的這個值跟CFUUID完全一樣(不過也可能不一樣)
在iOS 5發布時,uniqueIdentifier被棄用了,這引起了廣大開發者需要尋找一個可以替代UDID,並且不受蘋果控制的方案。由此OpenUDID成為了當時使用最廣泛的開源UDID替代方案。OpenUDID在工程中實現起來非常簡單,並且還支持一系列的廣告提供商。
OpenUDID利用了一個非常巧妙的方法在不同程序間存儲標示符 — 在粘貼板中用了一個特殊的名稱來存儲標示符。通過這種方法,別的程序(同樣使用了OpenUDID)知道去什麼地方獲取已經生成的標示符(而不用再生成一個新的)。而且根據貢獻者的代碼和方法,和一些開發者的經驗,如果把使用了OpenUDID方案的應用全部都刪除,再重新獲取OpenUDID,此時的OpenUDID就跟以前的不一樣。可見,這種方法還是不保險。
但是OpenUDID庫早已經棄用了, 在其官方的博客中也指明了, 停止維護OpenUDID的原因是為了更好的向蘋果的舉措靠攏, 還指明了MAC Address不是一個好的選擇。
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又能在服務器端保存相應的信息,省去用戶名、密碼等注冊過程。
主要分三種:
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 是不能夠被使用為獲取設備唯一標識的。
廣告標示符,在同一個設備上的所有App都會取到相同的值,是蘋果專門給各廣告提供商用來追蹤用戶而設的。但好在Apple默認是允許追蹤的,而且一般用戶都不知道有這麼個設置,所以基本上用來監測推廣效果,是戳戳有余了。
它是iOS 6中另外一個新的方法,提供了一個方法advertisingIdentifier,通過調用該方法會返回一個NSUUID實例,最後可以獲得一個UUID,由系統存儲著的。
#import NSString *adId = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
不過即使這是由系統存儲的,但是有幾種情況下,會重新生成廣告標示符。如果用戶完全重置系統((設置程序 -> 通用 -> 還原 -> 還原位置與隱私) ,這個廣告標示符會重新生成。另外如果用戶明確的還原廣告(設置程序-> 通用 -> 關於本機 -> 廣告 -> 還原廣告標示符) ,那麼廣告標示符也會重新生成。
關於廣告標示符的還原,有一點需要注意:如果程序在後台運行,此時用戶“還原廣告標示符”,然後再回到程序中,此時獲取廣 告標示符並不會立即獲得還原後的標示符。必須要終止程序,然後再重新啟動程序,才能獲得還原後的廣告標示符。
所以IDFA也不可以作為獲取唯一標識的方法,來識別用戶。
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的方法:
1、應用中增加推送用來獲取token
2、獲取應用bundle_id
3、根據token+bundle_id進行散列運算
apple push token保證設備唯一,但必須有網絡情況下才能工作,該方法並不是依賴於設備本身,而是依賴於apple push機制,所以當蘋果push做出改變時, 你獲取所謂的唯一標識也就隨之失效了。所以此方法還是不可取的。
首次運行
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去存儲標識的方式應該是最接近的。
開發者可以在應用第一次啟動時調用一次,然後將該串存儲起來,以便以後替代UDID來使用。但是,如果用戶刪除該應用再次安裝時,又會生成新的字符串,所以不能保證唯一識別該設備。這就需要各路高手想出各種解決方案。所以,之前很多應用就采用MAC Address。但是現在如果用戶升級到iOS7(及其以後的蘋果系統)後,他們機子的MAC Address就是一樣的,沒辦法做區分,只能棄用此方法,重新使用UUID來標識。如果使用UUID,就要考慮應用被刪除後再重新安裝時的處理。
一、在應用間利用KeyChain共享數據
我們可以把KeyChain理解為一個Dictionary,所有數據都以key-value的形式存儲,可以對這個Dictionary進行add、update、get、delete這四個操作。對於每一個應用來說,KeyChain都有兩個訪問區,私有區和公共區。私有區是一個sandbox,本程序存儲的任何數據都對其他程序不可見。而要想在將存儲的內容放在公共區,需要先聲明公共區的名稱,官方文檔管這個名稱叫“keychain access group”,聲明的方法是新建一個plist文件,名字隨便起,內容如下:
“yourAppID.com.yourCompany.whatever”就是你要起的公共區名稱,除了whatever字段可以隨便定之外,其他的都必須如實填寫。這個文件的路徑要配置在 Project->build setting->Code Signing Entitlements裡,否則公共區無效,配置好後,須用你正式的證書簽名編譯才可通過,否則xcode會彈框告訴你code signing有問題。所以,蘋果限制了你只能同公司的產品共享KeyChain數據,別的公司訪問不了你公司產品的KeyChain。
二、保存私密信息
iOS的keychain服務提供了一種安全的保存私密信息(密碼,序列號,證書等)的方式,每個ios程序都有一個獨立的keychain存儲。相對於NSUserDefaults、文件保存等一般方式,keychain保存更為安全,而且keychain裡保存的信息不會因App被刪除而丟失,所以在重裝App後,keychain裡的數據還能使用。
首先, 我先推薦兩篇文章,裡面介紹了如利用keyChain和UUID永久獲得設備的唯一標識, Classes/KeychainItemWrapper.m和 如何使用KeyChain保存和獲取UDID 。
然後, 再介紹下我使用的方法以及封裝的工具類, 在應用裡使用使用keyChain,我們需要導入Security.framework。下面介紹下, 我在其他庫基礎上封裝的一個獲取唯一標識的工具類:
#import#import NSString * const KEY_UDID_INSTEAD = @"com.myapp.udid.test"; @interface LZKeychain : NSObject /** 本方法是得到 UUID 後存入系統中的 keychain 的方法 不用添加 plist 文件 程序刪除後重裝,仍可以得到相同的唯一標示 但是當系統升級或者刷機後,系統中的鑰匙串會被清空,此時本方法失效 */ +(NSString *)getDeviceIDInKeychain; @end
#import "LZKeychain.h" @implementation LZKeychain +(NSString *)getDeviceIDInKeychain { NSString *getUDIDInKeychain = (NSString *)[LZKeychain load:KEY_UDID_INSTEAD]; NSLog(@"從keychain中獲取到的 UDID_INSTEAD %@",getUDIDInKeychain); if (!getUDIDInKeychain ||[getUDIDInKeychain isEqualToString:@""]||[getUDIDInKeychain isKindOfClass:[NSNull class]]) { CFUUIDRef puuid = CFUUIDCreate( nil ); CFStringRef uuidString = CFUUIDCreateString( nil, puuid ); NSString * result = (NSString *)CFBridgingRelease(CFStringCreateCopy( NULL, uuidString)); CFRelease(puuid); CFRelease(uuidString); NSLog(@"\n \n \n _____重新存儲 UUID _____\n \n \n %@",result); [LZKeychain save:KEY_UDID_INSTEAD data:result]; getUDIDInKeychain = (NSString *)[LZKeychain load:KEY_UDID_INSTEAD]; } NSLog(@"最終 ———— UDID_INSTEAD %@",getUDIDInKeychain); return getUDIDInKeychain; } #pragma mark - private + (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:(NSData *)keyData]; } @catch (NSException *e) { NSLog(@"Unarchive of %@ failed: %@", service, e); } @finally { } } if (keyData) CFRelease(keyData); return ret; } + (void)delete:(NSString *)service { NSMutableDictionary *keychainQuery = [self getKeychainQuery:service]; SecItemDelete((CFDictionaryRef)keychainQuery); } @end