美文网首页iOS
iOS 本地存储数据的几种方式

iOS 本地存储数据的几种方式

作者: 最强的小强 | 来源:发表于2019-03-12 17:39 被阅读0次

    写在前面:iOS本地持久化存储的路径
    Documents: 最常用的目录,存放重要的数据,iTunes同步时会备份该目录
    Library/Caches: 一般存放体积大,不重要的数据,iTunes同步时不会备份该目录
    Library/Preferences: 存放用户的偏好设置,iTunes同步时会备份该目录
    tmp: 用于存放临时文件,在程序未运行时可能会删除该文件夹中的数据,iTunes同步时不会备份该目录
    存储方式:NSUserDefaultsPlistNSKeyedArchiverSQLite3Core DataKeychainFMDB


    一、NSUserDefaults 方式存储

    1.1 写入

     NSUserDefaults *login = [NSUserDefaults standardUserDefaults];
     [login setObject:self.passwordField.text forKey:@"token"];
     [login synchronize];
    

    1.2 读取

     NSUserDefaults *login = [NSUserDefaults standardUserDefaults];
     NSString *str = [login objectForKey:@"token"];
    

    \color{#f5576c}{写在后面:}
    1.只能存储OC常用数据类型(NSString、NSDictionary、NSArray、NSData、NSNumber等类型)而不能直接存储自定义数据。
    2.键值对存储,直接指定存储类型。


    二、plist 方式存储

    2.1 写入

    #pragma mark - 保存到本地
    - (void)saveToLocale {
        [self.view endEditing:YES];
        if (!self.nameField.text.length) {
            [TipUtils showToast:self.view message:@"应用名称不能为空"];
            return;
        }
        NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0]stringByAppendingPathComponent:@"password.plist"];
        NSFileManager *fileManager = [NSFileManager defaultManager];
        BOOL Exists = [fileManager fileExistsAtPath:path];
        if (!Exists) {
            _serectArray = [[NSMutableArray alloc] init];
        } else {
            _serectArray = [[NSMutableArray alloc] initWithContentsOfFile:path];
        }
        // 这里是修改更新数据
        for (int i=0;i <self.serectArray.count;i++) {
            if ([self.myPassword.timeID isEqualToString:self.serectArray[i][@"timeID"]]) {
                self.serectArray[i][@"name"] = self.nameField.text;
                [self.serectArray writeToFile:path atomically:YES];
                if ([self.serectArray writeToFile:path atomically:YES]) {
                    [TipUtils showToast:self.view message:@"保存成功"];
                    [self  performSelector:@selector(delayMethod) withObject:nil afterDelay:1.0f];
                } else {
                    NSLog(@"保存失败");
                }
                return;
            }
        }
        NSDictionary *dict = [NSDictionary dictionary];
        dict = @{@"timeID":[self getItemID], @"name":self.nameField.text};
        [self.serectArray addObject:dict];
        [self.serectArray writeToFile:path atomically:YES];
        if ([self.serectArray writeToFile:path atomically:YES]) {
            [TipUtils showToast:self.view message:@"保存成功"];
            [self  performSelector:@selector(delayMethod) withObject:nil afterDelay:1.0f];
            NSLog(@"存储的数据有:%@",self.serectArray);
        } else {
            NSLog(@"保存失败");
        }
    }
    
    #pragma mark - 延时函数
    - (void)delayMethod {
        [self.navigationController popViewControllerAnimated:YES];
    }
    

    2.2 读取

    #pragma mark - 读取数据
    - (void)readDataFromPlist {
        NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0]stringByAppendingPathComponent:@"password.plist"];
    NSLog(@"存储路径-%@",path);
        NSFileManager *fileManager = [NSFileManager defaultManager];
        BOOL Exists = [fileManager fileExistsAtPath:path];
        if (!Exists) {
            _serectArray = [[NSMutableArray alloc] init];
        } else {
            _serectArray = [[NSMutableArray alloc] initWithContentsOfFile:path];
        }
    }
    

    \color{#f5576c}{写在后面:}
    1.只能存储OC常用数据类型(NSString、NSDictionary、NSArray、NSData、NSNumber等类型)而不能直接存储自定义模型对象。
    · 如果想用对象,只能再用明杰的框架进行字典转模型了。
    · 或者想存储自定义模型对象 -> 只能将自定义模型对象转换为字典存储;
    2.系统的plist文件,只能读取,不行写入,自定义的可读可写
    3.plist文件存储的位置,但一般存在Documents中
    4.如果存储图片路径的话,一定要存储相对位置,因为每次启动APP,plist文件的路径就会变化,自然图片的位置也就变化了。


    三、NSKeyedArchiver归档(NSCoding)
    • 归档解档最大的好处在于可以存储自定义对象数据
    • 归档解档要注意ios版本
    1. 新建一个数据模型类Person
    #import <Foundation/Foundation.h>
    @interface Person : NSObject<NSCoding>
    @property(nonatomic, copy) NSString *name;
    @end
    
    #import "Person.h"
    @implementation Person
    -(id)init {
        if (self == nil) {
            self = [super init];
        }
        return self;
    }
    - (void)encodeWithCoder:(nonnull NSCoder *)aCoder {
        [aCoder encodeObject:self.name forKey:@"name"];
    }
    - (nullable instancetype)initWithCoder:(nonnull NSCoder *)aDecoder {
        if (self = [super init]) {
            self.name = [aDecoder decodeObjectForKey:@"name"];
        }
        return self;
    }
    @end
    
    1. 新建一个归档解档通用工具类ArchiveTools
    #import <Foundation/Foundation.h>
    @interface ArchiveTools : NSObject
    + (BOOL)archiveObject:(id)object prefix:(NSString *)prefix;
    + (id)unarchiveClass:(Class)class prefix:(NSString *)prefix;
    + (NSString *)getPathWithPrefix: (NSString *)prefix;
    @end
    
    #import "ArchiveTools.h"
    
    @implementation ArchiveTools
    #pragma mark - 归档解档
    + (BOOL)archiveObject:(id)object prefix:(NSString *)prefix {
        if (!object) {
            return NO;
        }
        NSError *error;
        if (@available(iOS 11.0, *)) {
            NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object
                                                 requiringSecureCoding:YES
                                                                 error:&error];
            if (error)
                return NO;
            [data writeToFile:[self getPathWithPrefix:prefix] atomically:YES];
         } else {
             NSData*data = [NSKeyedArchiver archivedDataWithRootObject:object];
             [data writeToFile:[self getPathWithPrefix:prefix] atomically:YES];
         }
        return YES;
    }
    + (id)unarchiveClass:(Class)class prefix:(NSString *)prefix {
        
        NSError *error;
        NSData *data = [[NSData alloc] initWithContentsOfFile:[self getPathWithPrefix:prefix]];
        //会调用对象的initWithCoder方法
        if (@available(iOS 11.0, *)) {
            id content = [NSKeyedUnarchiver unarchivedObjectOfClass:class fromData:data error:&error];
            if (error) {
                return nil;
            }
            return content;
        } else {
            id content = [NSKeyedUnarchiver unarchiveObjectWithData:data];
            return content;
        }
    }
    #pragma mark - 存放文件的路径
    + (NSString *)getPathWithPrefix: (NSString *)prefix {
        // document路径
        NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0];
        // 自定义一个文件夹
        NSString *filePathFolder = [documentPath stringsByAppendingPaths:@[@"archiveTemp"]].firstObject;
        if (![[NSFileManager defaultManager] fileExistsAtPath:filePathFolder]) {
            [[NSFileManager defaultManager] createDirectoryAtPath:filePathFolder withIntermediateDirectories:YES attributes:nil error:nil];
        }
        NSString *path = [NSString stringWithFormat:@"%@/%@.archive",filePathFolder,prefix];
        NSLog(@"%@",path);
        return path;
    }
    

    3.调用使用

    #import "ViewController.h"
    #import "Person.h"
    #import "ArchiveTools.h"
    @interface ViewController ()
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        Person *person = [[Person alloc] init];
        person.name = @"I am Lili";
        // 存储
        BOOL isSuccess = [ArchiveTools archiveObject:person prefix:NSStringFromClass(person.class)];
        NSLog(@"啦啦-%@",NSStringFromClass(person.class));
        if (isSuccess) {
            NSLog(@"存储成功");
        } else {
            NSLog(@"存储失败");
        }
        // 读取
        Person  *content = [ArchiveTools unarchiveClass:Person.class prefix:NSStringFromClass(Person.class)];
        NSLog(@"内容是-%@",content);
        NSLog(@"具体-%@", content.name);
        //删除归档文件
        NSFileManager *defaultManager = [NSFileManager defaultManager];
        if ([defaultManager isDeletableFileAtPath:[ArchiveTools getPathWithPrefix:NSStringFromClass(Person.class)]]) {
            [defaultManager removeItemAtPath:[ArchiveTools getPathWithPrefix:NSStringFromClass(Person.class)] error:nil];
        }
    }
    

    四、SQLite3

    1. 在项目中导入libsqlite3.0.tbd框架
    2.使用数据库

    #import "ViewController.h"
    #import <sqlite3.h>
    @interface ViewController ()
    @property(nonatomic, assign) int res;
    
    @end
    
    @implementation ViewController
    {
        sqlite3 *db; // 声明对象
    }
    - (void)viewDidLoad {
        [super viewDidLoad];
        [self createDB];
        [self createTable];
        [self insertData];
        [self selectData];
        // 关闭数据库
        sqlite3_close(db);
    }
    #pragma mark - 数据库相关操作
    - (void)createDB {
        // 创建数据库路径
        NSString *dbPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0]stringByAppendingPathComponent:@"test.sqlite"];
        NSLog(@"数据库路径-%@",dbPath);
        // 创建或者打开数据库
        const char *p = [dbPath UTF8String];
        int res = sqlite3_open(p, &db);
        self.res = res;
    }
    // 一、创建表格
    - (void)createTable {
        if(self.res == SQLITE_OK) {
            NSLog(@"数据库成功打开");
            NSString *sql = @"create table if not exists temps (t_id integer primary key autoincrement, t_name varchar(20))";
            if ([self execNoQueryWithSQL:sql]) {
                NSLog(@"创建表格成功");
            } else {
                NSLog(@"创建表格失败");
            }
        } else {
            NSLog(@"数据库未正常打开-%d",self.res);
        }
    }
    // 二、插入数据
    - (void)insertData {
        if (self.res == SQLITE_OK) {
            NSString *insert_sql = @"insert into temps (t_name) values('banala')";
            if ([self execNoQueryWithSQL:insert_sql]) {
                NSLog(@"成功插入数据");
            } else {
                NSLog(@"插入数据失败");
            }
        } else {
            NSLog(@"数据库未正常打开-%d",self.res);
        }
    }
    // 三、删除数据
    - (void)deleteData {
        if (self.res == SQLITE_OK) {
            NSString *delete_sql = @"delete from temps where t_id=2";
            if ([self execNoQueryWithSQL:delete_sql]) {
                NSLog(@"成功删除数据");
            } else {
                NSLog(@"删除数据失败");
            }
        } else {
            NSLog(@"数据库未正常打开-%d",self.res);
        }
    }
    // 四、修改数据
    - (void)updataData {
        if (self.res == SQLITE_OK) {
            NSString *update_sql = @"update temps set t_name='ios' where t_id=1";
            if ([self execNoQueryWithSQL:update_sql]) {
                NSLog(@"成功更新数据");
            } else {
                NSLog(@"更新数据失败");
            }
        } else {
            NSLog(@"数据库未正常打开-%d",self.res);
        }
    }
    // 五、查询简单数据,无参数
    - (void)selectData {
        if (self.res == SQLITE_OK) {
    //        NSString *select_sql1 = @"select * from temps where t_id=1";
           NSString *select_sql1 = @"select * from temps";
            sqlite3_stmt *stmt1 = [self execQueryWithSQL:select_sql1];
            while (sqlite3_step(stmt1) == SQLITE_ROW) {
                int t_id = sqlite3_column_int(stmt1, 0);
                const unsigned char *t_name = sqlite3_column_text(stmt1, 1);
                NSString *name = [NSString stringWithUTF8String:(char *)t_name];
                NSLog(@"%i %@",t_id,name);
            }
            // 释放stmt statement
            sqlite3_finalize(stmt1);
        } else {
            NSLog(@"数据库未正常打开-%d",self.res);
        }
    }
    // 六、查询数据2 参数化的sql语句  查找 id>2 并且名字以p开头的 用 ?占位
    - (void)selectDataWithParams {
        if (self.res == SQLITE_OK) {
            int seachId2 = 1;
            NSString *seach_name = @"p%";
            NSString *seach_sql = @"select * from temps where t_id>? and t_name like ?";
            sqlite3_stmt *stmt6 = [self execQueryWithSQL:seach_sql andWithParams:@[[NSNumber numberWithInt:seachId2],seach_name]];
            
            //准备执行(相当于点击run query),执行的时候是一行一行的执行
            while (sqlite3_step(stmt6) == SQLITE_ROW) {
                //按照当前列的类型选数据,列数从0开始
                int t_id = sqlite3_column_int(stmt6, 0);
                const unsigned char *t_name = sqlite3_column_text(stmt6, 1);
                NSString *name = [NSString stringWithUTF8String:(char*)t_name];
                NSLog(@"..>>>>>>...%i %@",t_id,name);
            }
            sqlite3_finalize(stmt6);
        } else {
            NSLog(@"数据库未正常打开-%d",self.res);
        }
    }
    
    #pragma mark - 执行除查询以外的操作
    - (BOOL)execNoQueryWithSQL:(NSString *)sql {
        /*
         执行
         参数1:sqlite3 对象
         参数2:c形式的 sql语句
         参数3:回调函数
         参数4:回调函数的参数
         参数5:错误信息(可以char类型指针接受错误信息,用来查错使用)
         */
        char *error;
        int result =  sqlite3_exec(db, [sql UTF8String], NULL, NULL, &error);
        if (result == SQLITE_OK) {
            return YES;
        } else {
            NSLog(@"%s",error);
        }
        return NO;
    }
    #pragma mark - 返回查询结果集,无参数
    -(sqlite3_stmt *)execQueryWithSQL:(NSString *)sql {
        sqlite3_stmt *stmt;
        /*
         准备执行查询的sql语句 (相当于把查询语句写好)
         参数3:sql语句长度,通常用-1表示(系统会自动计算),也可以用strlength函数计算
         参数4:sql_stmt对象 (执行的对象)
         参数5:未执行的sql语句
         */
        int pre_res = sqlite3_prepare_v2(db, [sql UTF8String], -1, &stmt, NULL);
        if (pre_res == SQLITE_OK) {
            return stmt;
        }
        return NULL;
    }
    #pragma mark - 返回查询结果集m,有参数
    -(sqlite3_stmt *)execQueryWithSQL:(NSString *)sql andWithParams:(NSArray *)params{
        sqlite3_stmt *stmt;
        int pre_res = sqlite3_prepare_v2(db, [sql UTF8String], -1, &stmt, NULL);
        if (pre_res == SQLITE_OK) {
            if (params!=nil) {
                for (int i = 0 ; i<params.count; i++) {
                    id obj = params[i];
                    //绑定的数据类型可能为NSString或者NSNumber,或者数据为空,分别判断
                    if (obj == nil) {
                        // 数据为空
                        sqlite3_bind_null(stmt, i+1);
                    } else if ([obj respondsToSelector:@selector(objCType)]) {
                        //当前的绑定的数据类型位NSNumber
                        //NSNumber判断包装的是int?longInt?shortInt?float?double?
                        /*
                         strstr(参数1,参数2) (strstr() c中函数搜索一个字符串在另一个字符串中的第一次出现,则该函数返回第一次匹配的字符串的地址,找不到返回NULL)
                         判断参数1中的字符在参数2的字符串char*中出现的索引
                         [obj objCType] 如果obj是int返回字符串i
                         */
                        if (strstr("ilsILS", [obj objCType])) {
                            /*
                             绑定参数 如果有where
                             参数1:sqlite_stmt对象 (statement结果集)
                             参数2:占位符索引 从1开始
                             参数3:替代占位符的真实参数
                             */
                            sqlite3_bind_int(stmt, i+1, [obj intValue]);
                        } else if (strstr("fdFD", [obj objCType])){
                            sqlite3_bind_double(stmt, i+1, [obj doubleValue]);
                        } else {
                            stmt = nil;
                        }
                    } else if ([obj respondsToSelector:@selector(UTF8String)]) {
                        //当前的绑定的数据类型为NSString 判断是否有UTF8String方法
                        //用bind替换占位符 索引从1开始
                        sqlite3_bind_text(stmt, i+1, [obj UTF8String], -1, NULL);
                    } else {
                        stmt = nil;
                    }
                }
            }
            return stmt;
        }
        return NULL;
    }
    @end
    
    

    五、Core Data

    Core Data是iOS5之后才出现的一个框架,提供了直接使用SQLite数据库的大部分灵活性,它提供了对象-关系映射(ORM)的功能,即能够将OC对象转化成数据,保存在SQLite数据库文件中,也能够将保存在数据库中的数据还原成OC对象,通过CoreData管理应用程序的数据模型,可以极大程度减少需要编写的代码数量!

    • 废话不多说,直接上代码!
    //创建数据库
    - (void)createSqlite{
        
        //1、创建模型对象
        //获取模型路径
        NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"CoreData__" withExtension:@"momd"];
        //根据模型文件创建模型对象
        NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
        
        
        //2、创建持久化存储助理:数据库
        //利用模型对象创建助理对象
        NSPersistentStoreCoordinator *store = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
        
        //数据库的名称和路径
        NSString *docStr = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
        NSString *sqlPath = [docStr stringByAppendingPathComponent:@"coreData.sqlite"];
        NSLog(@"数据库 path = %@", sqlPath);
        NSURL *sqlUrl = [NSURL fileURLWithPath:sqlPath];
        
        NSError *error = nil;
        //设置数据库相关信息 添加一个持久化存储库并设置存储类型和路径,NSSQLiteStoreType:SQLite作为存储库
        [store addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:sqlUrl options:nil error:&error];
        
        if (error) {
            NSLog(@"添加数据库失败:%@",error);
        } else {
            NSLog(@"添加数据库成功");
        }
        
        //3、创建上下文 保存信息 操作数据库
        
        NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        
        //关联持久化助理
        context.persistentStoreCoordinator = store;
        
        _context = context;
        
        
    }
    

    六、Keychain(SQLite API进行封装的库)
    • Keychain存储也并不是绝对安全,越狱设备可以拿到
    • 程序卸载还存在
    • 对于每个应用来说,keychain都有两个访问区,私有区和公共区
      私有区是一个闭合的存储区域,每个应用只能操作自己的私有区,本应用存储的任何数据对其他程序不可见,其他程序也没有权限访问这个私有区。(可以理解为存在钥匙串的沙盒)。
      公共区,apple提供给同一个开发者账号开发的多个app之间的一个数据共享模块。现在只局限于同一个开发者账号下的不同app之间数据共享。这个区域是独立于私有区的另外一个数据存储空间。实现多个应用间共同访问一些数据。
      缺陷:Keychain变化的几种情况
      1.越狱机
      2.部分操作系统bug
      3.应用卸载后升级iOS系统

    七、第三方FMDB,BGFMDB
    • 下载FMDB框架,程序导入sqlite3.0框架
    • 废话不多说,上代码
    #import "ViewController.h"
    #import "FMDB.h"
    #import "Person.h"
    @interface ViewController ()
    {
        FMDatabase *db;
    }
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        [self createDB];
        [self deleteData];
        [self queryData];
    }
    
    -(void)createDB {
       // 1.
        NSString *docuPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
        NSString *dbPath = [docuPath stringByAppendingPathComponent:@"test.db"];
        NSLog(@"!!!dbPath = %@",dbPath);
        //2.创建对应路径下数据库
        db = [FMDatabase databaseWithPath:dbPath];
        //3.在数据库中进行增删改查操作时,需要判断数据库是否open
        [db open];
        if (![db open]) {
            NSLog(@"db open fail");
            return;
        }
        //4.数据库中创建表(可创建多张)
        NSString *sql = @"create table if not exists t_student ('ID' INTEGER PRIMARY KEY AUTOINCREMENT,'name' TEXT NOT NULL, 'phone' TEXT NOT NULL,'score' INTEGER NOT NULL)";
        //5.执行更新操作 此处database直接操作,不考虑多线程问题,多线程问题,用FMDatabaseQueue 每次数据库操作之后都会返回bool数值,YES,表示success,NO,表示fail,可以通过 @see lastError @see lastErrorCode @see lastErrorMessage
        BOOL result = [db executeUpdate:sql];
        if (result) {
            NSLog(@"create table success");
            
        }
        [db close];
    }
    - (void)insertData {
        [db open];
        // 插入
        BOOL result2 = [db executeUpdate:@"insert into 't_student'(ID,name,phone,score) values(?,?,?,?)" withArgumentsInArray:@[@113,@"x3",@"13",@53]];
        if (result2) {
            NSLog(@"insert into 't_studet' success");
            [self showAlertWithTitle:@"insert  success" message:nil person:nil];
        } else {
            [self showAlertWithTitle:[db lastError].description message:nil person:nil];
        }
        [db close];
    }
    -(void)deleteData{
        [db open];
        BOOL result = [db executeUpdate:@"delete from 't_student' where ID = ?" withArgumentsInArray:@[@153]];
        if (result) {
            NSLog(@"delete from 't_student' success");
            [self showAlertWithTitle:@"delete  success" message:nil person:nil];
        } else {
            [self showAlertWithTitle:[db lastError].description message:nil person:nil];
        }
        [db close];
        
    }
    -(void)updateData{
        [db open];
        BOOL result = [db executeUpdate:@"update 't_student' set ID = ? where name = ?" withArgumentsInArray:@[@153,@"x3"]];
        if (result) {
            NSLog(@"update 't_student' success");
            [self showAlertWithTitle:@"update  success" message:nil person:nil];
        } else {
            [self showAlertWithTitle:[db lastError].description message:nil person:nil];
        }
        [db close];
    }
    -(void)queryData {
        [db open];
        FMResultSet *result = [db executeQuery:@"select * from 't_student' where ID = ?" withArgumentsInArray:@[@153]];
        NSMutableArray *arr = [NSMutableArray array];
        while ([result next]) {
            Person *person = [Person new];
            person.ID = [result intForColumn:@"ID"];
            person.name = [result stringForColumn:@"name"];
            person.phone = [result stringForColumn:@"phone"];
            person.score = [result intForColumn:@"score"];
            [arr addObject:person];
            NSLog(@"从数据库查询到的人员 %d-%@-%@-%d",person.ID, person.name,person.phone,person.score);
            [self showAlertWithTitle:@"query  success" message:nil person:person];
            
        }
    }
    -(void)showAlertWithTitle:(NSString *)title
                      message:(NSString *)message
                       person:(Person *)person
    {
        UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"sure" style:UIAlertActionStyleDefault handler:nil];
        [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
            textField.text = person.name ? person.name : @"other";
        }];
        [alert addAction:cancelAction];
        [self presentViewController:alert animated:YES completion:^{
          }];
    }
    @end
    

    相关文章

      网友评论

        本文标题:iOS 本地存储数据的几种方式

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