背景
??近期公司app要加入類(lèi)似統(tǒng)計(jì)的功能,把設(shè)備的唯一標(biāo)志符傳遞給服務(wù)器,用以統(tǒng)計(jì)未登錄用戶(hù)的操作行為。所以做了一些調(diào)研,把iOS中的各種可能碰到的標(biāo)志符的定義及使用情況羅列出來(lái),最后附上獲取唯一標(biāo)志符的辦法。
幾大標(biāo)志符
1、UDID(Unique Device Identifier)
??UDID的全稱(chēng)是Unique Device Identifier,它就是蘋(píng)果IOS設(shè)備的唯一識(shí)別碼,它由40個(gè)字符的字母和數(shù)字組成。移動(dòng)網(wǎng)絡(luò)可利用UDID來(lái)識(shí)別移動(dòng)設(shè)備,但是,從IOS5.0(2011年8月份)開(kāi)始,蘋(píng)果宣布將不再支持用uniqueIdentifier方法獲取設(shè)備的UDID,iOS5以下是可以用的。在2013年3月21日蘋(píng)果已經(jīng)通知開(kāi)發(fā)者:從2013年5月1日起,訪(fǎng)問(wèn)UIDIDs的程序?qū)⒉辉俦粚徍送ㄟ^(guò),替代的方案是開(kāi)發(fā)者應(yīng)該使用“在iOS 6中介紹的Vendor或Advertising標(biāo)示符”。
??UDID可以在iTunes中查看,多次點(diǎn)擊ECID后,該位置會(huì)顯示為UDID,即為設(shè)備的唯一標(biāo)志符,且永久不變(當(dāng)然越獄設(shè)備通過(guò)某種手段還是可以修改的),UDID可以在ipa包的測(cè)試配置文件中添加,以達(dá)到多人測(cè)試的目的。

