美文网首页iOS知识小总结
iOS 数据持久化基础知识小总结

iOS 数据持久化基础知识小总结

作者: 宇宇宇啊啊啊 | 来源:发表于2018-04-13 18:02 被阅读0次

1. 应用的沙盒

注意:如果要为应用程序启用分享功能,需要打开它的info.plist文件并添加键为Application supports iTunes file sharing值为YES的Item;

  • Documents:应用程序可以将数据存储在Documents目录中。如果应用程序启用了iTunes文件分享功能,用户就可以在iTunes中看到目录的内容(以及应用程序创建的所有子目录),还可以对其更新文件。

  • Library:应用程序也可以在这里存储数据。它用来存放不想分享给用户的文件。需要时你可以创建自己的子目录。

  • tmp: tmp目录供应用存储临时文件。当iOS设备执行同步时,iTunes不会备份tmp中的文件。在不需要这些文件时,应用要负责删除tmp中的文件,以免占用文件系统的控件。

1. 获取Documents目录:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

NSString *documentsDirectory = [paths firstObject];

a.常量NSDocumentDirectory表明正在查找Document目录的路径。

b.常量NSUserDomainMask表明希望将所搜限制在应用的沙盒中。

NSString *filename = [documentsDirectory stringByAppendingPathComponent:@"theFile.txt"];

完成此调用后,filename就包含了指向用用Documents目录中theFile.txt文件的完整路径。然后可以根据filename来创建、读取和写入文件。

2. 获取Library目录:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);

NSString *libraryDirectory = [paths firstObject];

文件操作同上;

3. 获取tmp目录:

NSString *tempPath = NSTemporaryDirectory();

创建该路径下的文件的路径:

NSString *tempFile = [tempPath stringByAppendingPathComponent:@"tempFile.txt"];


2. 属性列表序列化

序列化对象是指可以被转化为字节流以便于存储到文件中或者通过网络进行传输的对象。虽然任何对象都可以被序列化,但只有某些对象才能被放置到某个集合类中(如NSDictionary或NSArray中),然后才使用该集合类的writeToFile:atomically:或writeToURL:atomically:方法将它们存储到属性列表中。可以按照该方法序列化下面的类:

  • NSArray
  • NSMutableArray
  • NSDictionary
  • NSData
  • NSMutableData
  • NSString
  • NSMutableString
  • NSNumber
  • NSDate
  • 如果只使用这些对象构建数据模型,就可以使用属性列表来方便的保存和加载数据。

  • 如果打算使用属性列表持久保存应用数据,则可以使用字典或数组。假设放到数组或者字典中的所有对象都是前面列出的可序列化对象,则可以通过对字典或者数组实例调用writeToFile:atomically:方法来写入属性列表,如下:

[myArray writeToFile:@"some/file/location/output.plist" atomically:YES];

注意:atomically参数让该方法将数据写入辅助文件,而不是写入指定位置。成功写入该文件后,辅助文件将被复制到第一个参数指定的位置。这是更安全的写入文件的方法,因为如果应用在保存期间崩溃,则现有文件不会被破坏。尽管这增加了一点开销,但是多数情况下还是值得的。

  • 属性列表方法的一个问题是无法将自定义对象序列化到属性列表中,另外也不能使用没有在可以序列化对象类型列表中指定的Cocoa Touch的其他类。这意味着无法直接使用NSURL、UIImage和UIColor等类。

  • 且不说序列化问题,将这些模型对象保存到属性列表中还意味着你无法轻松创建派生的或需要计算的属性(例如,等于两个属性之和的属性),并且必须将实际上应该包含在模型类中的某些代码移动到控制器类。这些限制也适用于简单的数据模型和简单应用。但在多数情况下,如果创建了专用的模型类,则应用更容易维护。

  • 在复杂的应用中,简单的属性列表仍然非常有用。它们是将静态数据包含在应用中的最佳方法。例如,当应用包含一个选取器时,创建一个属性列表文件并将其放在项目的Resources文件夹中,就是将项目列表包含到选取器中的最佳方法,这样就能把项目列表编译到应用中。



