iOS 数据本地化

需求

将数据保存至本地以便后续的使用,在应用中非常的常见,例如资讯类应用、即时通讯类应用等。即时非上述应用,那么也避免不了本地化用户的偏好信息,登陆信息等等。

iOS 开发有多种本地化的手段,针对不同场景显示出不同的优缺点,你可以根据任务的情况进行选择。

存储方式

iOS 本地化存储方式大体分为下面几种。

  • 文件存储,如NSUserDefaults
  • 归档
  • SQLite 数据库
  • Core Data

iOS 中的沙盒(sandbox)

每个 iOS 应用程序都有自己的应用沙盒,可以将沙盒简单的理解为应用的文件夹,每个应用的沙盒都是相互独立的,其他应用不能访问该沙盒,自己也不能访问其他应用的沙盒。一般情况下,想要将应用文件分享到其他应用需要借助系统,编写相应的扩展才能实现,这涉及到 iOS 8 新出的程序扩展类容,这里不做过多的讲解。

沙盒的结构与用途

可以借助 Xcode 工具将开发程序的沙盒数据下载到本地,右击显示包内容查看当前应用下的沙盒目录,以及已经存储数据信息。

应用的沙盒目录
  • Documents:保存应用运行时生成的需要持久化的数据,iTunes同步设备时会备份该目录。重要数据
  • Library/Caches:保存应用运行时生成的需要持久化的数据,iTunes同步设备不会备份该目录。非重要数据
  • Library/Preference:保存应用的偏好设置,iTunes同步设备时会备份该目录。
  • tmp:保存应用运行时所需的临时数据,使用完毕后再将相应的文件从该目录删除。应用没有运行时,系统在存储空间紧缺的情况下也可能会清除该目录下的文件。iTunes同步设备时不会备份该目录。

注:使用 NSUserDefaults 对象存储数据时,会默认创建名为 BundleIdentifier.plist 的文件来保存数据,就如上图展示。

沙盒目录的获取方式

Documents 文件夹的获取方式

// 利用沙盒根目录拼接字符串
NSString *homePath = NSHomeDirectory();
NSString *docPath = [homePath stringByAppendingString:@"/Documents"];

// 利用沙盒根目录拼接”Documents”字符串
NSString *homePath = NSHomeDirectory();
NSString *docPath = [homePath stringByAppendingPathComponent:@"Documents"];

推荐下面的方式来获取

// NSDocumentDirectory 要查找的文件
// NSUserDomainMask 代表从用户文件夹下找
// 在iOS中,只有一个目录跟传入的参数匹配,所以这个集合里面只有一个元素
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *filePath = [path stringByAppendingPathComponent:@"xxx.plist"];

这里的参数说明一下:
第一个参数:枚举类型,常用的有

  • NSDocumentDirectory(Documents文件夹)
  • NSCachesDirectory(Library/Caches)

第二个参数:NSUserDomainMask,表示从用户文件夹下找。
第三个参数:打印的路径会是这种形式~/Documents,我们一般都会用YES,这样可以获取完整路径字符串。
这个方法的返回值是一个数组,但在iOS中,只有一个目录跟传入的参数匹配,所以这个集合里面只有一个元素,所以我们取第一个元素

Library/Caches 文件夹的获取方式

NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
NSString *filePath = [path stringByAppendingPathComponent:@"student.data"];

tmp 文件夹的获取方式

NSString *tmp= NSTemporaryDirectory();

文件形式存储

  • NSUserDefaults

NSUserDefaults 类设计用来保存应用的偏好属性,例如WIFI情况下是否自动播放视频,是否缓存图像等等,又或者是用户相关的数据信息等。

可以存储的数据类型:NSData、NSString、NSNumber、NSDate、NSArray、NSDictionary。如果要存储其他类型,如 UIImage,则需要转换为前面的类型,才能用 NSUserDefaults 存储。

使用示例一:

