如何使用KeyChain保存和獲取UDID。iOS7獲取唯一標示符的方法

轉自:http://www.haodaima.net/art/2388057


本文主要介紹使用KeyChain保存和獲取APP數據,解決iOS7上獲取不變UDID的問題。並給出一個獲取UDID的工具類,使用方便,只需要替換兩個地方即可。

一、iOS不用版本獲取UDID的方法比較

  1)iOS 5.0

  iOS 2.0版本以後UIDevice提供一個獲取設備唯一標識符的方法uniqueIdentifier,通過該方法我們可以獲取設備的序列號,這個也是目前爲止唯一可以確認唯一的標示符。好景不長,因爲該唯一標識符與手機一一對應,蘋果覺得可能會泄露用戶隱私,所以在 iOS 5.0之後該方法就被廢棄掉了。

  而且蘋果做的更狠,今年5月份以後提交App Store的產品都不允許再用uniqueIdentifier接口,甚至有些朋友因爲代碼中有UDID還被打回來,看來這條路是被封死了。

  2)iOS 6.0

  iOS 6.0系統新增了兩個用於替換uniqueIdentifier的接口,分別是:identifierForVendor,advertisingIdentifier。

  identifierForVendor接口的官方文檔介紹如下:

The value of this property is the same for apps that come from the same vendor running on the same device. 
A different value is returned for apps on the same device that come from different vendors, and for apps on different devices regardless of vendor. The value of this property may be nil if the app is running in the background, before the user has unlocked the device the first time after the device has been restarted. If the value is nil, wait and get the value again later. The value in this property remains the same while the app (or another app from the same vendor) is installed on the iOS device. The value changes when the user deletes all of that vendor’s apps from the device and subsequently reinstalls one or more of them. Therefore, if your app stores the value of this property anywhere, you should gracefully handle situations where the identifier changes.


大概意思就是「同一開發商的APP在指定機器上都會獲得同一個ID。當我們刪除了某一個設備上某個開發商的所有APP之後,下次獲取將會獲取到不同的ID。」 也就是說我們通過該接口不能獲取用來唯一標識設備的ID,問題總是難不倒聰明的程序員,於是大家想到了使用WiFi的mac地址來取代已經廢棄了的uniqueIdentifier方法。具體的方法晚上有很多,大家感興趣的可以自己找找,這兒提供一個網址:  http://stackoverflow.com/questions/677530/how-can-i-programmatically-get-the-mac-address-of-an-iphone

3)iOS 7.0  

  iOS 7中蘋果再一次無情的封殺mac地址,使用之前的方法獲取到的mac地址全部都變成了02:00:00:00:00:00。有問題總的解決啊,於是四處查資料,終於有了思路是否可以使用KeyChain來保存獲取到的唯一標示符呢,這樣以後即使APP刪了再裝回來,也可以從KeyChain中讀取回來。有了方向以後就開始做,看關於KeyChain的官方文檔,看官方使用KeyChain的Demo,大概花了一下午時間,問題終於解決了。 

二、KeyChain介紹

   我們搞iOS開發,一定都知道OS X裏面的KeyChain(鑰匙串),通常要鄉鎮及調試的話,都得安裝證書之類的,這些證書就是保存在KeyChain中,還有我們平時瀏覽網頁記錄的賬號密碼也都是記錄在KeyChain中。iOS中的KeyChain相比OS X比較簡單,整個系統只有一個KeyChain,每個程序都可以往KeyChain中記錄數據,而且只能讀取到自己程序記錄在KeyChain中的數據。iOS中Security.framework框架提供了四個主要的方法來操作KeyChain:

  1. // 查詢 OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef *result);   
  1. // 添加 OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef *result);   
  1. // 更新KeyChain中的Item OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate);   
  1. // 刪除KeyChain中的Item OSStatus SecItemDelete(CFDictionaryRef query)  

