iOS的Sandbox及一些存储方式

今天谈谈存储,主要是沙盒和一些存储方式,本章全部讨论的闪存中操作,除 NSURLCache 会涉及一些内存缓存之外,其余均为闪存持久化操作。

1. Sandbox 系统

Sandbox 本身存于闪存之中,是持久化的载体。

获取App Sandbox根目录:

NSString *homeDirectory = NSHomeDirectory();  

1.1 Documents

用户产生的数据建议放在此目录下,iTunes 备份/恢复是包含此目录的。 获取 Documents 目录:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);  
NSString *path = [paths objectAtIndex:0];  

1.2 Library

存放 App 默认设置及其相关信息 获取 Library 目录:

//String格式
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);  
NSString *path = [paths objectAtIndex:0];  
//Url格式
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)  
let path = urls[urls.count-1] as NSURL  
  • Library/Caches 存放临时的缓存文件,iTunes 不会备份此目录,但也不会在 APP 退出时删除,适合存放一些离线数据,比如新闻客户端的离线新闻之类的。

  • Library/Preferences 存放程序设置等相关属性

1.3 tmp

提供一个临时存放路径,系统重启后会删除里面所有内容 获取 tmp 目录:

NSString *tmpDir = NSTemporaryDirectory();  

1.4 App Bundle

App 程序本身是个压缩包,包括可执行文件和相关资源

  • + (NSBundle *)mainBundle 得到程序本身的 bundle 类. NSBundle 提供了很多 path 相关函数
  • 运行期只读,不可写
  • + (UIImage *)imageNamed:(NSString *)name可以直接加载包中的图片文件
  • 包中带有的数据不能太多,不然不利于用户安装
  • - (NSString *)pathForResource:(NSString *)name ofType:(NSString *)extension使用这个函数查找包存放的文件

2. 持久化存储方式

2.1 NSUserDefaults

Cocoa会为每个 app 自动创建一个 plist 文件,用来存储 App 自身的偏好设置,NSUserDefaults 是一个单例,文件存放在 $(沙盒目录)/Library/Preferences/{Bundle Identifier}.plist中,使用时通过[NSUserDefaults standardUserDefaults]方法获取单例对象。synchronize 方法会定期被系统调用,来保证内存中的 cache 与用户 default 数据库同步。NSUserDefaults 本质上是以 Key-Value 形式存成 plist 文件,要注意的是:

  • 只能存储 property list(NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary)如果要存储这六种类型之外的数据需要序列化为NSData
  • 通过 valueForKey 返回的对象是不可变的,即使当初设置了一个可变的对象。

写入数据:

// 获取一个NSUserDefaults对象
NSUserDefaults *userDefaults =  [NSUserDefaults standardUserDefaults];  
// 设置值
[userDefaults setObject:person.name forKey:@"Name"];
// 这里是为了把设置及时写入文件,防止由于崩溃等情况App内存信息丢失
[userDefaults synchronize];

读取数据:

NSUserDefaults * aUserDefaults = [NSUserDefaults standardUserDefaults];  
NSString * name = [userDefaults objectForKey:@"Name"];  

2.2 Plist File

Plist 是 XML 格式的文件,NSUserDefaults 就是一个 plist 文件,但只能是 /Library/Preferences/{Bundle Identifier}.plist 这个文件,同样的 plist 只能存储 property list。
可以通过 Xcode 和 NSFileManger 来生成:

写入数据:

NSString * _path = [[NSTemporaryDirectory() stringByAppendingString:@"test.plist"] retain];  
NSFileManager * fileManager = [NSFileManager defaultManager];  
//如果不存在,用fileManager在制定路径创建一个test.plist
if (![fileManager fileExistsAtPath:_path]){  
//新建一个字典写入路径
      NSMutableDictionary * dict = [[NSMutableDictionary alloc] init];                 
      [dict setObject:@"test" forKey:@"Test"];
      if (![dict writeToFile:_path atomically:YES]) {
          NSLog(@"write to file failed");
      }
}

读取数据:

NSMutableDictionary * dict = [NSMutableDictionary dictionaryWithContentsOfFile:_path];  
NSSrting *str = [dict objectForKey:@"Test"];  
NSLog(@"str is %@", str);  

2.3 NSCoding => NSKeyedArchived

将自定义的类序列化,可以先让他遵循 NSCoding 协议,然后再使用 NSKeyedArchived 归档。NSCoding 是一个简单的协议,有两个方法: -initWithCoder: 和 encodeWithCoder:。遵循 NSCoding 协议的类可以被序列化和反序列化,这样可以归档到磁盘上或分发到网络上。