2、UUID(Universally Unique Identifier)
??UUID是Universally Unique Identifier的縮寫(xiě),中文意思是通用唯一識(shí)別碼。它是讓分布式系統(tǒng)中的所有元素,都能有唯一的辨識(shí)資訊,而不需要透過(guò)中央控制端來(lái)做辨識(shí)資訊的指定。這樣,每個(gè)人都可以建立不與其它人沖突的 UUID。在此情況下,就不需考慮數(shù)據(jù)庫(kù)建立時(shí)的名稱(chēng)重復(fù)問(wèn)題。蘋(píng)果公司建議使用UUID為應(yīng)用生成唯一標(biāo)識(shí)字符串。每次生成都會(huì)改變。
??生成方法:
- (NSString *) uuid {
CFUUIDRef puuid = CFUUIDCreate( nil );
CFStringRef uuidString = CFUUIDCreateString( nil, puuid );
NSString * result = (NSString *)CFStringCreateCopy( NULL, uuidString);
CFRelease(puuid);
CFRelease(uuidString);
return [result autorelease];
}
3、Vendor標(biāo)示符 (IDFV-identifierForVendor)
??Vendor標(biāo)示符,也是在iOS 6中新增的,跟advertisingIdentifier一樣,該方法返回的是一個(gè) NSUUID對(duì)象,可以獲得一個(gè)UUID。如果滿(mǎn)足條件“相同的一個(gè)程序里面-相同的vendor-相同的設(shè)備”,那么獲取到的這個(gè)屬性值就不會(huì)變。如果是“相同的程序-相同的設(shè)備-不同的vendor,或者是相同的程序-不同的設(shè)備-無(wú)論是否相同的vendor”這樣的情況,那么這個(gè)值是不會(huì)相同的。
NSUUID * currentDeviceUUID = [UIDevice currentDevice].identifierForVendor;
??以上就是獲取Vendor標(biāo)志符方法,返回一個(gè)UUID(UUID是標(biāo)志符的一個(gè)種類(lèi),而Vendor標(biāo)志符是UUID的一個(gè)子類(lèi))。
這里生成的是NSUUID,這里解釋下CFUUID和NSUUID。
??CFUUID從iOS2.0開(kāi)始,CFUUID就已經(jīng)出現(xiàn)了。它是CoreFoundatio包的一部分,因此API屬于C語(yǔ)言風(fēng)。
??NSUUID在iOS 6中才出現(xiàn),這跟CFUUID幾乎完全一樣,只不過(guò)它是Objective-C接口。
4、MAC Address
??MAC(Medium/Media Access Control)地址,用來(lái)表示互聯(lián)網(wǎng)上每一個(gè)站點(diǎn)的標(biāo)識(shí)符,采用十六進(jìn)制數(shù)表示,共六個(gè)字節(jié)(48位)。其中,前三個(gè)字節(jié)是由IEEE的注冊(cè)管理機(jī)構(gòu) RA負(fù)責(zé)給不同廠(chǎng)家分配的代碼(高位24位),也稱(chēng)為“編制上唯一的標(biāo)識(shí)符” (Organizationally Unique Identifier),后三個(gè)字節(jié)(低位24位)由各廠(chǎng)家自行指派給生產(chǎn)的適配器接口,稱(chēng)為擴(kuò)展標(biāo)識(shí)符(唯一性)。
??MAC地址在網(wǎng)絡(luò)上用來(lái)區(qū)分設(shè)備的唯一性,接入網(wǎng)絡(luò)的設(shè)備都有一個(gè)MAC地址,他們肯定都是不同的,是唯一的。一部iPhone上可能有多個(gè)MAC地址,包括WIFI的、SIM的等,但是iTouch和iPad上就有一個(gè)WIFI的,因此只需獲取WIFI的MAC地址就好了,也就是en0的地址。
??形象的說(shuō),MAC地址就如同我們身份證上的身份證號(hào)碼,具有全球唯一性。這樣就可以非常好的標(biāo)識(shí)設(shè)備唯一性,類(lèi)似與蘋(píng)果設(shè)備的UDID號(hào),通常的用途有:1)用于一些統(tǒng)計(jì)與分析目的,利用用戶(hù)的操作習(xí)慣和數(shù)據(jù)更好的規(guī)劃產(chǎn)品;2)作為用戶(hù)ID來(lái)唯一識(shí)別用戶(hù),可以用游客身份使用app又能在服務(wù)器端保存相應(yīng)的信息,省去用戶(hù)名、密碼等注冊(cè)過(guò)程。
??iOS7之前,因?yàn)镸ac地址是唯一的, 一般app開(kāi)發(fā)者會(huì)采取獲取MAC地址的方式來(lái)識(shí)別安裝對(duì)應(yīng)app的設(shè)備。但iOS7之后,獲取到的MAC地址全部都變?yōu)?2:00:00:00:00:00,也就不存在唯一標(biāo)志一說(shuō)了。
??獲取MAC地址的方法:
#include <sys/sysctl.h>
#include <net/if.h>
#include <net/if_dl.h>
- (NSString *) macAddress {
int mib[6];
size_t len;
char *buf;
unsigned char *ptr;
struct if_msghdr *ifm;
struct sockaddr_dl *sdl;
mib[0] = CTL_NET;
mib[1] = AF_ROUTE;
mib[2] = 0;
mib[3] = AF_LINK;
mib[4] = NET_RT_IFLIST;
if ((mib[5] = if_nametoindex("en0")) == 0) {
printf("Error: if_nametoindex error/n");
return NULL;
}
if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) {
printf("Error: sysctl, take 1/n");
return NULL;
}
if ((buf = malloc(len)) == NULL) {
printf("Could not allocate memory. error!/n");
return NULL;
}
if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) {
printf("Error: sysctl, take 2");
return NULL;
}
ifm = (struct if_msghdr *)buf;
sdl = (struct sockaddr_dl *)(ifm + 1);
ptr = (unsigned char *)LLADDR(sdl);
NSString *outstring = [NSString stringWithFormat:@"%02x:%02x:%02x:%02x:%02x:%02x", *ptr, *(ptr+1), *(ptr+2), *(ptr+3), *(ptr+4), *(ptr+5)];
// NSString *outstring = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x", *ptr, *(ptr+1), *(ptr+2), *(ptr+3), *(ptr+4), *(ptr+5)];
NSLog(@"outString:%@", outstring);
free(buf);
return [outstring uppercaseString];
}
5、OPEN UDID
??OPEN UDID能保證同一臺(tái)設(shè)備上的不同應(yīng)用使用同一個(gè)OpenUDID,只要用戶(hù)設(shè)備上有一個(gè)使用了OpenUDID的應(yīng)用存在時(shí),其他后續(xù)安裝的應(yīng)用如果獲取OpenUDID,都將會(huì)獲得第一個(gè)應(yīng)用生成的那個(gè)。但是如果把使用了OpenUDID的應(yīng)用全部都刪除,再重新獲取OpenUDID,此時(shí)的OpenUDID就跟以前的不一樣。
6、廣告標(biāo)示符(IDFA-identifierForIdentifier)
??廣告標(biāo)示符,是iOS 6中另外一個(gè)新的方法,提供了一個(gè)方法advertisingIdentifier,通過(guò)調(diào)用該方法會(huì)返回一個(gè)NSUUID實(shí)例,最后可以獲得一個(gè)UUID,由系統(tǒng)存儲(chǔ)著的。不過(guò)即使這是由系統(tǒng)存儲(chǔ)的,但是有幾種情況下,會(huì)重新生成廣告標(biāo)示符。如果用戶(hù)完全重置系統(tǒng)((設(shè)置程序 -> 通用 -> 還原 -> 還原位置與隱私) ,這個(gè)廣告標(biāo)示符會(huì)重新生成。另外如果用戶(hù)明確的還原廣告(設(shè)置程序-> 通用 -> 關(guān)于本機(jī) -> 廣告 -> 還原廣告標(biāo)示符) ,那么廣告標(biāo)示符也會(huì)重新生成。關(guān)于廣告標(biāo)示符的還原,有一點(diǎn)需要注意:如果程序在后臺(tái)運(yùn)行,此時(shí)用戶(hù)“還原廣告標(biāo)示符”,然后再回到程序中,此時(shí)獲取廣 告標(biāo)示符并不會(huì)立即獲得還原后的標(biāo)示符。必須要終止程序,然后再重新啟動(dòng)程序,才能獲得還原后的廣告標(biāo)示符。
??但I(xiàn)DFA在上架審核的時(shí)候,是比較嚴(yán)格的。友盟的統(tǒng)計(jì)功能,就使用了IDFA。

??在上架審核的時(shí)候,會(huì)有警告,且審核會(huì)有被拒的可能。

7、Device Token
??apple push token保證設(shè)備唯一,但必須有網(wǎng)絡(luò)情況下才能工作,該方法不依賴(lài)于設(shè)備本身,但依賴(lài)于apple push,而蘋(píng)果push有時(shí)候會(huì)抽風(fēng)的。
解決方案
??綜合以上方案信息,我們會(huì)發(fā)現(xiàn),iOS7及之后的系統(tǒng),是沒(méi)有辦法獲取一個(gè)類(lèi)似UDID這樣的能保證設(shè)備唯一的標(biāo)志符的。
??所以我們只能通過(guò)KeyChain的方式來(lái)保存我們獲取的UUID。KeyChain(鑰匙串)是使用蘋(píng)果設(shè)備經(jīng)常使用的,通常要調(diào)試的話(huà),都得安裝證書(shū)之類(lèi)的,這些證書(shū)就是保存在KeyChain中,還有我們平時(shí)瀏覽網(wǎng)頁(yè)記錄的賬號(hào)密碼也都是記錄在KeyChain中。iOS中的KeyChain相比OS X比較簡(jiǎn)單,整個(gè)系統(tǒng)只有一個(gè)KeyChain,每個(gè)程序都可以往KeyChain中記錄數(shù)據(jù),而且只能讀取到自己程序記錄在KeyChain中的數(shù)據(jù)。鑰匙串的訪(fǎng)問(wèn)需要Security.framework。鑰匙串的API都比較惡心,這里推薦使用SAMKeyChain, https://github.com/soffes/SAMKeychain
??具體實(shí)現(xiàn)代碼就這些:
+ (NSString *)getUniqueStrByUUID {
NSString * currentDeviceUUIDStr = [SAMKeychain passwordForService:@" " account:@"GreatChefUUID"];
if (currentDeviceUUIDStr == nil || [currentDeviceUUIDStr isEqualToString:@""])
{
NSUUID * currentDeviceUUID = [UIDevice currentDevice].identifierForVendor;
currentDeviceUUIDStr = currentDeviceUUID.UUIDString;
currentDeviceUUIDStr = [currentDeviceUUIDStr stringByReplacingOccurrencesOfString:@"-" withString:@""];
currentDeviceUUIDStr = [currentDeviceUUIDStr lowercaseString];
[SAMKeychain setPassword: currentDeviceUUIDStr forService:@" " account:@"GreatChefUUID"];
}
return currentDeviceUUIDStr;
}
??當(dāng)然這也是有弊端的,比如刷機(jī)、恢復(fù)出廠(chǎng)設(shè)置這類(lèi)會(huì)重置手機(jī)信息的,都會(huì)清空鑰匙串信息,我們保存的UUID也就跟著清除了,不過(guò)蘋(píng)果各種限制,我們能做到這種底部,已經(jīng)很不錯(cuò)了?。?!