這四個方法參數比較複雜,一旦傳錯就會導致操作KeyChain失敗,這塊兒文檔中介紹的比較詳細,大家可以查查官方文檔Keychain Services Reference

  前面提到了每個APP只允許訪問自己在KeyChain中記錄的數據,那麼是不是就沒有別的辦法訪問其他APP存在KeyChain的數據了?

  蘋果提供了一個方法允許同一個發商的多個APP訪問各APP之間的途徑,即在調SecItemAdd添加數據的時候指定AccessGroup,即訪問組。一個APP可以屬於同事屬於多個分組,添加KeyChain數據訪問組需要做一下兩件事情:

  a、在APP target的bulibSetting裏面設置Code Signing Entitlements,指向包含AceessGroup的分組信息的plist文件。該文件必須和工程文件在同一個目錄下,我在添加訪問分組的時候就因爲plist文件位置問題,操作KeyChain失敗,查找這個問題還花了好久的時間。

  b、在工程目錄下新建一個KeychainAccessGroups.plist文件,該文件的結構中最頂層的節點必須是一個名爲「keychain-access-groups」的Array,並且該Array中每一項都是一個描述分組的NSString。對於String的格式也有相應要求,格式爲:"AppIdentifier.com.***",其中APPIdentifier就是你的開發者帳號對應的ID。

  c、在代碼中往KeyChain中Add數據的時候,設置kSecAttrAccessGroup,代碼如下:

  1. NSString *accessGroup = [NSString stringWithUTF8String:"APPIdentifier.com.cnblogs.smileEvday"];   
  2. if (accessGroup != nil)   
  3. {   
  4. #if TARGET_IPHONE_SIMULATOR   
  5. // Ignore the access group if running on the iPhone simulator.   
  6. // // Apps that are built for the simulator aren't signed, so there's no keychain access group   
  7. // for the simulator to check. This means that all apps can see all keychain items when run   
  8. // on the simulator.   
  9. // // If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the   
  10. // simulator will return -25243 (errSecNoAccessForItem).   
  11. #else   
  12. [dictForQuery setObject:accessGroup forKey:(id)kSecAttrAccessGroup];   
  13. #endif   
  14. }  

這段代碼是從官方的Demo中直接拷貝過來的,根據註釋我們可以看到,模擬器是不支持AccessGroup的,所以才行了預編譯宏來選擇性添加。

  注:appIdentifer就是開發者帳號的那一串標識,如下圖所示:

   

  打開xcode的Organizer,選擇Device選項卡,連接設備就可以看到設備上安裝的開發者賬號描述文件列表,其中第五列最開始的10個字符即爲App Identifier,這塊兒前面寫的不是很清楚,好多朋友加我qq問我,今天特地補上。

 

三、使用KeyChain保存和獲取UDID

  

  說了這麼多終於進入正題了,如何在iOS 7上面獲取到不變的UDID。我們將第二部分所講的知識直接應用進來就可以了輕鬆達到我們要的效果了,下面我們先看看往如何將獲取到的identifierForVendor添加到KeyChain中的代碼。

  1. + (BOOL)settUDIDToKeyChain:(NSString*)udid   
  2. {   
  3. NSMutableDictionary *dictForAdd = [[NSMutableDictionary alloc] init];   
  4. [dictForAdd setValue:(id)kSecClassGenericPassword forKey:(id)kSecClass];   
  5. [dictForAdd setValue:[NSString stringWithUTF8String:kKeychainUDIDItemIdentifier] forKey:kSecAttrDescription];   
  6. [dictForAdd setValue:@"UUID" forKey:(id)kSecAttrGeneric];   
  7. // Default attributes for keychain item.   
  8. [dictForAdd setObject:@"" forKey:(id)kSecAttrAccount];   
  9. [dictForAdd setObject:@"" forKey:(id)kSecAttrLabel];   
  10. // The keychain access group attribute determines if this item can be shared   
  11. // amongst multiple apps whose code signing entitlements contain the same keychain access group.   
  12. NSString *accessGroup = [NSString stringWithUTF8String:kKeyChainUDIDAccessGroup];   
  13. if (accessGroup != nil)   
  14. {   
  15. #if TARGET_IPHONE_SIMULATOR   
  16. // Ignore the access group if running on the iPhone simulator.   
  17. // // Apps that are built for the simulator aren't signed, so there's no keychain access group   
  18. // for the simulator to check. This means that all apps can see all keychain items when run   
  19. // on the simulator.   
  20. // // If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the   
  21. // simulator will return -25243 (errSecNoAccessForItem).   
  22. #else   
  23. [dictForAdd setObject:accessGroup forKey:(id)kSecAttrAccessGroup];   
  24. #endif   
  25. }   
  26. const char *udidStr = [udid UTF8String];   
  27. NSData *keyChainItemValue = [NSData dataWithBytes:udidStr length:strlen(udidStr)];   
  28. [dictForAdd setValue:keyChainItemValue forKey:(id)kSecValueData];   
  29. OSStatus writeErr = noErr;   
  30. if ([SvUDIDTools getUDIDFromKeyChain])   
  31. {   
  32. // there is item in keychain   
  33. [SvUDIDTools updateUDIDInKeyChain:udid];   
  34. [dictForAdd release]; return YES;   
  35. }   
  36. else   
  37. {   
  38. // add item to keychain   
  39. writeErr = SecItemAdd((CFDictionaryRef)dictForAdd, NULL);   
  40. if (writeErr != errSecSuccess)   
  41. {   
  42. NSLog(@"Add KeyChain Item Error!!! Error Code:%ld", writeErr);   
  43. [dictForAdd release];   
  44. return NO;   
  45. }   
  46. else   
  47. {   
  48. NSLog(@"Add KeyChain Item Success!!!");   
  49. [dictForAdd release]; return YES;   
  50. }   
  51. }   
  52. [dictForAdd release];   
  53. return NO;   
  54. }  

