现在这个世纪,手机就是我的半条命,还有可能是多半条。
每人一部手机是标配,有的人可能还有好几个。
那么,这么多设备,我们在互联网的世界,如何标明唯一的一个设备呢?
这个时候就引入了“设备唯一标识符”的概念。
在iOS的生态中,有我们所熟知的IDFA,IDFV,UDID,UUID。
简直能把人整晕。但是他们含义不同,应用场景不同。
为了能在我们的业务中更好的标识一个唯一设备,我们引入OpenUDID类库。
而在介绍这个库之前,我们先来看看,上面说的这几个编码到底有哪里不同,以及如何应用。
一、IDFA
IDFA是一串16进制的32位串。全称是Identifier For Advertising
设计目的是,标识用户设备,用于提供给各个APP之间跟踪广告所用。
例如,你在淘宝里搜索了某个商品之后,你在用浏览器去浏览网页的时候,那个网页的广告就会给你展示相应的那个商品的广告。
IDFA具有以下特性:
- IDFA和设备绑定。所有应用在统一设备上获取的都是一样的
- iOS10之后,用户可以在设置中关闭限制APP获取
- 可以被更改,更改的时机为:
- 设置程序 -> 通用 -> 还原 -> 还原位置与隐私
- 设置程序-> 通用 -> 关于本机 -> 广告 -> 还原广告标示符
因此,IDFA可以作为一台设备的唯一标识符。但是会有风险被重试,或者被限制获取。
二、IDFV
和IDFA非常类似,是一串16进制的32位串。全称是Identifier For Vendor。
但是不同的是,不仅和设备相关,也和APP提供商相关。
因此,具有以下特性:
-
IDFV同时和设备,以及APP的提供商相关。一台设备上,同一个APP提供商的IDFV是一样的。
因此,如果将此台设备上,所有这个APP提供商的APP都删除了,则下一次重新安装此提供商APP时,获取的IDFV会变化。
例如,Dailyyoga和每日瑜伽获取的IDFV相同,但是把他们两个都删除了,下次重新安装后,IDFV会变化。
所以,IDFV,一般可以用于同一个厂家不同APP之间的在本地的数据互通。
三、UDID
是 iOS 设备的一个唯一识别码,每台 iOS 设备都有一个独一无二的编码,全称是Unique Device Identifier。由40位16进制数组成的字符串。
但是在iOS5以后,已经被苹果禁止通过代码获取了。
目前还有三种方式可以看到UDID:
- 使用iTunes,将手机连接电脑
-
使用XCode->Device,将手机连接电脑
-
使用企业级的MDM,制作描述文件,然后搭配网页展示获取UDID。但是只能在Safari中实现。
目前蒲公英就是这么做的。可以参考蒲公英的获取UDID的地址:www.pgyer.com/udid
具体实现方案,可以参考这里:www.skyfox.org/safari-ios-…
因此,苹果为了防止厂家滥用,对于客户端来说,目前已经失去了获取UDID的能力。
四、UUID
UUID 是 通用唯一识别码(Universally Unique Identifier)的缩写,是一种软件建构的标准,是国际标准化组织(ISO)提出了一个概念。
它是一个32位的十六进制序列。
其设计目的是,让分布式系统中的所有元素,都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。
目前几乎所有系统,windows,Linux,macOS,都已经接入此标准。
UUID是基于当前时间、计数器(counter)和硬件标识(通常为无线网卡的MAC地址)等数据计算生成的。因此,每次重新获取的时候,都不一样,可以在此时此地标识唯一的一台设备。
五、OpenUDID —— 我们使用的识别标识库
UUID目前是比较通用的,在OC中可以通过NSUUID来获取。根据其获取方式,可以知道每次获取都会不同,实际验证也如此。OpenUDID实际也是基于UUID来实现的。
但是我们想要是,一旦我的APP在这台机器上安装上了,无论以后怎么折腾,都获取的是统一个。那么,我们应该如何做呢?
此时,OpenUUID给了我们一个解决策略。(虽然这个库已经很多年没更新过了,API基于系统底层,所以相对稳定。并且我们的APP使用了很长时间,也没有办法替换)
由于DeviceID对于我们的重要性,我们还是有必要仔细阅读一下此库的源码,来弄清楚我们DeviceID的前世今生。接下来,我们一起来看看。
我们首先用流程图来表示一下,OpenUDID的核心功能,以及其数据流向:
-
OpenUDID中所获取的UDID是使用32位UUID进行了40位的补全而来,它的本质就是一个UUID,是可变的
-
为了使其不可变,OpenUDID将此值分别存储在了三个地方
- 内存变量 — — 在不退出APP时,可以从内存中直接获取之前的ID
- NSUserDefault — — 在不删除APP时,每次打开APP,可以在NSUserDefault中获取到之前生成的ID
- UIPasteboard — — 删除了APP后,在同一设备上重新安装APP,依然可以获取到之前已经生成的ID
-
不可变是我们要的常态,那么我们需要着重关心的是,这个ID什么时候会变化
由于最终是存在UIPasteboard的不同域中,而iOS不允许用户主动清理剪贴板,所以变化的时机有以下几个:
- 用户抹掉手机中所有数据,出厂还原为一个新手机
- 有其他APP,使用API强制清除所有剪贴板(待确认可实施性,但是不排除有这个可能)
- 由于OpenUDID是开源,不排除极端情况,有其他恶意APP清除OpenUDID使用的剪贴板域
综上,OpenUDID由于聪明地使用了剪贴板做数据持久化以及共享。所以可以保证只要设备上有APP使用了OpenUDID,就可以生成一个唯一识别码,除非以上情况,此识别码将可以一直持续使用,因此,可靠性很高。
对于我们的业务来说,部分功能使用了DeviceID(即我们通过OpenUDID获取给服务器的ID)进行设备识别的模块,例如,即使用户删除APP,再次安装时,也要在登录页面弹出上次他在这台设备上最后一次登录的账号的功能,可以大胆放心的使用此ID作为识别码。
最后,放上OpenUDID源码。有表述不清或者不对的地方,还请提出质疑。
Let’s think!
//
// OpenUDID.m
// openudid
//
// initiated by Yann Lechelle (cofounder @Appsfire) on 8/28/11.
// Copyright 2011, 2012 OpenUDID.org
//
// Initiators/root branches
// iOS code: https://github.com/ylechelle/OpenUDID
// Android code: https://github.com/vieux/OpenUDID
//
// Contributors:
// https://github.com/ylechelle/OpenUDID/contributors
//
/*
http://en.wikipedia.org/wiki/Zlib_License
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
*/
#if __has_feature(objc_arc)
#error This file uses the classic non-ARC retain/release model; hints below...
// to selectively compile this file as non-ARC, do as follows:
// https://img.skitch.com/20120717-g3ag5h9a6ehkgpmpjiuen3qpwp.png
#endif
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
#import "OpenUDID.h"
#import <CommonCrypto/CommonDigest.h> // Need to import for CC_MD5 access
#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
#import <UIKit/UIPasteboard.h>
#import <UIKit/UIKit.h>
#else
#import <AppKit/NSPasteboard.h>
#endif
#define OpenUDIDLog(fmt, ...)
//#define OpenUDIDLog(fmt, ...) //NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__);
//#define OpenUDIDLog(fmt, ...) //NSLog((@"[Line %d] " fmt), __LINE__, ##__VA_ARGS__);
static NSString * kOpenUDIDSessionCache = nil;
//static NSString * const kOpenUDIDDescription = @"OpenUDID_with_iOS6_Support";
static NSString * const kOpenUDIDKey = @"OpenUDID";
static NSString * const kOpenUDIDSlotKey = @"OpenUDID_slot";
static NSString * const kOpenUDIDAppUIDKey = @"OpenUDID_appUID";
static NSString * const kOpenUDIDTSKey = @"OpenUDID_createdTS";
static NSString * const kOpenUDIDOOTSKey = @"OpenUDID_optOutTS";
static NSString * const kOpenUDIDDomain = @"org.OpenUDID";
static NSString * const kOpenUDIDSlotPBPrefix = @"org.OpenUDID.slot.";
static int const kOpenUDIDRedundancySlots = 100;
@interface OpenUDID (Private)
+ (void) _setDict:(id)dict forPasteboard:(id)pboard;
+ (NSMutableDictionary*) _getDictFromPasteboard:(id)pboard;
+ (NSString*) _generateFreshOpenUDID;
@end
@implementation OpenUDID
// Archive a NSDictionary inside a pasteboard of a given type
// Convenience method to support iOS & Mac OS X
//
+ (void) _setDict:(id)dict forPasteboard:(id)pboard {
#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
[pboard setData:[NSKeyedArchiver archivedDataWithRootObject:dict] forPasteboardType:kOpenUDIDDomain];
#else
[pboard setData:[NSKeyedArchiver archivedDataWithRootObject:dict] forType:kOpenUDIDDomain];
#endif
}
// Retrieve an NSDictionary from a pasteboard of a given type
// Convenience method to support iOS & Mac OS X
//
+ (NSMutableDictionary*) _getDictFromPasteboard:(id)pboard {
#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
id item = nil;
if([pboard respondsToSelector:@selector(dataForPasteboardType:)]){
item = [pboard dataForPasteboardType:kOpenUDIDDomain];
}
#else
id item = [pboard dataForType:kOpenUDIDDomain];
#endif
if (item) {
@try{
item = [NSKeyedUnarchiver unarchiveObjectWithData:item];
} @catch(NSException* e) {
OpenUDIDLog(@"Unable to unarchive item %@ on pasteboard!", [pboard name]);
item = nil;
}
}
// return an instance of a MutableDictionary
return [NSMutableDictionary dictionaryWithDictionary:(item == nil || [item isKindOfClass:[NSDictionary class]]) ? item : nil];
}
// Private method to create and return a new OpenUDID
// Theoretically, this function is called once ever per application when calling [OpenUDID value] for the first time.
// After that, the caching/pasteboard/redundancy mechanism inside [OpenUDID value] returns a persistent and cross application OpenUDID
//
+ (NSString*) _generateFreshOpenUDID {
NSString* _openUDID = nil;
// August 2011: One day, this may no longer be allowed in iOS. When that is, just comment this line out.
// March 25th 2012: this day has come, let's remove this "outlawed" call...
// August 2012: well, perhaps much ado about nothing; in any case WWDC2012 gave us something to work with; read below
#if TARGET_OS_IPHONE
// if([UIDevice instancesRespondToSelector:@selector(uniqueIdentifier)]){
// _openUDID = [[UIDevice currentDevice] uniqueIdentifier];
// }
#endif
//
// !!!!! IMPORTANT README !!!!!
//
// August 2012: iOS 6 introduces new APIs that help us deal with the now deprecated [UIDevice uniqueIdentifier]
// Since iOS 6 is still pre-release and under NDA, the following piece of code is meant to produce an error at
// compile time. Accredited developers integrating OpenUDID are expected to review the iOS 6 release notes and
// documentation, and replace the underscore ______ in the last part of the selector below with the right
// selector syntax as described here (make sure to use the right one! last word starts with the letter "A"):
// https://developer.apple.com/library/prerelease/ios/#documentation/UIKit/Reference/UIDevice_Class/Reference/UIDevice.html
//
// The semantic compiler warnings are still normal if you are compiling for iOS 5 only since Xcode will not
// know about the two instance methods used on that line; the code running on iOS will work well at run-time.
// Either way, it's time that you junped on the iOS 6 bandwagon and start testing your code on iOS 6 ;-)
//
// WHAT IS THE SIGNIFICANCE OF THIS CHANGE?
//
// Essentially, this means that OpenUDID will keep on behaving the same way as before for existing users or
// new users in iOS 5 and before. For new users on iOS 6 and after, the new official public APIs will take over.
// OpenUDID will therefore be obsoleted when iOS reaches significant adoption, anticipated around mid-2013.
/*
September 14; ok, new development. The alleged API has moved!
This part of the code will therefore be updated when iOS 6 is actually released.
Nevertheless, if you want to go ahead, the code should be pretty easy to
guess... it involves including a .h file from a nine-letter framework that ends
with the word "Support", and then assigning _openUDID with the only identifier method called on the sharedManager of that new class... don't forget to add
the framework to your project!
#if TARGET_OS_IPHONE
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"6.0")) {
_openUDID = [[[UIDevice currentDevice] identifierForA_______] UUIDString];
# error ^ read comments above, fix accordingly, and remove this #error line
}
#endif
*/
// Next we generate a UUID.
// UUIDs (Universally Unique Identifiers), also known as GUIDs (Globally Unique Identifiers) or IIDs
// (Interface Identifiers), are 128-bit values guaranteed to be unique. A UUID is made unique over
// both space and time by combining a value unique to the computer on which it was generated—usually the
// Ethernet hardware address—and a value representing the number of 100-nanosecond intervals since
// October 15, 1582 at 00:00:00.
// We then hash this UUID with md5 to get 32 bytes, and then add 4 extra random bytes
// Collision is possible of course, but unlikely and suitable for most industry needs (e.g. aggregate tracking)
//
if (_openUDID==nil) {
CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
CFStringRef cfstring = CFUUIDCreateString(kCFAllocatorDefault, uuid);
const char *cStr = CFStringGetCStringPtr(cfstring,CFStringGetFastestEncoding(cfstring));
unsigned char result[16];
CC_MD5( cStr, (int)strlen(cStr), result );
CFRelease(uuid);
CFRelease(cfstring);
_openUDID = [NSString stringWithFormat:
@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%08lx",
result[0], result[1], result[2], result[3],
result[4], result[5], result[6], result[7],
result[8], result[9], result[10], result[11],
result[12], result[13], result[14], result[15],
(unsigned long)(arc4random() % NSUIntegerMax)];
}
// Call to other developers in the Open Source community:
//
// feel free to suggest better or alternative "UDID" generation code above.
// NOTE that the goal is NOT to find a better hash method, but rather, find a decentralized (i.e. not web-based)
// 160 bits / 20 bytes random string generator with the fewest possible collisions.
//
return _openUDID;
}
// Main public method that returns the OpenUDID
// This method will generate and store the OpenUDID if it doesn't exist, typically the first time it is called
// It will return the null udid (forty zeros) if the user has somehow opted this app out (this is subject to 3rd party implementation)
// Otherwise, it will register the current app and return the OpenUDID
//
+ (NSString*) value {
static NSString*ygOpenUDID = nil;
if(!ygOpenUDID){
@try {
ygOpenUDID = [OpenUDID valueWithError:nil]?:@"";
}@catch (NSException *exception) {
return @"";
}
}
return ygOpenUDID?:@"";
}
+ (NSString*) valueWithError:(NSError **)error {
if (kOpenUDIDSessionCache!=nil) {
if (error!=nil)
*error = [NSError errorWithDomain:kOpenUDIDDomain
code:kOpenUDIDErrorNone
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"OpenUDID in cache from first call",@"description", nil]];
return kOpenUDIDSessionCache;
}
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// The AppUID will uniquely identify this app within the pastebins
//
NSString * appUID = [defaults objectForKey:kOpenUDIDAppUIDKey];
if(appUID == nil)
{
// generate a new uuid and store it in user defaults
CFUUIDRef uuid = CFUUIDCreate(NULL);
appUID = (NSString *) CFUUIDCreateString(NULL, uuid);
CFRelease(uuid);
[appUID autorelease];
}
NSString* openUDID = nil;
NSString* myRedundancySlotPBid = nil;
NSDate* optedOutDate = nil;
BOOL optedOut = NO;
BOOL saveLocalDictToDefaults = NO;
BOOL isCompromised = NO;
// Do we have a local copy of the OpenUDID dictionary?
// This local copy contains a copy of the openUDID, myRedundancySlotPBid (and unused in this block, the local bundleid, and the timestamp)
//
id localDict = [defaults objectForKey:kOpenUDIDKey];
if ([localDict isKindOfClass:[NSDictionary class]]) {
localDict = [NSMutableDictionary dictionaryWithDictionary:localDict]; // we might need to set/overwrite the redundancy slot
openUDID = [localDict objectForKey:kOpenUDIDKey];
myRedundancySlotPBid = [localDict objectForKey:kOpenUDIDSlotKey];
optedOutDate = [localDict objectForKey:kOpenUDIDOOTSKey];
optedOut = optedOutDate!=nil;
OpenUDIDLog(@"localDict = %@",localDict);
}
// Here we go through a sequence of slots, each of which being a UIPasteboard created by each participating app
// The idea behind this is to both multiple and redundant representations of OpenUDIDs, as well as serve as placeholder for potential opt-out
//
NSString* availableSlotPBid = nil;
NSMutableDictionary* frequencyDict = [NSMutableDictionary dictionaryWithCapacity:kOpenUDIDRedundancySlots];
for (int n=0; n<kOpenUDIDRedundancySlots; n++) {
NSString* slotPBid = [NSString stringWithFormat:@"%@%d",kOpenUDIDSlotPBPrefix,n];
#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
UIPasteboard* slotPB = [UIPasteboard pasteboardWithName:slotPBid create:NO];
#else
NSPasteboard* slotPB = [NSPasteboard pasteboardWithName:slotPBid];
#endif
OpenUDIDLog(@"SlotPB name = %@",slotPBid);
if (slotPB==nil) {
// assign availableSlotPBid to be the first one available
if (availableSlotPBid==nil) availableSlotPBid = slotPBid;
} else {
NSDictionary* dict = [OpenUDID _getDictFromPasteboard:slotPB];
NSString* oudid = [dict objectForKey:kOpenUDIDKey];
OpenUDIDLog(@"SlotPB dict = %@",dict);
if (oudid==nil) {
// availableSlotPBid could inside a non null slot where no oudid can be found
if (availableSlotPBid==nil) availableSlotPBid = slotPBid;
} else {
// increment the frequency of this oudid key
int count = [[frequencyDict valueForKey:oudid] intValue];
[frequencyDict setObject:[NSNumber numberWithInt:++count] forKey:oudid];
}
// if we have a match with the app unique id,
// then let's look if the external UIPasteboard representation marks this app as OptedOut
NSString* gid = [dict objectForKey:kOpenUDIDAppUIDKey];
if (gid!=nil && [gid isEqualToString:appUID]) {
myRedundancySlotPBid = slotPBid;
// the local dictionary is prime on the opt-out subject, so ignore if already opted-out locally
if (optedOut) {
optedOutDate = [dict objectForKey:kOpenUDIDOOTSKey];
optedOut = optedOutDate!=nil;
}
}
}
}
// sort the Frequency dict with highest occurence count of the same OpenUDID (redundancy, failsafe)
// highest is last in the list
//
NSArray* arrayOfUDIDs = [frequencyDict keysSortedByValueUsingSelector:@selector(compare:)];
NSString* mostReliableOpenUDID = (arrayOfUDIDs!=nil && [arrayOfUDIDs count]>0)? [arrayOfUDIDs lastObject] : nil;
OpenUDIDLog(@"Freq Dict = %@\nMost reliable %@",frequencyDict,mostReliableOpenUDID);
// if openUDID was not retrieved from the local preferences, then let's try to get it from the frequency dictionary above
//
if (openUDID==nil) {
if (mostReliableOpenUDID==nil) {
// this is the case where this app instance is likely to be the first one to use OpenUDID on this device
// we create the OpenUDID, legacy or semi-random (i.e. most certainly unique)
//
openUDID = [OpenUDID _generateFreshOpenUDID];
} else {
// or we leverage the OpenUDID shared by other apps that have already gone through the process
//
openUDID = mostReliableOpenUDID;
}
// then we create a local representation
//
if (localDict==nil) {
localDict = [NSMutableDictionary dictionaryWithCapacity:4];
[localDict setObject:openUDID forKey:kOpenUDIDKey];
[localDict setObject:appUID forKey:kOpenUDIDAppUIDKey];
[localDict setObject:[NSDate date] forKey:kOpenUDIDTSKey];
if (optedOut) [localDict setObject:optedOutDate forKey:kOpenUDIDTSKey];
saveLocalDictToDefaults = YES;
}
}
else {
// Sanity/tampering check
//
if (mostReliableOpenUDID!=nil && ![mostReliableOpenUDID isEqualToString:openUDID])
isCompromised = YES;
}
// Here we store in the available PB slot, if applicable
//
OpenUDIDLog(@"Available Slot %@ Existing Slot %@",availableSlotPBid,myRedundancySlotPBid);
if (availableSlotPBid!=nil && (myRedundancySlotPBid==nil || [availableSlotPBid isEqualToString:myRedundancySlotPBid])) {
#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
UIPasteboard* slotPB = [UIPasteboard pasteboardWithName:availableSlotPBid create:YES];
[slotPB setPersistent:YES];
#else
NSPasteboard* slotPB = [NSPasteboard pasteboardWithName:availableSlotPBid];
#endif
// save slotPBid to the defaults, and remember to save later
//
if (localDict) {
[localDict setObject:availableSlotPBid forKey:kOpenUDIDSlotKey];
saveLocalDictToDefaults = YES;
}
// Save the local dictionary to the corresponding UIPasteboard slot
//
if (openUDID && localDict)
[OpenUDID _setDict:localDict forPasteboard:slotPB];
}
// Save the dictionary locally if applicable
//
if (localDict && saveLocalDictToDefaults)
[defaults setObject:localDict forKey:kOpenUDIDKey];
// If the UIPasteboard external representation marks this app as opted-out, then to respect privacy, we return the ZERO OpenUDID, a sequence of 40 zeros...
// This is a *new* case that developers have to deal with. Unlikely, statistically low, but still.
// To circumvent this and maintain good tracking (conversion ratios, etc.), developers are invited to calculate how many of their users have opted-out from the full set of users.
// This ratio will let them extrapolate convertion ratios more accurately.
//
if (optedOut) {
if (error!=nil) *error = [NSError errorWithDomain:kOpenUDIDDomain
code:kOpenUDIDErrorOptedOut
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"Application with unique id %@ is opted-out from OpenUDID as of %@",appUID,optedOutDate],@"description", nil]];
kOpenUDIDSessionCache = [[NSString stringWithFormat:@"%040x",0] retain];
return kOpenUDIDSessionCache;
}
// return the well earned openUDID!
//
if (error!=nil) {
if (isCompromised)
*error = [NSError errorWithDomain:kOpenUDIDDomain
code:kOpenUDIDErrorCompromised
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Found a discrepancy between stored OpenUDID (reliable) and redundant copies; one of the apps on the device is most likely corrupting the OpenUDID protocol",@"description", nil]];
else
*error = [NSError errorWithDomain:kOpenUDIDDomain
code:kOpenUDIDErrorNone
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"OpenUDID succesfully retrieved",@"description", nil]];
}
kOpenUDIDSessionCache = [openUDID retain];
return kOpenUDIDSessionCache;
}
+ (void) setOptOut:(BOOL)optOutValue {
// init call
[OpenUDID value];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// load the dictionary from local cache or create one
id dict = [defaults objectForKey:kOpenUDIDKey];
if ([dict isKindOfClass:[NSDictionary class]]) {
dict = [NSMutableDictionary dictionaryWithDictionary:dict];
} else {
dict = [NSMutableDictionary dictionaryWithCapacity:2];
}
// set the opt-out date or remove key, according to parameter
if (optOutValue)
[dict setObject:[NSDate date] forKey:kOpenUDIDOOTSKey];
else
[dict removeObjectForKey:kOpenUDIDOOTSKey];
// store the dictionary locally
[defaults setObject:dict forKey:kOpenUDIDKey];
OpenUDIDLog(@"Local dict after opt-out = %@",dict);
// reset memory cache
kOpenUDIDSessionCache = nil;
}
@end