// 存储
NSString *passWord = @"1234567";
NSUserDefaults *user = [NSUserDefaults standardUserDefaults];
[user setObject:passWord forKey:@"userPassWord"];
// 获取
NSUserDefaults *user = [NSUserDefaults standardUserDefaults];
NSString *passWord = [ user objectForKey:@"userPassWord"];

使用示例二:

//存数据
UIImage *image=[[UIImage alloc]initWithContentsOfFile:@"photo.jpg"];
NSData *imageData = UIImageJPEGRepresentation(image, 100);//UIImage对象转换成NSData
[[NSUserDefaults standardUserDefaults] setObject:imageData forKey:@"image"]
[[NSUserDefaults standardUserDefaults] synchronize];//用synchronize方法把数据即时持久化到standardUserDefaults数据库中,因为NSUserDefaults存储数据不是及时的
//取数据
NSData *imageData = [[NSUserDefaults standardUserDefaults] dataForKey:@"image"];
UIImage *Image = [UIImage imageWithData:imageData];//NSData转换为UIImage

使用示例三:

自定义对象存储到本地,需要转换为 NSData 类型,再存储到本地。我们需要借助归档对象实现自定义对象到 NSData 的转换,并且对象需要遵守 NSCoding 协议,实现 encodeWithCoderinitWithCoder 方法,前者是将对象进行编码,后者用来解码。

以 Student 类为例。
首先 .h 文件中要继承 NSCoding 协议,.m文件如下:

- (void)encodeWithCoder:(NSCoder *)coder {
    [coder encodeObject:self.name forKey:@"name"];
    [coder encodeInteger:self.age forKey:@"age"];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
    if (self = [super init]) {
        self.age = [coder decodeIntegerForKey:@"age"];
        self.name = [coder decodeObjectForKey:@"name"];
    }
    return self;
}

自定义对象完成了 Coding 协议,我们来将自定义对象数组存储到本地。

// 存
NSArray * array = [NSArray arrayWithArray:dataArray];
[[NSUserDefaults standardUserDefaults] setObject:array forKey:@"allStudent"];
// 取
NSdData *data = [[NSUserDefaults standardUserDefaults] objectForKey:@"allStudent"];
NSArray *allStudentArray = [NSKeyedUnarchiver unarchiveObjectWithData:data];

注:不要企图将自定义对象数组直接存储到本地。

一般来说,NSUserDefaults 是被设计用来存储一段简单的偏好设置的,就如他的命名一样。不要将大篇幅的数据存储到 NSUserDefaults 中,因为 NSUserDefaults 本质上是文件存储,它会再你第一次使用它时为你创建一份 plist 文件,你的数据越大意味着存取所话费的时间越久。

  • 自定义文件

除了使用 NSUserDefaults 来存储信息,我们也可以将数组写到其他文件中。

可以存储的数据类型:NSArray、NSDictionary、NSString、NSData。

NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *filePath = [path stringByAppendingPathComponent:@"myfile.plist"];
// 存储
NSArray *arr = [[NSArray alloc] initWithObjects:@"1", @"2", nil];
[arr writeToFile:filePath atomically:YES];
// 获取
NSArray *arr = [NSArray arrayWithContentsOfFile:filePath];

注意事项:

使用自定义文件存储时,同样需要注意数据的合法性,例如自定义对象数组是无法写入的。另外,一份文件中通常只能存储同一种类型,并且写入数据默认是覆盖的,所以如果你是想要追加写入时,需要先将数据读取出来,追加数据之后再次写入。

因此 NSUserDefaults 类已经帮你封装好了,使用起来无需亲自创建文件,数据的添加、修改等,使用自定义文件存储,不如使用 NSUserDefaults。但是考虑到局限性,依旧不推荐存储大篇幅的数据。

自定义对象的存取

在之前有演示过自定义对象的存储,自定义对象需要遵守 NSCoding 协议。

@interface Possession:NSObject<NSCoding> {
     NSString *name;//待归档类型
}