3.对模型对象进行归档

  • 在Cocoa世界中,归档是指另一种形式的序列化,但它是任何对象都可以实现的更常规的类型。专门编写用于保存数据的任何模型对象都应该支持归档。使用对模型对象进行归档的技术可以轻松将复杂的对象写入文件,然后读取它们。
  • 只要在类中实现的每个属性都是标量(如整型和浮点型)或都是遵循NSCoding协议的某个类的实例,你就可以将整个对象进行完全的归档。由于大多数支持存储数据的Foundation和Cocoa Touch类都遵循NSCoding协议(不过有一些例外,如UIImage),对于大多数类来说,归档相对而言比较容易实现。
  • 尽管对归档的使用没有严格要求,但还有一个协议应该与NSCoding一起实现,即NSCopying协议。后者允许赋值对象,这使在使用数据模型对象时具备了较大的灵活性。

1.遵循NSCoding协议

NSCoding协议声明了两个方法,这两个方法都是必需的。一个方法将对象编码到归档中,另一个方法对归档解码来创建一个新对象。这两个方法都传递一个NSCoder实例,使用方式与NSUserDefaults非常相似。也可以使用KVC对对象和原生数据模型(如整型和浮点型)进行编码和解码。

  • 在OC中,必须使用正确的编码方法将所有实例变量编码为encoder。如果要子类化某个也遵循NSCoding的类,还需要确保对其父类调用encodeWithCoder:方法:
- (void)encodeWithCoder:(NSCoder *)encoder {
  [super encodeWithCoder:encoder]; // 让父类对状态进行编码
  [encoder encodeObject:foo forKey:kFooKey];
  [encoder encodeObject:bar forKey:kBarKey];
  [encoder encodeInt:someInt forKey:kSomeIntKey];
  [encoder encodeFloat:someFloat forKey:kSomeFloatKey];
}
  • 还需要实现一个通过NSCoder初始化对象的初始化方法,恢复之前归档的对象。
  • OC中实现initWithCoder:方法比实现encodeWithCoder:方法稍微复杂一些。如果直接对NSObject进行子类化,或者对某些不遵循NSCoding的其他类进行子类化,如下:
- (id)initWithCoder:(NSCoder *)decoder {

  if (self = [super init]) {

    foo = [decoder decodeObjectForKey:kFooKey];
    bar = [decoder decodeObjectForKey:kBarKey];
    someInt = [decoder decodeIntForKey:kSomeIntKey];
    someFloat = [decoder decodeFloatForKey:kSomeFloatKey];
  }
  return self;
}
  • 该方法使用[super init]初始化对象实例。如果初始化成功,则它通过解码NSCoder的实例中传递的值来设置其属性。当为某个具有父类遵循NSCoding的类实现NSCoding时,initWithCoder:方法稍有不同。它不再对super调用init,而是调用initWithCoder:,如:
- (id)initWithCoder:(NSCoder *)decoder {

  if (self = [super initWithCoder:decoder]) {

    foo = [decoder decodeObjectForKey:kFooKey];
    bar = [decoder decodeObjectForKey:kBarKey];
    someInt = [decoder decodeIntForKey:kSomeIntKey];
    someFloat = [decoder decodeFloatForKey:kSomeFloatKey];
  }
  return self;
}

2. 实现NSCopying协议

  • NSCopying有一个copyWithZone方法,可用来复制对象。实现NSCopying与实现initWithCoder非常相似,只需创建一个同一类的新实例,然后将实例的所有属性都设置为与该对象属性相同的值即可。
- (id) copyWithZone:(NSZone *)zone {

  MyClass *copy = [[[self class] allocWithZone:zone] init];
  copy.foo = [self.foo copyWithZone:zone];
  copy.bar = [self.bar copyWithZone:zone];
  copy.someInt = self.someInt;
  copy.someFloat = self.someFloat;

  return copy;
}
  • 不要过于担心NSZone参数。它指向系统用于管理内存的struct。只有在极少数情况下,才需要关注zone或者自己创建的zone。对某个对象调用copy的方法与使用默认zone调用copyWithZone的方法完全相同,几乎始终能满足需求。事实上,现在的iOS完全可以忽略zone。NSCopying用到zone在本质上是考虑向后兼容性所致。

3. 对数据对象进行归档和取消归档

  • 从遵循NSCoding的一个或者多个对象创建归档相对比较容易。首先创建一个NSMutableData实例,用于包含编码的数据,然后创建一个NSKeyedArchiver实例,用于将对象归档到此NSMutableData实例中:
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
  • 创建这两个实例后,使用key/value来对希望包含在归档中的所有对象进行归档:
