美文网首页
iOS本地数据存储

iOS本地数据存储

作者: 辉辉岁月 | 来源:发表于2021-09-26 16:02 被阅读0次

    正文

    数据存储

    数据存储本质就是运行时的对象保存在文件、数据库中。数据存储可以分为两步:首先是将对象转换成二进制数据,这一步也叫序列化;相反,将二进制数据转换成对象则称为反序列化;然后是考虑二进制数据如何保存和读取。

    沙盒目录

    iOS系统为每个App分配了独立的数据目录,App只能对自己的目录进行操作,这个目录所在被称为沙盒目录。
    一个应用的沙盒包括下面三个部分:应用目录、沙盒目录、iCloud目录。

    Documents目录用于保存App的数据,包括App运行时需要的各类文件以及用户的数据等。Documents文件夹可以在连接iTunes时选择备份,通常Documents目录用来存放可以对外的文件。
    Library目录用来保存不对外的数据,但同样可以被iTunes备份(Library/Caches目录除外,原因就和目录名一样,里面应该只放Caches)。Library/Caches目录用来放置运行时产生的临时文件以及缓存文件,空间不足时可能会被iOS系统删除。Library/Preferences目录通常用于保存用户的设置等信息,比如我们常用的NSUserDefaults类就会以plist的方式保存在该目录中。
    tmp目录用来保存不重要的临时文件,在系统重启后会被清空,容易知道这个也不会被iTunes备份。

    // 获取沙盒根目录路径
    NSString *homeDir = NSHomeDirectory();
    // 获取Documents目录路径
    NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) firstObject];
    //获取Library的目录路径
    NSString *libDir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES) lastObject];
    // 获取cache目录路径
    NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES) firstObject];
    // 获取tmp目录路径
    NSString *tmpDir =NSTemporaryDirectory();
    
    

    思考题🤔,我们工程中的图片资源是不是放在沙盒目录中呢?
    答案是工程中的资源文件在NSBundle,而NSBundle会被打包到.ipa文件上传到App Store,而用户安装App时候,会把App放置在应用目录(非沙盒目录)。

    NSFileManager

    系统提供了NSFileManager类给开发去读取沙盒目录中的文件。
    NSFileManager是单例,通过defaultManager方法可以获取:
    NSFileManager *fileManager = [NSFileManager defaultManager];
    拿到fileManager就可以判断文件是否存在,并且返回是文件还是文件夹:
    [fileManager fileExistsAtPath:filepath1 isDirectory:&isDirectory];
    遍历文件夹:
    [fileManager contentsOfDirectoryAtPath:filePath error:&error];
    复制或者移动文件:
    [fileManager copyItemAtPath:sourceFilePath toPath:targetFilePath error:nil];
    [fileManager moveItemAtPath:sourceFilePath toPath:targetFilePath error:nil];
    更详细的API可以自行查看NSFileManager.h文件。

    NSBundle

    在用NSFileManager去读取文件的时候需要提供文件路径,但是有时候我们并不知道资源被放置在哪个目录,此时可以用到NSBundle。
    在Xcode编译运行的时候,会把Xcode内的图片、xib、音频等都拷贝到.app文件中。
    NSBundle就是系统提供,用来读取这些资源的类。
    NSBundle * mainBundle = [NSBundle mainBundle];
    这样我们就拿到我们的mainBundle,通过mainBundle我们可以查找对应的资源:
    NSString *path =[mainBundle pathForImageResource:@"some_pic_name"]; // 查找图片地址
    也可以通过mainBundle直接加载xib:
    [[NSBundle mainBundle] loadNibNamed:@"SSProgressView" owner:self options:nil];

    思考题🤔,通过CocoaPods安装的Pod库,要如何读取其资源?
    NSString *path = [[NSBundle mainBundle] pathForResource:@"SSTestPod" ofType:@"bundle"];
    NSBundle *podBundle = [NSBundle bundleWithPath:path];

    NSUserDefault

    iOS系统提供的持久化存储数据的类,该方法是多线程安全的单例,在沙盒中的存储是用plist进行保存。
    如果是NSString、NSNumber、NSData等基础类型可以直接存储在NSUserDefault,如果是自定义对象则需要实现NSCoding进行对象的序列化和反序列化。

    比如说存储一个integer数据:
    [[NSUserDefaults standardUserDefaults] setInteger:1234 forKey:@"key_for_test"];
    读取存储的数据:
    [[NSUserDefaults standardUserDefaults] integerForKey:@"key_for_test"];

    NSUserDefault会由系统自动将数据写入plist中,iOS的老版本也可以调用synchronize方法手动同步,避免写入数据后系统还没将其写入plist而用户退出应用(最新的iOS版本已经不需要)。

    实际开发中,由于NSUserDefault的性能较差并且同步也不及时,多用第三库MMKV来取代NSUserDefault,但是因为某些系统库仍会读取NSUserDefault上的值,NSUserDefault在工程中仍占有一席之地。

    SQLite3和FMDB

    SQLite3是一款轻型的关系型数据库,在移动端中广泛应用。
    SQLite3基于C语言实现,OC可以直接兼容,iOS系统也自带了SQLite3,提供的方法是直接操作数据库。
    创建/打开数据库:

    NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:@"test_db.sqlite"];
    sqlite3 *database;
    sqlite3_open([path UTF8String], &database);
    
    

    建表:

    const char *createSQL = "create table if not exists test_table_name(id integer primary key autoincrement,test_name_key char)";
    char *error;
    sqlite3_exec(database, createSQL, NULL, NULL, &error);
    
    

    执行sql语句:

    // 比较复杂的方法:对SQL语句执行预编译
    int sqlite3_prepare(sqlite3 *db, const char *sql,int byte,sqlite3_stmt **stmt,const char **tail);
    
    // 具体过程
    sqlite3_stmt *stmt;
    const char *insertSQL = "insert into test_table_name(test_name_key) values('anyname')";
    int insertResult = sqlite3_prepare_v2(database, insertSQL, -1, &stmt, nil);
    if (insertResult == SQLITE_OK) {
        sqlite3_step(stmt);
    }
    
    

    结束处理

    // stmt是中间创建的结果,需要销毁
    sqlite3_finalize(stmt);     
    // 关闭数据库,释放文件句柄等资源
    sqlite3_close(database);
    
    

    可以感觉得出来,sqlite3的原生语言是C语言,接口的调用与OC风格不太一样,感觉较为复杂。

    FMDB

    FMDB对SQLite数据库进行封装,开放OC的接口便于开发者接入,是很普遍使用的iOS第三方数据库。
    GitHub仓库地址,也可以使用pod接入。

    三个核心类:
    1、FMDatabase:表示一个SQLite数据库,用于执行sql语句;
    2、FMResultSet:FMDatabase执行查询得到的结果集;
    3、FMDatabaseQueue:多线程用的查询或更新队列;

    FMDB的使用:

    FMDatabase *db = [FMDatabase databaseWithPath:path]; // create db
    [db open]; // open
    // create table
    NSString *createSqlStr = @"create table if not exists test_table_name(id integer primary key autoincrement,test_name_key char)";
    [db executeUpdate:createSqlStr];
    // insert table
    NSString *insertSqlStr = @"insert into test_table_name(test_name_key) values('anyname')";
    [db executeUpdate:insertSqlStr];
    
    

    sql还可以使用?参数,然后在执行的时候填写具体的值:

    NSString *insertSqlStr2 = @"insert into test_table_name(test_name_key) values(?)";
    [db executeUpdate:insertSqlStr2, @"another_name"];
    
    

    查询也很方便,可以结合FMDatabaseQueue来看:

    FMDatabaseQueue *sqlQueue = [FMDatabaseQueue databaseQueueWithPath:path];
    [sqlQueue inDatabase:^(FMDatabase * _Nonnull db) {
        NSString *selectSqlStr = @"select id, test_name_key FROM test_table_name";
        FMResultSet *result = [db executeQuery:selectSqlStr];
        while ([result next]) {
            int value_id = [result intForColumn:@"id"];
            NSString *value_name = [result stringForColumn:@"test_name_key"];
            NSLog(@"id:%d, name:%@", value_id, value_name);
        }
    }];
    
    

    FMDatabaseQueue是使所有操作都在同一个队列进行,避免多线程操作数据库,引起数据异常。

    CoreData

    如果不想使用第三方库,也可以使用iOS系统提供的CoreData框架。
    CoreData的接口更加简化,部分可视化操作,对象代码自动生成等。

    表结构(可视化操作,代码生成):

    根据这个表结构,先选中CoreData的模型文件,在Xcode的Editor有Create NSManagedObject Subclass的选项,选中后会自动生成类的代码,如下:

    @interface User (CoreDataProperties)
    + (NSFetchRequest<User *> *)fetchRequest;
    @property (nonatomic) int16_t gender;
    @property (nullable, nonatomic, copy) NSString *name;
    @end
    
    

    CoreData的具体使用:

    //从本地加载对象模型
    NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"LearnCoreData" ofType:@"momd"];
    NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:[NSURL fileURLWithPath:modelPath]];
    // 创建沙盒中的数据库
    NSPersistentStoreCoordinator* coord = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
    NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:@"database.sqlite"];
    [coord addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:path] options:nil error:nil];
    // 数据库关联缓存
    NSManagedObjectContext* objContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    objContext.persistentStoreCoordinator = coord;
    
    

    数据的插入操作:

    // 数据插入
    User *user = [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:objContext];
    user.name = [NSString stringWithFormat:@"name_%d", arc4random_uniform(100)];
    user.gender = arc4random_uniform(2);
    NSError *error;
    [objContext save:&error];
    
    

    数据查询操作:

    NSFetchRequest *fetch = [[NSFetchRequest alloc] initWithEntityName:@"User"];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"gender=1"]; //查询条件
    fetch.predicate = predicate;
    NSArray *results = [objContext executeFetchRequest:fetch error:nil];
    for (int i = 0; i < results.count; ++i) {
        User *selectedUser = results[i];
        NSLog(@"name…:%@", selectedUser.name);
    }
    
    

    配合前面所学的知识,我们从沙盒可以导出项目中实际使用的数据库。

    用SQLPro for SQLite打开,就可以看到里面的具体信息:(这在分析竞品的时候很有用)

    Keychain

    从上文我们可以知道,保存在沙盒目录的数据也是不安全的,用户可能会导出沙盒数据进行分析。
    有没有什么保存方式是更安全的呢?
    iOS给出的答案是keychain。
    keychain是iOS提供给App存储敏感和安全相关数据用的工具。keychain同样会被iTunes备份,即使App重装仍能读取到上次保存的结果。为了保证数据安全,keychain内的数据都是经过加密。

    keychain的使用
    1、打开keychain的开关。

    2、import <Security/Security.h>
    3、使用API;

    // SELECT
    OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef *result);
    // ADD
    OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef *result);
    // UPDATE
    OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate);
    // DELETE
    OSStatus SecItemDelete(CFDictionaryRef query);
    
    

    这些api非常不友好,幸好苹果官方有提供demo,第三方开发者也有人尝试去封装这些接口,我们以
    KeychainWrapper为例,来看看封装后更简单的接口。

    - (void)savePassword:(NSString *)password;
    - (BOOL)deleteItem;
    
    - (NSString *)readPassword;
    //返回当前accessGroup下的service的所有Keychain Item
    + (NSArray *)passwordItemsForService:(NSString *)service accessGroup:(NSString *)accessGroup;
    
    

    比之前更加贴近OC的语法。

    具体的使用样例:

    KeychainWrapper *wrapper = [[KeychainWrapper alloc] initWithSevice:kKeychainService account:self.account accessGroup:kKeychainAccessGroup];
    NSString *saveStr = [wrapper readPassword];
    if (!saveStr) {
        [wrapper savePassword:@"test_password"];
    }
    NSLog(@"saveStr:%@", saveStr);
    
    

    只要保存在keychain,即使应用卸载重装,仍旧能读取到该值。

    具体的逻辑可见GitHub

    对象序列化

    前面介绍了各种存储的工具,那么如何把运行中的对象序列化成第三方库呢?
    有的开发者会使用系统提供的NSCoding协议手动添加字段,有的开发者会使用Runtime自动实现NSCoding,有的开发者会使用成熟的第三方库(例如YYModel),下面分别介绍这几种序列化的方式。

    NSCoding是系统提供的序列化协议,在对象转换为二进制的时候,会通过NSCoding的方法回调开发者。

    @protocol NSCoding
    - (void)encodeWithCoder:(NSCoder *)aCoder;
    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER
    @end
    
    

    使用样例:

    @interface SSUser : NSObject <NSCoding>
    
    @property (nonatomic, assign) NSInteger gender;
    @property (nonatomic, strong) NSString *userName;
    
    - (instancetype)initWithCoder:(NSCoder *)aDecoder {
        self = [super init];
        self.gender = [[aDecoder decodeObjectForKey:@"gender"] integerValue];
        self.userName = [aDecoder decodeObjectForKey:@"userName"];
        return self;
    }
    
    - (void)encodeWithCoder:(NSCoder *)aCoder {
        [aCoder encodeObject:@(self.gender) forKey:@"gender"];
        [aCoder encodeObject:self.userName forKey:@"userName"];
    }
    @end
    
    

    上面的方式随着属性增多,代码越来越臃肿,于是有的开发者便利用Runtime的特性,读取类的属性名,自动完成这个过程。
    随着iOS的社区发展,有一个序列化的第三方库脱颖而出,那就是YYModel。

    YYModel具有几大特点:
    1、利用iOS的Runtime特点,无需继承;
    2、安全转换数据类型,常见Crash都进行了保护;
    3、扩展性强,提供多种容器扩展;

    YYModel的使用:
    1、安装Pod库,pod 'YYModel'
    2、import<NSObject+YYModel.h>
    在对象添加YYModel的声明。

    @interface SSUser : NSObject <YYModel>
    
    @property (nonatomic, assign) NSInteger gender;
    @property (nonatomic, strong) NSString *userName;
    
    @end
    
    

    3、将字典转换会对象;

    NSDictionary *dic = @{
                      @"gender":@0,
                      @"userName": @"test_name",
                        };
    SSUser *user = [SSUser modelWithDictionary:dic];
    
    

    YYModel还提供丰富的特性,比如说自定义属性名映射、容易类型转换、自定义类的数据映射。

    以自定义属性名映射为例:

    + (NSDictionary *)modelCustomPropertyMapper {
        return @{@"userName":@"name"};
    }
    
    

    YYModel原理和更多进阶使用技巧可以见GitHub

    总结

    iOS的本地数据存储,其实就是内存数据的序列化和反序列化。

    通常我们的数据都会保存在沙盒目录中,读取的时候可以直接指定路径,也可以用NSFileManager去查找和遍历目录;我们工程中的资源文件会存在应用目录,需要用NSBundle去读取。

    APP在运行过程中,有时候需要临时保存一些变量,在下次运行时读取,此时可以用轻量级的持久化工具NSUserDefault,如果数据量比较大则需要考虑使用数据进行存储。SQLite3是iOS中最常用的数据库,通常我们会第三方封装库FMDB来操作,简化代码逻辑。

    如果涉及到安全相关的敏感数据,则不应该保存在文件、数据库等可以被抓取的地方。此时可以使用iOS提供的keychain对敏感数据进行保存。keychain的数据是经过加密处理,具有较高的安全性。

    在将对象转换成二进制数据,以及将二进制数据转换成对象时,可以使用系统提供的NSCoding协议,也可以使用第三方库YYModel。

    所有代码GitHub可见,地址

    CoreData注意事项

    在生成代码的时候,可能会如下的提示:

    看详细的编译错误并没有额外的信息,仍是符号冲突。

    duplicate symbol _OBJC_CLASS_$_CDUser in: 
        /Users/loyinglin/Library/Developer/Xcode/DerivedData/LearnDatabase-dkstmlwuljogjqbnffnrdaqurvyv/Build/Intermediates.noindex/LearnDatabase.build/Debug-iphonesimulator/LearnDatabase.build/Objects-normal/x86_64/CDUser+CoreDataClass.o 
    duplicate symbol _OBJC_METACLASS_$_CDUser in: 
        /Users/loyinglin/Library/Developer/Xcode/DerivedData/LearnDatabase-dkstmlwuljogjqbnffnrdaqurvyv/Build/Intermediates.noindex/LearnDatabase.build/Debug-iphonesimulator/LearnDatabase.build/Objects-normal/x86_64/CDUser+CoreDataClass.o 
    ld: 2 duplicate symbols for architecture x86_64 
    clang: error: linker command failed with exit code 1 (use -v to see invocation) 
    
    

    但是在工程中,仅仅只有一个CDUser+CoreDataProperties.m,并没有其他CDUser的类。
    尝试把CDUser+CoreDataProperties.m从compile source中移除,工程中仍保留CDUser+CoreDataProperties.h文件,结果编译可以通过。
    检查工程的build settings也没有有用的信息,最后打开DerivedData中找到对应的目录,结果找到下面的CoreDataGenerated文件夹:

    从名字上可以得知,这也是CoreData自动生成!
    经过一番搜索,终于找到CoreData对应的设置。

    附录

    苹果官方文档-File System Programming Guide

    相关文章

      网友评论

          本文标题:iOS本地数据存储

          本文链接:https://www.haomeiwen.com/subject/wubqnltx.html