NSUserDefaults
An interface to the user’s defaults database, where you store key-value pairs persistently across launches of your app.
一个用户默认数据库的对外接口,利用这个接口,在你app运行期间持续存储键值对。
在日常项目开发中,针对轻量本地数据存储我们一般会采用的方式是NSUserDefaults,NSUserDefaults的沙盒路径为Library—>Preferences文件夹中.由上述定义可知,NSUserDefaults保存的键值对相当于全局的变量,那么既然是全局的,就会产生一个问题:对于组件化结构的应用,各个模块之间可能互不知晓对方所使用的key,从而造成各个模块之间修改了同一个key值对应的value,造成数据混乱。另外还有一点就是NSUserDefaults存储效率较低。
针对上述问题,mmkv给出了我们很好地实践方式,mmkv提供了按照mmapID区分的私有存储空间,多个模块可以使用不同标志的mmapID来进行相应区分,以保证各个模块即使修改了相同key值value也不会相互影响。对于需要在每个模块中保持一致的key-value可以使用默认提供的defaultMMKV或者使用相同mmapID划分。具体解析请看这篇博客,此处不再赘述。
Keychain
对于app卸载后仍需保存的数据和共享数据的存储,我们可以使用系统提供的又一大利器——Keychain。
1.简介
Keychain是苹果提供的密码管理系统,存储多种数据例如:密码,私钥等数据,其本质实际上是保存在沙盒之外的数据库-一个小型的Sqlite,文件地址:/private/var/Keychains/keychain-2.db。
【1】由于独立于app的沙盒,所以在删除App重新下载后,这些信息依旧存在。
【2】你可以通过设置一些属性,让你的数据保存到iCloud中,达到跨设备存储的功能。
【3】虽然Keychain不同应用的数据之间默认是隔离的,但是如果可以在keychain存储的基础上,给项目的 Entitlements.plist里加上keychain-access-groups,那么同一个开发者账号下的app就可以共享这些数据。
那么对于存储的对象来说,其实实际上就是数据库中的一条记录。
2.记录分析
每条记录本质上都有其配置信息:
2.1 利用kSecClass属性指定要保存的表
// Specifies Internet password items 记录网络密码数据,对应genp表
extern const CFStringRef kSecClassInternetPassword
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0);
// Specifies generic password items 记录一般的密码数据
extern const CFStringRef kSecClassGenericPassword
__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_2_0);
// Specifies certificate items 记录证书数据
extern const CFStringRef kSecClassCertificate
__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_2_0);
// Specifies key items 记录key值数据
extern const CFStringRef kSecClassKey
__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_2_0);
// Specifies identity items 记录认证信息
extern const CFStringRef kSecClassIdentity
__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_2_0);
2.2 利用kSecAttrXXXX指定表中对应的字段,即指定记录的属性
属性名称 | 属性设置值 | 设置意义 |
---|---|---|
kSecAttrAccessible | kSecAttrAccessibleWhenUnlocked(ThisDeviceOnly) | 仅当设备解锁时可获取到数据(数据仅在当前设备绑定) |
kSecAttrAccessibleAfterFirstUnlock(ThisDeviceOnly) | 在设备第一次解锁后均可获取数据(数据仅在当前设备绑定) | |
kSecAttrAccessibleAlways(ThisDeviceOnly) | 始终可以获取数据(数据仅在当前设备绑定) | |
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly | app在前台并且设备有密码已解锁才可获取数据,并且数据仅在当前设备绑定 | |
kSecAttrAccessGroup | 自定义字段 | 区分你的存储区域,若未指定则默认使用bundleID,则多个app间是不同的 |
kSecAttrAccount | 自定义字段 | 标识此条记录的主Key之一(kSecClassGenericPassword, kSecClassInternetPassword这两个kSecClass指向的表含有这个属性) |
kSecAttrService | 自定义字段 | 标识此条记录的主Key之一(kSecClassGenericPassword这个kSecClass指向的表含有这个属性) |
kSecAttrSynchronizable | BOOL | 标识是否同步到iCloud,需要注意的是如果使用了这个属性来进行跨设备的备份,那么搜索匹配的时候要带上这个字段才能查询到相应的Item |
2.3 kSecValueData是要存储/获取的对象
kSecValueData Specifies a dictionary key whose value is of type CFDataRef. For keys and password items, data is secret (encrypted) and may require the user to enter a password for access.
kSecValueData的值只能是CFDataRef类型.对于密钥和密码项,数据是保密的(加密的)可能需要用户输入密码才能访问
2.4 kSecMatchLimit查询时设置返回匹配的数目
kSecMatchLimit Specifies a dictionary key whose value is a CFNumberRef. If provided, this value specifies the maximum number of results to return. If not provided, results are limited to the first item found.Predefined values are provided for a single item (kSecMatchLimitOne) and all matching items (kSecMatchLimitAll)
kSecMatchLimit是一个CFNumberRef数据类型的值,用来指定查询数目,如果提供了则按照该数目返回结果,如果没有提供则默认返回第一个匹配的记录数据。也可以通过设置为kSecMatchLimitOne来指定仅查找第一个,或者设置为kSecMatchLimitAll来指定全部查找。
3.常用操作
由于本质是数据库,所以常用操作逃不开增删改查,keychain提供了以下方法
增SecItemAdd
删SecItemDelete
改SecItemUpdate
查SecItemCopyMatching
只不过针对各个项的配置参数不同,相关参数我们在上述已经做了简单介绍,下面给大家看一个实际的:
- (NSMutableDictionary *)baseConfigDict {
// 设置kSecClass的表和kSecAttrService的查询主key
NSMutableDictionary *configDict = [@{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,//
(__bridge id)kSecAttrService: self.service,
} mutableCopy];
// 如果共享accessGroup存在,设置kSecAttrAccessGroup
if (self.accessGroup) {
attributes[(__bridge id)kSecAttrAccessGroup] = self.accessGroup;
}
return attributes;
}
- (NSMutableDictionary *)queryDictByKey:(NSString *)key{
NSAssert(key != nil, @"key 为空值");
NSMutableDictionary *query = [self baseConfigDict];
// 设置查询主键kSecAttrAccount
query[(__bridge id)kSecAttrAccount] = key;
// 设置查询限制数目
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
return query;
}
// 增加
- (BOOL)saveKeychainData:(NSData *)data forKey:(NSString *)key {
if (!key) {
return NO;
}
// 获取基础配置
NSMutableDictionary *configDict = [self baseConfigDict];
// 设置要存储的key和value,设置数据获取情景
configDict[(__bridge id)kSecAttrAccount] = key;
configDict[(__bridge id)kSecValueData] = value;
configDict[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlock;
// 调用SecItemAdd增加新的记录
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)configDict, NULL);
if (status == errSecSuccess) {
return YES;
}
return NO;
}
// 删除
- (void)deleteKeychainDataForKey:(NSString *)key {
NSMutableDictionary *configDict = [self baseConfigDict];
// 设置要删除的key值
configDict[(__bridge id)kSecAttrAccount] = key;
// 调用SecItemDelete删除对应记录
SecItemDelete((CFDictionaryRef)configDict);
}
// 修改
- (BOOL)updateKeychainData:(NSData *)data forKey:(NSString *)key {
NSMutableDictionary *queryDict = [self queryDictByKey:key];
NSDictionary *dataDict = @{(__bridge id)kSecValueData : data};
OSStatus status = SecItemUpdate((CFDictionaryRef)queryDict,
(CFDictionaryRef)dataDict);
if (status == errSecSuccess) {
return YES;
}
return NO;
}
// 查询
- (NSData *)fetchKeychainDataForKey:(NSString *)key {
if (!key) {
return nil;
}
NSDictionary *queryDict = [self queryDictByKey:key];
CFTypeRef data = nil;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)queryDict, &data);
if (status != errSecSuccess) {
return nil;
}
NSData *dataFound = [NSData dataWithData:(__bridge NSData *)data];
if (data) {
CFRelease(data);
}
return dataFound;
}
tip keychain-access-groups
因为keychain-access-groups这个参数比较特殊,所以我们还是多介绍一下,如果利用代码设置了kSecAttrAccessGroup来指定访问的是哪个accessgroup,那么也就意味着你可能有在多个应用之间共享数据的需求,但是在实际使用中你可能会遇到以下坑:
tip1 -34018错误errSecMissingEntitlement(笔者是个弱鸡,被困了好久。。。大佬可忽略)
可能原因:
【1】你没有在项目中开启Keychain Sharing。如果你没有开启此项,那么其实默认Keychain是按照你的bundleID设置该值的,那么因为每个应用的bundleID都是不同的,所以也就意味着你存进Keychain的数据并不能与其他应用共享。所以如果想进行数据共享,下图所示的配置项需要开启
另外请注意,开启上图Keychain Sharing后,你的项目中会自动出现如下文件:
其中的item即为自动生成的keychain group
注:如果你开启了Keychain Sharing,并添加了多个Keychain Groups,那么你进行存储时如果不指定需要使用哪个Keychain Groups进行存储,默认是取你添加的Keychain Groups的第一个值当做默认的值。
【2】在使用代码进行配置时需要注意kSecAttrAccessGroup的设置必须使用你添加了的item选项,例如你添加了一个com.test.sharing的keychain group,那么kSecAttrAccessGroup值须被设为$(AppIdentifierPrefix)com.test.sharing才生效,其中AppIdentifierPrefix指的是下图所示的值:
另外需要注意,你是用的kSecAttrAccessGroup的值和你添加的keychain-access-groups中的某一个item必须完全匹配,使用类似
*的通配符是不行的,例如使用AppIdentifierPrefix.com.test.sharing就是可以成功的,但是使用AppIdentifierPrefix.com.test.*,还会出现-34018错误errSecMissingEntitlement。
参考链接:
How to share Keychain between iOS apps