[archiver encodeObject:myObject forKey:@"keyValueString"];
  • 对所有要包含的对象进行编码之后,只需告知归档程序已经完成了这些操作,并将NSMutableData实例写入文件系统:
[archiver finishEncoding];
BOOL success = [data writeToFile:@"/path/to/archive" atomically:YES];
  • 写入文件时出现错误会将success设置为false或NO。如果success为true或YES,则数据已成功写入指定文件。从该归档创建的任何对象与之前写入该文件的对象一致。
  • 有一个快速方法可以获取同样的内容:使用NSKeyedArchiver的archiveDataWithRootObject方法可以分配一个NSData对象并一次性将对象编码进去,然后返回NSData对象。
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object];
BOOL success = [data writeToFile:@"path/to/archive" atomically:YES];
  • 也可以使用archiveRootObject:toFile:方法直接从对象归档数据到文件:
BOOL success = [NSKeyedArchiver archiveRootObject:object toFile:@"/path/to/archive"];
  • 从归档重组对象的步骤与上面相似。从归档文件创建一个NSData实例,并创建一个NSKeyedUnarchiver对数据进行解码:
NSData *data = [[NSData alloc] initWithContentsOfFile:@"/path/to/archive"];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
  • 然后,使用之前对对象进行归档的同一个key从解压程序中读取对象:
self.object = [unarchiver decodeObjectForKey:@"keyValueString"];
  • 最后,告知归档程序已经完成了该操作:
[unarchiver finishDecoding];


4. 使用iOS内嵌的SQLite3

1. 创建或打开数据库

使用SQLite3之前,必须打开数据库。用于执行此操作的命令是sqlite3_open,这样将打开一个现有数据库。如果指定位置上不存在数据库,则函数会创建一个新的数据库。下面是打开数据库的代码:

sqlite3 *database;
int result = sqlite3_open("/path/to/database/file", &database);

如果result等于常量SQLITE_OK,就表示数据库已成功打开。需要注意的是,数据库文件的路径必须以C字符串的形式进行传递。SQLite3是采用可移植的C(而非OC)编写的,它不知道什么是NSString。所幸,有一个NSString方法能从NSString实例生成C字符串:

const char *stringPath = [pathString UTF8String];

对SQLite3数据库执行完所有操作后,调用以下内容来关闭数据库:

sqlite3_close(database);

数据库将所有数据存储在表中。可以通过SQL的CREATE语句创建一个新表,并使用函数sqlite3_exec将其传递到打开的数据库,如下:

char *errorMsg;
const char *createSQL = "CREATE TABLE IF NOT EXISTS PEOPLE"
  "(ID INTEGER PRIMARY KEY AUTOINCREMENT , FIELD_DATA TEXT)";
int result= sqlite3_exec(database, createSQL, NULL, NULL, &errorMsg);

提示: 在OC中,如果两个字符串之间除了空白(包括换行符)之外没有其他的分隔字符,那么这两个字符串会被连接为一个字符串。

如之前所做的一样,需要检查result是否等于SQLITE_OK以确保命令成功运行。如果命令未成功运行,errorMsg或errMsg将对所发生的问题进行描述。

函数sqlite3_exec针对SQLite3运行任何不返回数据的命令。它用于执行更新、插入和删除操作。从数据库中检索数据有点复杂,必须首先向其输入SQL的SELECT命令来准备该语句:

NSString *query = @"SELECT ID, FIELD_DATA FROM FIELDS ORDER BY ROW";
sqlite3_stmt *statement;
int result = sqlite3_prepare_v2(database, [query UTF8String], -1, &statement, nil);

注意: OC中所有接受字符串的SQLite3函数都要求使用旧样式的C字符串。在实例中,可以创建并传递一个C字符串,也可以创建一个NSString并通过它的方法(名为UTF8String)派生一个C字符串。这两个方法都行。如果需要操作字符串,则使用NSString或NSMutableString比较容易,但将NSString转换为C字符串会导致一些额外开销。

如果result等于SQLITE_OK,则语句准备成功,可以开始遍历结果集。

遍历结果集并从数据库中检索整型和字符串:

