标签归档:IOS

iOS数据持久化存储总结

本文中的代码托管在github上:https://github.com/WindyShade/DataSaveMethods
相对复杂的App仅靠内存的数据肯定无法满足,数据写磁盘作持久化存储是几乎每个客户端软件都需要做的。简单如“是否第一次打开”的BOOL值,大到游戏的进度和状态等数据,都需要进行本地持久化存储。这些数据的存储本质上就是写磁盘存文件,原始一点可以用iOS本身支持有NSFileManager这样的API,或者干脆C语言fwrite/fread,Cocoa Touch本身也提供了一些存储方式,如NSUserDefaults,CoreData等。总的来说,iOS平台数据持久存储方法大致如下所列:

  • Raw File APIs
  • UserDefault
  • NSCoding => NSKeyedArchived
  • Plist File
  • SQLite(使用C语言)
  • CoreData

一、Raw File APIs

ObjC是C的一个超集,所以最笨的方法我们可以直接用C作文件读写来实现数据存储:
1. 写入文件

    // File path
    const char * pFilePath = [_path cStringUsingEncoding:NSUTF8StringEncoding];

    // Create a new file
    FILE * pFile = fopen(pFilePath, "w+");

    if (pFile == NULL) {
        NSLog(@"Open File ERROR!");
        return;
    }

    const char * content = [_textField.text cStringUsingEncoding:NSUTF8StringEncoding];
    fwrite(content, sizeof(content), 1, pFile);
    fclose(pFile);

2. 读取文件

    // File path
    const char * pFilePath = [_path cStringUsingEncoding:NSUTF8StringEncoding];

    // Create a new file
    FILE * pFile = fopen(pFilePath, "r+");

    if (pFile == NULL) {
        NSLog(@"Open File ERROR!");
        return;
    }

    int fileSize = ftell(pFile);
    NSLog(@"fileSize: %d", fileSize);

    char * content[20];

    fread(content, 20, 20, pFile);

    NSString * aStr = [NSString stringWithFormat:@"%s", &content];

    if (aStr != nil && ![aStr isEqualToString:@""]) {
        _textField.text = aStr;
    }

    fclose(pFile);

二、NSUserDefaults

但是既然在iOS平台作开发,我们当然不至于要到使用C的原生文件接口这种地步,下面就介绍几种iOS开发中常用的数据本地存储方式。使用起来最简单的大概就是Cocoa提供的NSUserDefaults了,Cocoa会为每个app自动创建一个数据库,用来存储App本身的偏好设置,如:开关音效,音量调整之类的少量信息。NSUserDefaults是一个单例,生命后期由App掌管,使用时用 [NSUserDefaults standardUserDefaults] 接口获取单例对象。NSUserDefaults本质上是以Key-Value形式存成plist文件,放在App的Library/Preferences目录下,对于已越狱的机器来说,这个文件是不安全的,所以**千万不要用NSUserDefaults来存储密码之类的敏感信息**,用户名密码应该使用**KeyChains**来存储。

1.写入数据

        // 获取一个NSUserDefaults对象
        NSUserDefaults * aUserDefaults = [NSUserDefaults standardUserDefaults];
        // 插入一个key-value值
        [aUserDefaults setObject:_textField.text forKey:@"Text"];

        // 这里是为了把设置及时写入文件,防止由于崩溃等情况App内存信息丢失
        [aUserDefaults synchronize];

2.读取数据

    NSUserDefaults * aUserDefaults = [NSUserDefaults standardUserDefaults];
                // 获取一个key-value值
    NSString * aStr = [aUserDefaults objectForKey:@"Text"];

使用起来很简单吧,它的接口跟 NSMutableDictionary 一样,看它的头文件,事实上在内存里面也是用dictionary来存的。写数据的时候记得用 synchronize 方法写入文件,否则 crash了数据就丢了。

三、Plist