@interface Book : NSObject <NSCoding>
@property NSString *title;
@property NSString *author;
@property NSUInteger pageCount;
@property NSSet *categories;
@property (getter = isAvailable) BOOL available;
@end  
@implementation Book    
#pragma mark - NSCoding  
- (id)initWithCoder:(NSCoder *)decoder {
    self = [super init];
    if (!self) {
        return nil;
    }  
    self.title = [decoder decodeObjectForKey:@"title"];
    self.author = [decoder decodeObjectForKey:@"author"];
    self.pageCount = [decoder decodeIntegerForKey:@"pageCount"];
    self.categories = [decoder decodeObjectForKey:@"categories"];
    self.available = [decoder decodeBoolForKey:@"available"];  
    return self;
}  
- (void)encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeObject:self.title forKey:@"title"];
    [encoder encodeObject:self.author forKey:@"author"];
    [encoder encodeInteger:self.pageCount forKey:@"pageCount"];
    [encoder encodeObject:self.categories forKey:@"categories"];
    [encoder encodeBool:[self isAvailable] forKey:@"available"];
}  
@end

NSKeyedArchiver 和 NSKeyedUnarchiver 提供了很方便的API把对象读取/写入磁盘
Archiving:

[NSKeyedArchiver archiveRootObject:books toFile:@"/path/to/archive"];

Unarchiving:

[NSKeyedUnarchiver unarchiveObjectWithFile:@"/path/to/archive"];

2.4 Keychain系统

之前说过不要把密码保存在 userDefault 中,越狱了会被暴菊的,那么正确的位置应该是用 KeyChain 来存放密码或证书,数据经过加密存储,keychain 里保存的信息也不会因 App 删除而丢失,重新安装 App 时依然有效果。苹果自身的 keyChain 比较难用,下面推荐两个 keychain wrapper,一个 OC 版本,一个 Swift 版本,都支持 touchID:

2.5 Cache系统

专门用来存放缓存,会有过期时间,一般存放在沙盒的 Library/Caches 目录下 下面两种 Cache 虽然名字相近,但其实没什么联系

  • NSURLCache The URL loading system 提供了一个 on-disk and in-memory cache(磁盘与内存联合缓存)来缓存针对 requests的responses。这个cache可以有效减少网络连接提高性能。在 iOS 上,on-disk cache 可能会在磁盘容量底的情况下被清空,但也仅限于app没有运行时。更多详情Understanding Cache Access,NSHipster NSURLCache
  • NSCache 基本上就是一个会自动移除对象来释放内存的 NSMutableDictionary。无需响应内存警告或者使用计时器来清除缓存。唯一的不同之处是键对象不会像 NSMutableDictionary 中那样被复制,这实际上是它的一个优点(键不需要实现 NSCopying 协议)。更重要的他是线程安全的,AFNetworking中也使用他来做图片缓存。更多详情NSHipster NSCache

2.6 SQLite

推荐使用 FMDB,swift 版本的使用见 在iOS开发中使用FMDB 目前 FMDB 也通过添加“bridging header”的方式支持 swift:

  1. 拷贝 FMDB 文件夹下所有的 .m and .h文件到工程中
  2. 创建 "bridging header",详情见Swift and swift in the Same Project.
  3. 在 briding header 中添加#import "FMDB.h"
  4. 这步是可选的,从 "src/extra/Swift Extensions" 文件夹下拷贝 FMDatabaseVariadic.swift 到你的工程之中,他允许你使用 executeUpdate 和 executeQuery
// 创建test.sqlite的路径
let documentsFolder = NSSearchPathForDirectoriesInDomains(.DocumentDirectory,  
                                .UserDomainMask, true)[0] as String
let path = documentsFolder.stringByAppendingPathComponent("test.sqlite")  
//根据制定路径创建数据库文件
    let database = FMDatabase(path: path)
//打开数据库
    if !database.open() {
        println("Unable to open database")
        return
    }
//创建一个test表
    if !database.executeUpdate("create table test(x text, y text, z text)", withArgumentsInArray: nil) {
        println("create table failed: \(database.lastErrorMessage())")
    }
//插入数据a,b,c
    if !database.executeUpdate("insert into test (x, y, z) values (?, ?, ?)", withArgumentsInArray: ["a", "b", "c"]) {
        println("insert 1 table failed: \(database.lastErrorMessage())")
    }
//继续插入e,f,g
    if !database.executeUpdate("insert into test (x, y, z) values (?, ?, ?)", withArgumentsInArray: ["e", "f", "g"]) {
        println("insert 2 table failed: \(database.lastErrorMessage())")
    }
//查询
    if let rs = database.executeQuery("select x, y, z from test", withArgumentsInArray: nil) {
        while rs.next() {
            let x = rs.stringForColumn("x")
            let y = rs.stringForColumn("y")
            let z = rs.stringForColumn("z")
            println("x = \(x); y = \(y); z = \(z)")
        }
    } else {
        println("select failed: \(database.lastErrorMessage())")
    }
//关闭数据库
    database.close()

2.7 CoreData

参照之前 CoreData 文章

2.8 Realm

一个专为移动设备设计的新型数据库旨在替代 Sqlite 和 CoreData,目前还在持续改进中,详情

3. 文件格式

二进制:NSData plist:本质是 xml,NSUserDefault
对象序列化:NSKeyedArchiver sqlite:文件数据库, Core Data/libsqlite3/FMDB


-EOF-

如果感觉此文对你有帮助,请随意打赏支持作者 😘

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!