while (sqlite3_step(statement) == SQLITE_ROW) {
  int rowNum = sqlite3_column_int(statement, 0);
  char *rowData = (char *)sqlite3_column_text(statement, 1);
  NSString *fieldValue = [[NSString alloc] initWithUTF8String:rowData];
  // 这里写对数据进行处理的代码
}
sqlite3_finalize(statement);

2. 绑定变量

虽然可以通过创建SQL字符串来插入值,但常用的方法是使用绑定变量来执行数据库插入操作。正确处理字符串并确保它们没有无效字符(以及引号处理过的属性)是非常繁琐的事情。借助绑定变量,这些问题迎刃而解。

要使用绑定变量插入值,只需按正常方式创建SQL语句即可,不过要在SQL字符串中添加一个问号。每个问号都表示一个需要在语句执行之前进行绑定的变量。然后,准备好SQL语句,将值绑定到各个变量并执行命令。

下面这个实例使用两个绑定变量预处理SQL语句。它将整形数绑定到第一个变量,将字符串绑定到第二个变量,然后执行结束语句:

char *sql = "insert into foo values(?, ?);";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, sql, -1, &stmt, nil) == SQLITE_OK) {
  sqlite3_bind_int(stmt, 1, 235);
  sqlite3_bind_text(stmt, 2, "bar", -1, NULL);
}
if (sqlite3_step(stmt) != SQLITE_DONE)
  NSLog(@"This should be real error checking");
sqlite3_finalize(stmt);

根据希望使用的数据类型,可以选择不同的绑定语句。大部分绑定函数都有3个参数。

  • 无论针对哪种数据类型,任何绑定函数的第一个参数都指向之前在sqlite3_prepare_v2()调用中使用的sqlite3_stmt。

  • 第二个参数是被绑定变量的索引。它是一个有序索引的值,这表示SQL语句中的第一个问好是索引1,其后的每个问号都依次按序增加1.

  • 第三个参数始终表示应该替换问号的值。有些绑定函数(比如用于绑定文本和二进制数据的绑定函数)拥有另外两个参数。

  • 一个参数是在上面第三个参数中传递的数据长度。对于C字符串,可以传递-1来代替字符串的长度,这样函数将使用整个字符串。对于所有其他情况,需要指定所传递数据的长度。

  • 另一个参数是可选的函数回调,用于在语句执行后完成内存清理工作。通常,这种函数使用malloc()释放已分配的内存。

绑定语句后面的语法看起来可能有点奇怪,因为我们执行了一个插入操作。当使用绑定变量时,会将相同语法同时用于查询和更新。如果SQL字符串包含一个SQL查询(而不是更新),就需要多次调用sqlite3_step(),直到它返回SQLITE_DONE。疑问这里是更新,所以仅调用一次。

3. SQLite3 的应用

1. 链接到SQLite3库

通过一个过程API来访问SQLite3,该API提供对很多C函数调用的接口。要使用该API,需要将应用链接到一个名为libsqlite3.tbd的动态库。

2. 实现代码
#import "ViewController.h"
#import <sqlite3.h>

@interface ViewController ()

@property (nonatomic, strong)IBOutletCollection(UITextField) NSArray *lineFields; // 四个UITextField

@end

@implementation ViewController

- (NSString *)dataFilePath {
    
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    return [documentsDirectory stringByAppendingString:@"data.sqlite"];
}

- (void)viewDidLoad {
    [super viewDidLoad];
   
    sqlite3 *database;
    if (sqlite3_open([[self dataFilePath] UTF8String], &database) != SQLITE_OK) {
        
        sqlite3_close(database);
        NSAssert(0, @"Failed to open database");
    }
    
    NSString *createSQL = @"CREATE TABLE IF NOT EXISTS FIELDS"
                        "(ROW INTEGER PRIMARK KEY, FIELD_DATA TEXT);";
    char *errorMsg;
    if (sqlite3_exec(database, [createSQL UTF8String], NULL, NULL, &errorMsg)) {
        sqlite3_close(database);
        NSAssert(0, @"Error creating table: %s", errorMsg);
    }
    
    NSString *query = @"SELECT ROW, FIELD_DATA FROM FIELDS ORDER BY ROW";
    sqlite3_stmt *statement;
    if (sqlite3_prepare_v2(database, [query UTF8String], -1, &statement, nil) == SQLITE_OK) {
        while (sqlite3_step(statement) == SQLITE_ROW) {
            int row = sqlite3_column_int(statement, 0);
            char *rowData = (char *)sqlite3_column_text(statement, 1);
            NSString *fieldValue = [[NSString alloc] initWithUTF8String:rowData];
            UITextField *field = self.lineFields[row];
            field.text = fieldValue;
        }
        sqlite3_finalize(statement);
    }
    sqlite3_close(database);
    
    UIApplication *app = [UIApplication sharedApplication];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:app];
}