上一节提到NSUserDefaults事实上是存成Plist文件,只是Apple帮我们封装好了读写方法而已。NSUserDefaults的缺陷是存储只能是Library/Preferences/<Application BundleIdentifier>.plist 这个文件,如果我们要自己写一个Plist文件呢? 使用NSFileManger可以很容易办到。事实上Plist文件是XML格式的,如果你存储的数据是Plist文件支持的类型,直接用NSFileManager的writToFile接口就可以写入一个plist文件了。 ### Plist文件支持的数据格式有: NSString, NSNumber, Boolean, NSDate, NSData, NSArray, 和NSDictionary. 其中,Boolean格式事实上以[NSNumber numberOfBool:YES/NO];这样的形式表示。NSNumber支持float和int两种格式。

读写Plist文件

1. 首先创建plist文件:

                // 文件的路径
                NSString * _path = [[NSTemporaryDirectory() stringByAppendingString:@"save.plist"] retain];
                // 获取一个NSFileManger
          NSFileManager * aFileManager = [NSFileManager defaultManager];
          if (![aFileManager fileExistsAtPath:_path]){
                // 文件不存在,创建之
                NSMutableDictionary * aDefaultDict = [[NSMutableDictionary alloc] init];
                                        // 插入一个值,此时数据仍存在内存里
                [aDefaultDict setObject:@"test" forKey:@"TestText"];

                                        // 使用NSMutableDictionary的写文件接口自动创建一个Plist文件
                if (![aDefaultDict writeToFile:_path atomically:YES]) {
                    NSLog(@"OMG!!!");
                }

                [aDefaultDict release];
            }

2. 写入文件

                // 写入数据
        NSMutableDictionary * aDataDict = [NSMutableDictionary dictionaryWithContentsOfFile:_path];
        [aDataDict setObject:_textField.text forKey:@"TestText"];
            if (![aDataDict writeToFile:_path atomically:YES]) {
                NSLog(@"OMG!!!");
            }

3. 读取文件

                 NSMutableDictionary * aDataDict = [NSMutableDictionary dictionaryWithContentsOfFile:_path];
            NSString * aStr = [aDataDict objectForKey:@"TestText"];
            if (aStr != nil && aStr.length > 0) {
                _textField.text = aStr;
            }

四、NSCoding + NSKeyedArchiver

上面介绍的几种方法中,直接用C语言的接口显然是最不方便的,拿出来的数据还得自己进行类型转换。NSUserDefaults和Plist文件支持常用数据类型,但是不支持自定义的数据对象,好像Cocoa提供了NSCoding和NSKeyArchiver两个工具类,可以把我们自定义的对象编码成二进制数据流,然后存进文件里面,下面的Sample为了简单我直接用cocoa的接口写成plist文件。 如果要使用这种方式进行存储,首先自定义的对象要继承NSCoding的delegate。

        @interface WSNSCodingData : NSObject<NSCoding>

然后继承两个必须实现的方法encodeWithCoder:和initWithCoder:

        - (void)encodeWithCoder:(NSCoder *)enoder {
            [enoder encodeObject:data forKey:kDATA_KEY];
        }

        - (id)initWithCoder:(NSCoder *)decoder {
            data = [[decoder decodeObjectForKey:kDATA_KEY] copy];
                        return [self init];
        }

这里data是我自己定义的WSNSCodingData这个数据对象的成员变量,由于数据在使用过程中需要持续保存在内存中,所以类型为copy,或者retain也可以,记得在dealloc函数里面要realease。这样,我们就定义了一个可以使用NSCoding进行编码的数据对象。

保存数据:

        - (void)saveData {
            if (aData == nil) {
                aData = [[WSNSCodingData alloc] init];
            }

            aData.data = _textField.text;

            NSLog(@"save data...%@", aData.data);
                        // 这里init的NSMutableData是临时用来存储数据的
            NSMutableData   * data = [[NSMutableData alloc] init];
                        // 这个NSKeyedArchiver则是进行编码用的
            NSKeyedArchiver * archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
            [archiver encodeObject:aData forKey:DATA_KEY];
            [archiver finishEncoding];
                        // 编码完成后的NSData,使用其写文件接口写入文件存起来
            [data writeToFile:_path atomically:YES];
            [archiver release];
            [data release];

            NSLog(@"save data: %@", aData.data);
        }