@implementation Possession
-(void)encodeWithCoder:(NSCoder *)aCoder{
   [aCoder encodeObject:name forKey:@"name"];
}
-(void)initWithCoder:(NSCoder *)aDecoder{
   name=[[aDeCoder decodeObjectforKey:@"name"] retain];
}

NSKeyedArchiver 除了可以将对象转换为 NSData 类型外,还能直接将数据保存到指定文件。

[NSKeyedArchiver archiveRootObject:allPossessions toFile: path];
allPossessions = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

归档能够方便的存储自定义对象,但是缺点也是显而易见的 -- 只能一次性归档一次性解压,如果想要修改部分数据,需要解压整个数据,所以只能针对数据量小的数据,否则会消耗巨大的内存空间,操作数组对象速度也不够快。

本地数据库

存储并管理大量数据时,数据库都是你非常不错的选择,有了数据库,你可以处理数据的数量级非常大,并且速度非常快。SQLite 是一款中小型数据库,在进行数据操作时候,需要使用到 C 语言的函数,相对比较麻烦。市面上有很多针对 SQLite 进行封装的库,如 FMDBPlausibleDatabase 等,其中 FMDB 是一款使用非常广泛的数据库。

FMDB 是对 libsqlite3 框架的封装,用起来的步骤与 SQLite 使用类似,并且它对于多线程的并发操作进行了处理,所以是线程安全的。

FMDB 中几个重要的类:

  • FMDatabase :数据库对象
  • FMResultSet :结果集
  • FMDatabaseQueue : 线程安全的操作队列
创建数据库

FMDB 能够接受处理 sql 语句,下面我们来演示一下数据库的创建和表的创建。

NSString *doc =[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES)  lastObject]; 
NSString *fileName = [doc stringByAppendingPathComponent:@“student.sqlite”];

// 获得数据库对象
FMDatabase *db = [FMDatabase databaseWithPath:fileName];
if ([db open]){
    // 创表
    BOOL result = [db executeUpdate:@“CREATE TABLE IF NOT EXISTS t_student (id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL, age integer NOT NULL);”];
    if (result){
        NSLog(@“创建表成功”);
    }
}

除了查询的 SQL 命令使用 -excuteQuery:,绝大部分的修改操作都是使用 -executeUpdate: 方法。

查询数据和处理结果集。

 //查询整个表
FMResultSet *resultSet = [self.db executeQuery:@“select * from t_student;”];
// 遍历结果集合   
while ([resultSet  next]){
    NSString *name = [resultSet objectForColumn:@“name”];
    int age = [resultSet intForColumn:@“age”];
}    

线程安全问题

在多个线程中使用一个 FMDatabase 实例是不安全的,FMDatabaseQueue 提供了一个线程安全的队列,你应该使用该队列取操作你的数据库,而不是 FMDatabase,除非你非常的确定数据操作不会发生在多个线程中。

//创建队列
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
__block BOOL whoopsSomethingWrongHappened = true;
//任务包装到事务里
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {  
    whoopsSomethingWrongHappened &= [db executeUpdate:@“INSERT INTO myTable VALUES (?)”,[NSNumber numberWith:1]];
    whoopsSomethingWrongHappened &= [db executeUpdata:@“INSERT INTO myTable VALUES (?)”,[NSNumber numberWithInt:2]];
    whoopsSomethingWrongHappened &= [db executeUpdata:@“INSERT INTO myTable VALUES (?)”[NSNumber numberWithInt:3]];
    // 如果有错误 返回
    if (!whoopsSomethingWrongHappened) { 
        *rollback = YES;
        return;
    }
}];

关于本地的数据库查看,你依然可以下载沙盒文件到本地,通过一些数据库工具打开查询当前的数据库信息。

Core Data

Core Data 是 iOS5 推出的框架,它提供了对象-关系映射(ORM)功能,能够将对象转换成数据保存到 SQLite 数据库中,也能够将数据取出还原成对象。

总结

本地化手段多样,但都各司其职,少量、偏好性质的数据可以使用 NSUserDefaults 方式本地保存,大批量、需频繁操作的数据最好采用数据库进行存取。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容