- (void)applicationWillResignActive:(NSNotification *)notification {
    
    sqlite3 *database;
    if (sqlite3_open([[self dataFilePath] UTF8String], &database) != SQLITE_OK) {
        sqlite3_close(database);
        NSAssert(0, @"Failed to open database");
    }
    
    for (int i = 0; i < 4; i++) {
        UITextField *field = self.lineFields[i];
        
        char *update = "INSERT OR REPLACE INTO FIELDS (ROW, FIELD_DATA)"
        "VALUES (?, ?);";
        char *errorMsg = NULL;
        sqlite3_stmt *stmt;
        if (sqlite3_prepare_v2(database, update, -1, &stmt, nil) == SQLITE_OK) {
            sqlite3_bind_int(stmt, 1, i);
            sqlite3_bind_text(stmt, 2, [field.text UTF8String], -1, NULL);
        }
        
        if (sqlite3_step(stmt) != SQLITE_DONE) {
            NSAssert(0, @"Error updating table: %s", errorMsg);
        }
        sqlite3_finalize(stmt);
    }
    sqlite3_close(database);
}

@end

首先我们打开数据库,如果打开时遇到了问题,则关闭它并抛出一个断言错误(或者打印一段错误信息并中断程序):

sqlite3 *database;
    if (sqlite3_open([[self dataFilePath] UTF8String], &database) != SQLITE_OK) {
        
        sqlite3_close(database);
        NSAssert(0, @"Failed to open database");
    }

接下来,需要确保有一个表来保存我们的数据。可以使用CREATE TABLE完成此任务。通过指定IF NOT EXISTS,可以防止数据库覆盖现有数据。如果已有一个具有相同名称的表,此命令不会执行任何操作,所以可以在应用每次启动时安全的调用它,不需要显式的检查表是否存在:

 NSString *createSQL = @"CREATE TABLE IF NOT EXISTS FIELDS"
                        "(ROW INTEGER PRIMARK KEY, FIELD_DATA TEXT);";
    char *errorMsg;
    if (sqlite3_exec(database, [createSQL UTF8String], NULL, NULL, &errorMsg)) {
        sqlite3_close(database);
        NSAssert(0, @"Error creating table: %s", errorMsg);
    }

数据库中的每一行包含一个整型和一个字符串。整型指出图形界面中得到的是哪一行的数字(从0开始计数)。而字符串是这一行中文本框的内容。最后,需要加载数据。为此,使用SELECT语句。在这个例子中,创建一个SELECT从数据库请求所有行并要求SQLite3准备的SELECT。要告诉SQLite3按行号顺序排序各行,以便总是以相同顺序获取它们。否则,SQLite3将按内部存储数据返回各行:

    NSString *query = @"SELECT ROW, FIELD_DATA FROM FIELDS ORDER BY ROW";
    sqlite3_stmt *statement;
    if (sqlite3_prepare_v2(database, [query UTF8String], -1, &statement, nil) == SQLITE_OK) {

然后遍历返回的每一行:

while (sqlite3_step(statement) == SQLITE_ROW) {

抓取行号并将它存储在一个int变量中,然后抓取字段数据保存到C语言字符串中:

int row = sqlite3_column_int(statement, 0);
char *rowData = (char *)sqlite3_column_text(statement, 1);

接下来,利用从数据库获取的值设置相应的字段:

NSString *fieldValue = [[NSString alloc] initWithUTF8String:rowData];
UITextField *field = self.lineFields[row];
field.text = fieldValue;

最后关闭数据库连接,所有操作到此结束:

         }
            sqlite3_finalize(statement);
     }
    sqlite3_close(database);