读取数据:

        - (void)loadData {
            NSLog(@"load file: %@", _path);
            NSData * codedData = [[NSData alloc] initWithContentsOfFile:_path];
            if (codedData == nil) return;

                        // NSKeyedUnarchiver用来解码
            NSKeyedUnarchiver * unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:codedData];
                        // 解码后的数据被存在一个WSNSCodingData数据对象里面
            aData = [[unarchiver decodeObjectForKey:DATA_KEY] retain];
            [unarchiver finishDecoding];
            [unarchiver release];

            [codedData release];

            if (aData.data != nil) {
                _textField.text = aData.data;
            }
        }

所以其实使用NSCoding和NSKeyedArchiver事实上也是写plist文件,只不过对复杂对象进行了编码使得plist支持更多数据类型而已。

五、 SQLite

如果App涉及到的数据多且杂,还涉及关系查询,那么毋庸置疑要使用到数据库了。Cocoa本身提供了CoreData这样比较重的数据库框架,下一节会讲到,这一节讲一个轻量级的数据库——SQLite。 SQLite是C写的的,做iOS开发只需要在工程里面加入需要的框架和头文件就可以用了,只是我们得用C语言来进行SQLite操作。 关于SQLite的使用参考了这篇文章:http://mobile.51cto.com/iphone-288898.htm但是稍微有点不一样。

1. 在编写SQLite代码之前,我们需要引入SQLite3头文件:

        #import <sqlite3.h>

2. 然后给工程加入 libsqlite3.0.dylib 框架。 3. 然后就可以开始使用了。首先是打开数据库:

                - (void)openDB {
                    NSArray * documentsPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory
                                                                                , NSUserDomainMask
                                                                                , YES);
                    NSString * databaseFilePath = [[documentsPaths objectAtIndex:0] stringByAppendingPathComponent:@"mydb"];

                    // SQLite存的最终还是文件,如果没有该文件则会创建一个
                    if (sqlite3_open([databaseFilePath UTF8String], &_db) == SQLITE_OK) {
                        NSLog(@"Successfully open database.");
                        // 如果没有表则创建一个表
                        [self creatTable];
                    }
                }

3.关闭数据库,在dealloc函数里面调用:

                - (void)closeDB {
                    sqlite3_close(_db);
                }

4.创建一个表:

                - (void)creatTable {
                    char * errorMsg;
                    const char * createSql="create table if not exists datas (id integer primary key autoincrement,name text)";

                    if (sqlite3_exec(_db, createSql, NULL, NULL, &errorMsg) == SQLITE_OK) {
                        NSLog(@"Successfully create data table.");
                    }
                    else {
                        NSLog(@"Error: %s",errorMsg);
                        sqlite3_free(errorMsg);
                    }
                }

5. 写入数据库

                - (void)saveData {
                    char * errorMsg;
                        // 向 datas 表中插入 name = _textFiled.text 的数据
                    NSString * insertSQL = [NSString stringWithFormat:@"insert into datas (name) values('%@')", _textField.text];

                        // 执行该 SQL 语句
                    if (sqlite3_exec(_db, [insertSQL cStringUsingEncoding:NSUTF8StringEncoding], NULL, NULL, &errorMsg)==SQLITE_OK) {
                        NSLog(@"insert ok.");
                    }
                }

6. 读取数据库

                - (void)loadData {
                    [self openDB];

                    const char * selectSql="select id,name from datas";
                    sqlite3_stmt * statement;
                    if (sqlite3_prepare_v2(_db, selectSql, -1, &statement, nil)==SQLITE_OK) {
                        NSLog(@"select ok.");
                    }

                    while (sqlite3_step(statement) == SQLITE_ROW) {
                        int _id = sqlite3_column_int(statement, 0);
                        NSString * name = [[NSString alloc] initWithCString:(char *)sqlite3_column_text(statement, 1) encoding:NSUTF8StringEncoding];
                        NSLog(@"row>>id %i, name %@",_id,name);

                        _textField.text = name;
                    }

                    sqlite3_finalize(statement);
                }

五、CoreData