上面代碼中,首先構建一個要添加到KeyChain中數據的Dictionary,包含一些基本的KeyChain Item的數據類型,描述,訪問分組以及最重要的數據等信息,最後通過調用SecItemAdd方法將我們需要保存的UUID保存到KeyChain中。

  獲取KeyChain中相應數據的代碼如下:

  1. + (NSString*)getUDIDFromKeyChain   
  2. {   
  3. NSMutableDictionary *dictForQuery = [[NSMutableDictionary alloc] init];   
  4. [dictForQuery setValue:(id)kSecClassGenericPassword forKey:(id)kSecClass];   
  5. // set Attr Description for query   
  6. [dictForQuery setValue:[NSString stringWithUTF8String:kKeychainUDIDItemIdentifier] forKey:kSecAttrDescription];   
  7. // set Attr Identity for query   
  8. NSData *keychainItemID = [NSData dataWithBytes:kKeychainUDIDItemIdentifier length:strlen(kKeychainUDIDItemIdentifier)];   
  9. [dictForQuery setObject:keychainItemID forKey:(id)kSecAttrGeneric];   
  10. // The keychain access group attribute determines if this item can be shared   
  11. // amongst multiple apps whose code signing entitlements contain the same keychain access group.   
  12. NSString *accessGroup = [NSString stringWithUTF8String:kKeyChainUDIDAccessGroup];   
  13. if (accessGroup != nil)   
  14. {   
  15. #if TARGET_IPHONE_SIMULATOR   
  16. // Ignore the access group if running on the iPhone simulator.   
  17. // // Apps that are built for the simulator aren't signed, so there's no keychain access group   
  18. // for the simulator to check. This means that all apps can see all keychain items when run   
  19. // on the simulator.   
  20. // // If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the   
  21. // simulator will return -25243 (errSecNoAccessForItem).   
  22. #else   
  23. [dictForQuery setObject:accessGroup forKey:(id)kSecAttrAccessGroup];   
  24. #endif   
  25. }   
  26. [dictForQuery setValue:(id)kCFBooleanTrue forKey:(id)kSecMatchCaseInsensitive];   
  27. [dictForQuery setValue:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];   
  28. [dictForQuery setValue:(id)kCFBooleanTrue forKey:(id)kSecReturnData];   
  29. OSStatus queryErr = noErr;   
  30. NSData *udidValue = nil;   
  31. NSString *udid = nil;   
  32. queryErr = SecItemCopyMatching((CFDictionaryRef)dictForQuery, (CFTypeRef*)&udidValue);   
  33. NSMutableDictionary *dict = nil;   
  34. [dictForQuery setValue:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];   
  35. queryErr = SecItemCopyMatching((CFDictionaryRef)dictForQuery, (CFTypeRef*)&dict);   
  36. if (queryErr == errSecItemNotFound)   
  37. {   
  38. NSLog(@"KeyChain Item: %@ not found!!!", [NSString stringWithUTF8String:kKeychainUDIDItemIdentifier]);   
  39. }   
  40. else if (queryErr != errSecSuccess)   
  41. {   
  42. NSLog(@"KeyChain Item query Error!!! Error code:%ld", queryErr);   
  43. }   
  44. if (queryErr == errSecSuccess)   
  45. {   
  46. NSLog(@"KeyChain Item: %@", udidValue);  
  47. if (udidValue)   
  48. {   
  49. udid = [NSString stringWithUTF8String:udidValue.bytes];   
  50. }   
  51. }   
  52. [dictForQuery release];   
  53. return udid;   
  54. }  

上面代碼的流程也差不多一樣,首先創建一個Dictionary,其中設置一下查找條件,然後通過SecItemCopyMatching方法獲取到我們之前保存到KeyChain中的數據。

  

四、總結

  本文介紹了使用KeyChain實現APP刪除後依然可以獲取到相同的UDID信息的解決方法。

  你可能有疑問,如果系統升級以後,是否仍然可以獲取到之前記錄的UDID數據?

  答案是肯定的,這一點我專門做了測試。就算我們程序刪除掉,系統經過升級以後再安裝回來,依舊可以獲取到與之前一致的UDID。但是當我們把整個系統還原以後是否還能獲取到之前記錄的UDID,這一點我覺得應該不行,不過手機裏面數據太多,沒有測試,如果大家有興趣可以測試一下,驗證一下我的猜想。

  

  完整代碼地址: https://github.com/smileEvday/SvUDID

  大家如果要在真機運行時,需要替換兩個地方:

  第一個地方是plist文件中的accessGroup中的APPIdentifier。

  第二個地方是SvUDIDTools.m中的kKeyChainUDIDAccessGroup的APPIdentity爲你所使用的profile的APPIdentifier。

  文章和代碼中如果有什麼不對的地方,歡迎指正,在這兒先謝過了。