轻量本地数据存储

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就可以共享这些数据。

keyChain

那么对于存储的对象来说,其实实际上就是数据库中的一条记录。

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

另外请注意,开启上图Keychain Sharing后,你的项目中会自动出现如下文件:


entitlements文件

其中的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指的是下图所示的值:


AppIdentifierPrefix

另外需要注意,你是用的kSecAttrAccessGroup的值和你添加的keychain-access-groups中的某一个item必须完全匹配,使用类似
*的通配符是不行的,例如使用AppIdentifierPrefix.com.test.sharing就是可以成功的,但是使用AppIdentifierPrefix.com.test.*,还会出现-34018错误errSecMissingEntitlement。

参考链接:

How to share Keychain between iOS apps

你可能感兴趣的:(轻量本地数据存储)