大型数据存储和管理。 XCode自带有图形化工具,可以自动生成数据类型的代码。 最终存储格式不一定存成SQLite,可以是XML等形式。 (未完待续。。。)

iOS xcode debug调试

1. Xcode内置GDB,可以使用GDB调试,调试命令:

  1.1 po 命令:为 print object 的缩写,显示对象的文本描述

  (lldb) po [$eax class]:输出异常对象的地址

  (lldb) po [$eax name]:输出这个异常的名字

  (lldb) po [$eax reason]:这个将会输出错误消息:

  (lldb) “po $eax”:对这个对象调用“description”方法和打印出来

  “$eax”是cup的一个寄存器。在一个异常的情况下,这个寄存器将会包含一个异常对象的指针。注意:$eax只会在模拟器里面工作,假如你在设备上调试,你将需要使用”$r0″寄存器

  1.2 print 命令:有点类似于格式化输出,可以输出对象的不同信息

  比如:print (char*)[[dic description] cString]、(lldb) print (int)[label retainCount]

  1.3 info 命令:我们可以查看内存地址所在信息

  1.4 info line *内存地址:可以获取内存地址所在的代码行相关信息

  1.5 show 命令:显示 GDB 相关的信息。如:show version 显示GDB版本信息

  1.6 bt: 显示当前进程的函数调用栈的情况;”up num”:查看调用的详细信息;down:返回栈列表;l:显示详细代码信息;p:输出数值。

  2. 添加全局断点(Add Exception BreakPoint):

  2.1 添加步骤:

  1. In the bottom-left corner of the breakpoints navigator, click the Add button.

  2. Choose Add Exception Breakpoint.

  3. Choose the type of exception from the Exception pop-up menu.

  4. Choose the phase of the exception handling process at which you want program execution to stop.

  5. Click Done.

  2.2 使用场景:

  程序因为SIGABRT而crash,想要定位到导致crash的行。

  3. 添加符号断点(Add Symbolic BreakPoint):

  3.1 断点执行的时机:Symbolic breakpoints stop program execution when a specific function or method starts executing

  3.2 添加步骤:

  1. Steps In the bottom-left corner of the breakpoint navigator, click the Add button.

  2. Choose Add Symbolic Breakpoint.

  3. Enter the symbol name in the Symbol field.

  4. Click Done.

  3.3 使用场景:

  当想让系统在某个指定条件处中断时,设置相应的断点。

  比如:

  objc_exception_throw:在系统抛出异常处设置断点。

  -[NSException raise]:

  4. 设置NSZombieEnabled、MallocStackLogging、NSAutoreleaseFreedObjectCheckEnabled、NSDebugEnabled:

  4.1 设置方法:

  1. Product->Edit Scheme…->Run…->EnvironmentVariables.

  2. add NSZombieEnabled,set the value with YES

  3. add MallocStackLogging, set the value with YES.

  4. add NSAutoreleaseFreedObjectCheckEnabled, set the value with YES.

  5. add NSDebugEnabled, set the value with YES.

  4.2 使用场景:

  主要为了解决EXC_BAD_ACCESS问题,MallocStackLogging用来启用malloc记录(使用方式 malloc_history ${App_PID} ${Object_instance_addr})。

  4.3 需要注意的问题

  NSZombieEnabled只能在调试的时候使用,千万不要忘记在产品发布的时候去掉,因为NSZombieEnabled不会真正去释放dealloc对象的内存。

  5. 重写respondsToSelector方法

  5.1 实现方式

  #ifdef _FOR_DEBUG_

  -(BOOL) respondsToSelector:(SEL)aSelector {

  printf(“SELECTOR: %sn”, [NSStringFromSelector(aSelector) UTF8String]);

  return [super respondsToSelector:aSelector];

  }

  #endif

  5.2 使用方法:

  需要在每个object的.m或者.mm文件中加入上面代码(应该可以使用类属实现),并且在other c flags中加入-D _FOR_DEBUG_(记住请只在Debug Configuration下加入此标记)。这样当你程序崩溃时,Xcode的console上就会准确地记录了最后运行的object的方法。