注意,示例在创建表和加载它所包含的所有数据后立即关闭了数据库连接,而不是在应用运行的整个过程中保持打开状态。这是管理连接最简单的方式,对于这个小示例,可以在需要连接时再打开它。在其他需要频繁使用数据库的应用中,可能有必要始终打开连接。

其他更改是在applicationWillResignActive方法中进行的,我们需要把应用数据保存在这里。

applicationWillResignActive方法首次会再次打开数据库。然后保存数据,在4个字段中进行循环,生成4条独立的命令来更新数据库中的每一行:

for (int i = 0; i < 4; i++) {
        UITextField *field = self.lineFields[i];

示例设计了一条带有两个绑定变量的INSERT OR REPLACE语句。第一个变量代表所存储的行,第二个变量代表要存储的实际字符串值。使用INSERT OR REPLACE,而不是更标准的INSERT,就不需要担心某个行是否已经存在:

        char *update = "INSERT OR REPLACE INTO FIELDS (ROW, FIELD_DATA)"
        "VALUES (?, ?);";

接下来声明一个指向语句的指针,然后为语句添加绑定变量,并将值绑定到两个绑定变量:

       sqlite3_stmt *stmt;
       if (sqlite3_prepare_v2(database, update, -1, &stmt, nil) == SQLITE_OK) {
           sqlite3_bind_int(stmt, 1, i);
           sqlite3_bind_text(stmt, 2, [field.text UTF8String], -1, NULL);
       }

然后调用sqlite3_step来执行更新,检查并确定其运行正常,然后完成语句,结束循环:

          if (sqlite3_step(stmt) != SQLITE_DONE) {
            NSAssert(0, @"Error updating table: %s", errorMsg);
        }
        sqlite3_finalize(stmt);

注意,OC代码中使用了一个断言来检查错误条件。之所以会使用断言,而不使用异常或手动错误检查,是因为这种情况只有在开发人员出错的情况下才会出现。使用此断言宏将有助于我们调试代码,并且可以脱离最终的应用。

注意: 有一个条件可能导致前面的SQLite代码出现错误,但不是程序员错误。如果设备的存储区已满,SQLite无法将其更改保存到数据库,那么这里也会发生错误。但是,这种情况很少见,并可能为用户带来更深层次的问题,不过这已经超出了应用数据的范围。如果系统处于这一状态,我们的应用甚至可能无法成功启动。因此可以不用考虑这个问题。

完成循环之后,关闭数据库:

sqlite3_close(database);

相关文章

  • iOS Realm数据持久化--Realm集合分页(四)

    iOS Realm数据持久化--Realm基础知识 (一)iOS Realm数据持久化--数据分页与复用原理 (二...

  • iOS本地数据持久化

    iOS本地数据持久化 iOS本地数据持久化

  • iOS 数据持久化基础知识小总结

    1. 应用的沙盒 注意:如果要为应用程序启用分享功能,需要打开它的info.plist文件并添加键为Applica...

  • iOS本地数据持久化

    转载自:CocoaChina - iOS本地数据持久化 本文内容:iOS本地数据持久化的几种类型iOS本地数据持久...

  • iOS 数据持久化方案-Realm的使用

    iOS 数据持久化方案-Realm的使用 iOS 数据持久化方案-Realm的使用

  • iOS本地数据持久化

    本文内容: iOS本地数据持久化的几种类型 iOS本地数据持久化几种类型的应用场景及使用 一.iOS本地数据持久化...

  • iOS 数据持久化

    iOS本地数据持久化 本文内容:iOS本地数据持久化的几种类型iOS本地数据持久化几种类型的应用场景及使用 一.i...

  • iOS本地数据持久化

    本文内容:iOS本地数据持久化的几种类型iOS本地数据持久化几种类型的应用场景及使用 一,iOS本地数据持久化的类...

  • ios数据持久化

    本文内容:iOS本地数据持久化的几种类型iOS本地数据持久化几种类型的应用场景及使用 一.iOS本地数据持久化的类...

  • iOS | 面试- 数据持久化

    iOS | 面试知识整理 - 数据持久化(八) 1. iOS中数据持久化方案有哪些? NSUserDefault简...

网友评论

    本文标题:iOS 数据持久化基础知识小总结

    本文链接:https://www.haomeiwen.com/subject/ujchkftx.html