前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS本地数据存储

iOS本地数据存储

作者头像
落影
发布2019-07-15 16:06:43
2.9K0
发布2019-07-15 16:06:43
举报
文章被收录于专栏:落影的专栏

前言

工作需要,特意准备一篇入门文章,为新人开发者介绍常见的数据存储。

正文

数据存储

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

沙盒目录

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

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

代码语言:javascript
复制
// 获取沙盒根目录路径
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,提供的方法是直接操作数据库。 创建/打开数据库:

代码语言:javascript
复制
NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:@"test_db.sqlite"];
sqlite3 *database;
sqlite3_open([path UTF8String], &database);

建表:

代码语言:javascript
复制
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语句:

代码语言:javascript
复制
// 比较复杂的方法:对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);
}

结束处理

代码语言:javascript
复制
// 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的使用:

代码语言:javascript
复制
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还可以使用?参数,然后在执行的时候填写具体的值:

代码语言:javascript
复制
NSString *insertSqlStr2 = @"insert into test_table_name(test_name_key) values(?)";
[db executeUpdate:insertSqlStr2, @"another_name"];

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

代码语言:javascript
复制
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的选项,选中后会自动生成类的代码,如下:

代码语言:javascript
复制
@interface User (CoreDataProperties)
+ (NSFetchRequest<User *> *)fetchRequest;
@property (nonatomic) int16_t gender;
@property (nullable, nonatomic, copy) NSString *name;
@end

CoreData的具体使用:

代码语言:javascript
复制
//从本地加载对象模型
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;

数据的插入操作:

代码语言:javascript
复制
// 数据插入
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];

数据查询操作:

代码语言:javascript
复制
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;

代码语言:javascript
复制
// 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为例,来看看封装后更简单的接口。

代码语言:javascript
复制
- (void)savePassword:(NSString *)password;
- (BOOL)deleteItem;

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

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

具体的使用样例:

代码语言:javascript
复制
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的方法回调开发者。

代码语言:javascript
复制
@protocol NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER
@end

使用样例:

代码语言:javascript
复制
@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的声明。

代码语言:javascript
复制
@interface SSUser : NSObject <YYModel>

@property (nonatomic, assign) NSInteger gender;
@property (nonatomic, strong) NSString *userName;

@end

3、将字典转换会对象;

代码语言:javascript
复制
NSDictionary *dic = @{
                  @"gender":@0,
                  @"userName": @"test_name",
                    };
SSUser *user = [SSUser modelWithDictionary:dic];

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

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

代码语言:javascript
复制
+ (NSDictionary *)modelCustomPropertyMapper {
    return @{@"userName":@"name"};
}

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

总结

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

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

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

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

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

所有代码GitHub可见,地址

CoreData注意事项

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

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

代码语言:javascript
复制
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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019.07.13 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 正文
    • 数据存储
      • 沙盒目录
        • NSFileManager
          • NSBundle
            • NSUserDefault
              • SQLite3和FMDB
                • FMDB
                  • CoreData
                    • Keychain
                      • 对象序列化
                      • 总结
                        • CoreData注意事项
                          • 附录
                          相关产品与服务
                          文件存储
                          文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档