数据存储
要将对象存储在文件、沙盒等,首先第一步是将其转换成二进制数据,这一步也叫序列化。相反,将二进制数据转换成对象,则称为反序列化。其次是考虑二进制数据的保存和读取。
沙盒目录
iOS系统为每个App分配了独立的目录,App只能对自己对目录进行操作,这个目录所在被称为沙盒。
-
Sandbox
-
Bundle Container
- myApp.app
-
Data Container
-
Documents
Documents目录用于保存App的数据,包括App运行时需要的各类文件以及用户的数据等。Documents文件夹虎仔连接iTunes时选择备份。通常Documents用于存放可以对外的文件。
-
Library
Library目录用来保存不对外的数据,但同样会收到iTunes但备份(Library/Caches目录除外,原因就是里面只放Caches)。Library/Caches目录通常用于放置运行时产生但临时文件以及缓存文件,空间不足时可能会被系统清除。而Library/Preferences目录通常用于保存用户但设置等信息,比如我们常用但NSUserDefaults类就会以plist但方式保存在目录中。
-
Tmp
tmp目录用来保存不重要的临时文件,在系统重启后会被晴空,同样也不会被iTunes备份。
-
-
iCloud Container
……
-
访问沙盒目录
//获取沙盒根目录路径
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();
-
NSFileManager
系统提供了NSFileManager类给开发者读取沙盒目录中的文件。NSFileManager是单例,通过defaultManager方法可以获取:
NSFileManager *fileManager = [NSFileManager defaultManager]; 更详细的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];
-
NSUserDefault
iOS系统提供的持久化存储数据的类,该方法是多线程安全的单例,在沙盒中的存储是用plist进行保存。如果是NSString、NSNubmber、NSData等基础类型可以直接存储在NSUserDefault,如果是自定义对象则需要实现NSCoding进行对象的序列化和反序列化。
比如说存储一个integer数据:
[[NSUserDefaults standardUserDefaults] setinteger:1234 forKey:@"key_for_test"];
读取存储的数据:
[[NSUserDefaults standardUserDefaults] integerForKey:@"key_for_test"];
NSUserDefault会由系统自动将数据写入plist中,iOS的老版本也可以调用sychronize方法手动同步,避免写入数据后系统还没将其写入plist而用户退出应用(最新的iOS版本已经不需要)。 实际开发中,由于NSUserDefault的性能较差并且同步也不及时,多用第三方库MMKV来取代NSUserDefault,但是因为某些系统库仍会读取NSUserDefault上的值,NSUserDefault在工程中仍占有一席之地。
思考题
-
我们工程中的图片资源是不是放在沙盒中呢?
- 不,是应用目录。
-
通过CocoaPods安装的Pod库,要如何读取其资源? 需要通过pod库的名字加载指定的bundle
NSString *path = [[NSBundle mainBundle] pathForResource:@"BDReader" ofType:@"bundle"];
NSBundle *podBundle = [NSBundle bundleWithPath:path];
SQLite
SQLite3是一款轻型的关系型数据库,在移动端中广泛应用。
SQLite3基于C语言实现, OC可以直接兼容, iOS系统也自带了SQLite3,提供的方法是直接操作数据库。
sqlite3的原生语言是C语言,接口的调用与OC风格不太一样,感觉较为复杂。
FMDB
FMDB对SQLite数据库进行封装,开放OC的接口便于开发者接入,是很普遍使用的iOS第三方数据库,也可以使用pod接入。
三个核心类
- FMDatabase:表示一个SQLite数据库,用于执行sql语句;
- FMResultSet:FMDatabase执行查询得到的结果集;
- FMDatabaseQueue:多线程用的查询或更新队列;
FMDB的使用
FMDatabase *db = [FMDatabase databaseWithPath:path];//create db
[db open];//open
NSString *createSqlStr = @"create table if not exists test_table_name(id integer primary key autoincrement,test_name_key char)";//create table
[db executeUpdate:createSqlStr];
//insert table
NSString *insertSqlStr = @"insert into test_table_name(test_name_key) values('anymore)";
[db executeUpdate:insertSqlStr];
sql还可以使用?参数,然后再执行时填写具体的值;查询也很方便,可以结合FMDatabaseQueue。 FMDatabaseQueue是使所有操作都在同一个队列进行,避免多线程操作数据库,引起数据异常。
具体使用方法请查阅官方文档
CoreData
如果不想使用第三方库,也可以使用iOS系统提供的CoreData框架。
CoreData的接口更加简化,部分可视化操作,对象代码自动生成等。
CoreData的操作过程中完全没有c语言的风格,也没有sql语句。
具体使用方法请查阅官方文档
配合前面所学的知识,我们从沙盒可以导出项目中实际使用的数据库,比如说找到Tracker用的db。用SQLPro for SQlite打开,就可以看到里面的具体信息。
Keychain
>从上文我们可以知道,保存在沙盒目录的数据也是不安全的,用户可能会导出沙盒数据进行分析。有没有什么保存方式是更安全的呢?
iOS给出的答案是keychain。 keychain是iOS提供给App存储敏感和安全相关数据用的工具。keychain同样会被itunes备份,即使app重装仍能读取到上次保存的结果。为了保证数据安全,keychain内的数据都是经过加密的。 使用方法:
- 打开keychain的开关;
- import<Security/Security.h>;
- 使用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;
比之前更加贴近OC的语法。 具体的使用样例:
KeychainWrapper *wrapper = [[KeychainWrapper alloc] initWithService:kKeychainService account:self.account accessGroup:kKeychainAccessGroup];
NSString *saveStr = [wrapper readPassword];
if(!saveStr){
[wrapper savePassword:@"test_password"];
}
NSLog(@"saveStr:%@", saveStr);
只要保存在keychain中,即使应用卸载重装,仍旧能读取到该值。
对象的序列化
前面介绍了各种存储的工具,那么如何把运行中的对象序列化成第三方库呢? 有的开发者会使用系统提供的NSCoding协议手动添加字段,有的开发者会使用Runtime自动实现NSCoding,有的开发者会使用成熟的第三方库(例如YYModel)。
- NSCoding NSCoding是系统提供的序列化协议,在对象转换为二进制的时候,会通过NSCoding的方法回调开发者。
- YYModel
随着iOS社区的发展,又一个序列化的第三方库脱颖而出,那就是YYModel。
- 利用iOS的Runtime特点,无需继承
- 安全转换数据类型,常见Crash都进行了保护
- 扩展性强,提供多种容器扩展 YYModel可以使用CocoaPod安装,使用时现在SSUser.h添加YYModel的协议声明; 将字典转换为对象:
YYModel还提供丰富的特性,比如自定义属性名映射、容易类型转换、自定义类的数据映射。NSDictionary *dic = @{ @"gender":@0, @"userName":@"test_name", }; SSUser *user = [SSUser modelWithDictionary:dic];
总结
iOS的本地数据存储,其实就是内存数据的序列化和反序列化。
通常我们的数据都会保存在沙盒目录中,读取的时候可以直接指定路径,也可以用NSFileManager去查找和遍历目录:我们工程中的资源文件会存在应用目录,需要用NSBundle去读取。
APP在运行过程中,有时候需要临时保存一些变量,在下次运行时读取,此时可以用轻量级的持久化工具NSUserDefault,如果数据量比较大则需要考虑使用数据存储。SQLite3是iOS中最常用的数据库,通常我们会使用第三方封装库FMDB来操作,简化代码逻辑。
如果涉及到安全相关的敏感数据,则不应该 保存在文件、数据库等可以被抓取的地方。此时可以使用iOS提供的keychain对敏感数据进行保存。keychain的数据是经过加密处理,具有较高的安全性。
再将对象转换成二进制数据,以及将二进制数据转换成对象时,可以使用系统提供的NSCoding协议,也可以使用第三方库YYModel。
课后思考
- NSUserDefault和MMKV,两者各自的优缺点是什么?
- 一个App在选择用什么数据库的时候,要考虑哪些因素?
- YYModel的实现原理是什么?