MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C 文件操作与数据持久化

2022-05-212.6k 阅读

1. Objective - C 文件操作基础

1.1 路径相关操作

在 Objective - C 中处理文件操作,首先要熟悉路径的相关操作。获取文件路径是进行文件读取、写入等操作的前提。

NSFileManager 类提供了丰富的方法来处理文件路径相关任务。例如,要获取应用程序的文档目录路径,可以使用以下代码:

NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentsURL = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
NSString *documentsPath = [documentsURL path];
NSLog(@"Documents Path: %@", documentsPath);

这里通过 [NSFileManager defaultManager] 获取默认的文件管理器实例,然后使用 URLsForDirectory:inDomains: 方法获取文档目录的 URL,最后通过 path 属性将 URL 转换为字符串路径。

除了文档目录,还有应用程序主目录、缓存目录等重要路径。获取应用程序主目录路径可以使用 NSBundle 类:

NSString *mainBundlePath = [[NSBundle mainBundle] bundlePath];
NSLog(@"Main Bundle Path: %@", mainBundlePath);

主目录包含应用程序的可执行文件、资源文件等。而缓存目录用于存储应用程序可以重新创建的临时数据,获取缓存目录路径如下:

NSURL *cachesURL = [fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask].firstObject;
NSString *cachesPath = [cachesURL path];
NSLog(@"Caches Path: %@", cachesPath);

1.2 创建文件

创建文件是文件操作的基本任务之一。可以使用 NSFileManager 类的 createFileAtPath:contents:attributes: 方法来创建文件。以下是一个创建文本文件的示例:

NSString *filePath = [documentsPath stringByAppendingPathComponent:@"test.txt"];
NSString *fileContent = @"This is a test file content.";
NSData *contentData = [fileContent dataUsingEncoding:NSUTF8StringEncoding];
BOOL success = [fileManager createFileAtPath:filePath contents:contentData attributes:nil];
if (success) {
    NSLog(@"File created successfully.");
} else {
    NSLog(@"Failed to create file.");
}

在上述代码中,首先构建了文件的完整路径 filePath,然后将字符串内容转换为 NSData 类型,最后调用 createFileAtPath:contents:attributes: 方法创建文件。如果文件创建成功,success 变量将为 YES

1.3 读取文件

读取文件内容也是常见的操作。对于文本文件,可以使用 NSStringinitWithContentsOfFile:encoding:error: 方法。例如读取刚才创建的 test.txt 文件:

NSError *error;
NSString *readContent = [[NSString alloc] initWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&error];
if (readContent) {
    NSLog(@"File content: %@", readContent);
} else {
    NSLog(@"Failed to read file: %@", error);
}

这里通过 initWithContentsOfFile:encoding:error: 方法尝试读取文件内容,如果读取成功,readContent 将包含文件的文本内容;如果失败,error 将包含错误信息。

对于二进制文件,可以使用 NSDatainitWithContentsOfFile: 方法。假设我们有一个二进制图片文件 image.png,读取它的代码如下:

NSString *imageFilePath = [documentsPath stringByAppendingPathComponent:@"image.png"];
NSData *imageData = [[NSData alloc] initWithContentsOfFile:imageFilePath];
if (imageData) {
    NSLog(@"Image data read successfully.");
} else {
    NSLog(@"Failed to read image data.");
}

NSData 类将文件内容以二进制数据的形式存储,方便后续处理,比如显示图片等操作。

1.4 写入文件

除了创建文件时写入初始内容,有时也需要对已存在的文件进行追加或覆盖写入。对于文本文件,NSString 提供了 writeToFile:atomically:encoding:error: 方法。如果要覆盖写入 test.txt 文件新的内容:

NSString *newContent = @"This is the new content.";
NSData *newContentData = [newContent dataUsingEncoding:NSUTF8StringEncoding];
success = [newContentData writeToFile:filePath atomically:YES];
if (success) {
    NSLog(@"File written successfully.");
} else {
    NSLog(@"Failed to write to file.");
}

这里 atomically:YES 表示写入操作是原子性的,即要么完全成功,要么完全失败,不会出现部分写入的情况。

如果要追加内容到文件,可以先读取文件原有的内容,然后与新内容拼接,再进行写入。以下是追加内容的示例:

NSError *readError;
NSString *originalContent = [[NSString alloc] initWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&readError];
if (originalContent) {
    NSString *appendContent = @" This is the appended content.";
    NSString *finalContent = [originalContent stringByAppendingString:appendContent];
    NSData *finalContentData = [finalContent dataUsingEncoding:NSUTF8StringEncoding];
    success = [finalContentData writeToFile:filePath atomically:YES];
    if (success) {
        NSLog(@"Content appended successfully.");
    } else {
        NSLog(@"Failed to append content.");
    }
} else {
    NSLog(@"Failed to read original content for appending: %@", readError);
}

2. 目录操作

2.1 创建目录

在文件系统中,目录用于组织文件。使用 NSFileManagercreateDirectoryAtPath:withIntermediateDirectories:attributes:error: 方法可以创建目录。例如,在文档目录下创建一个名为 newFolder 的目录:

NSString *newFolderPath = [documentsPath stringByAppendingPathComponent:@"newFolder"];
NSError *dirError;
BOOL dirCreated = [fileManager createDirectoryAtPath:newFolderPath withIntermediateDirectories:YES attributes:nil error:&dirError];
if (dirCreated) {
    NSLog(@"Directory created successfully.");
} else {
    NSLog(@"Failed to create directory: %@", dirError);
}

withIntermediateDirectories:YES 表示如果父目录不存在,会自动创建所有必要的中间目录。

2.2 列举目录内容

要获取目录中的文件和子目录列表,可以使用 NSFileManagercontentsOfDirectoryAtPath:error: 方法。继续以上面创建的 newFolder 目录为例,列举其内容:

NSArray *contents = [fileManager contentsOfDirectoryAtPath:newFolderPath error:&dirError];
if (contents) {
    NSLog(@"Directory contents: %@", contents);
} else {
    NSLog(@"Failed to list directory contents: %@", dirError);
}

该方法返回一个包含目录中所有文件和子目录名称的数组。如果需要获取文件和目录的详细属性,比如文件大小、创建时间等,可以使用 contentsOfDirectoryAtURL:includingPropertiesForKeys:options:error: 方法。示例如下:

NSURL *newFolderURL = [NSURL fileURLWithPath:newFolderPath];
NSArray *keys = @[NSURLNameKey, NSURLIsDirectoryKey, NSURLFileSizeKey];
NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtURL:newFolderURL includingPropertiesForKeys:keys options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:^(NSURL *url, NSError *error) {
    NSLog(@"Error accessing %@: %@", url, error);
    return YES;
}];
NSURL *fileURL;
while (fileURL = [enumerator nextObject]) {
    NSDictionary *properties = [fileURL resourceValuesForKeys:keys error:nil];
    NSString *name = properties[NSURLNameKey];
    NSNumber *isDirectory = properties[NSURLIsDirectoryKey];
    NSNumber *fileSize = properties[NSURLFileSizeKey];
    NSString *type = isDirectory.boolValue? @"Directory" : @"File";
    NSLog(@"%@ - %@ - Size: %@", name, type, fileSize);
}

这里通过 NSDirectoryEnumerator 逐个枚举目录中的项目,并获取指定的属性。

2.3 删除文件和目录

删除文件或目录可以使用 NSFileManagerremoveItemAtPath:error: 方法。例如删除刚才创建的 test.txt 文件:

BOOL fileDeleted = [fileManager removeItemAtPath:filePath error:&error];
if (fileDeleted) {
    NSLog(@"File deleted successfully.");
} else {
    NSLog(@"Failed to delete file: %@", error);
}

删除目录的操作类似,比如删除 newFolder 目录:

BOOL dirDeleted = [fileManager removeItemAtPath:newFolderPath error:&dirError];
if (dirDeleted) {
    NSLog(@"Directory deleted successfully.");
} else {
    NSLog(@"Failed to delete directory: %@", dirError);
}

需要注意的是,删除目录时,如果目录不为空,默认情况下会删除失败。可以先列举目录内容并逐个删除子项目,然后再删除目录本身,或者使用 NSFileManagerremoveItemAtURL:error: 方法并结合 NSURL 的相关属性来处理非空目录的删除。

3. 数据持久化概念与方式

3.1 数据持久化概述

数据持久化是指将应用程序中的数据保存到存储设备(如硬盘)中,以便在应用程序关闭后数据仍然存在,下次启动应用程序时可以重新加载使用。在 Objective - C 开发中,数据持久化是非常重要的功能,常见的场景包括保存用户设置、应用程序状态、游戏进度等。

3.2 归档(Archiving)

归档是一种将对象转换为字节流并保存到文件的过程,反之,从文件中读取字节流并还原为对象的过程称为解档。在 Objective - C 中,NSKeyedArchiverNSKeyedUnarchiver 类用于实现归档和解档操作。

要对自定义对象进行归档和解档,该对象必须遵守 NSCoding 协议。例如,定义一个简单的 Person 类:

@interface Person : NSObject <NSCoding>
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end

@implementation Person
- (instancetype)initWithCoder:(NSCoder *)coder {
    self = [super init];
    if (self) {
        self.name = [coder decodeObjectForKey:@"name"];
        self.age = [coder decodeIntegerForKey:@"age"];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
    [coder encodeObject:self.name forKey:@"name"];
    [coder encodeInteger:self.age forKey:@"age"];
}
@end

这里实现了 NSCoding 协议的两个方法 initWithCoder:encodeWithCoder:initWithCoder: 方法用于从归档数据中解档对象,encodeWithCoder: 方法用于将对象的属性编码到归档数据中。

归档 Person 对象的示例代码如下:

Person *person = [[Person alloc] init];
person.name = @"John";
person.age = 30;
NSString *archivePath = [documentsPath stringByAppendingPathComponent:@"person.archive"];
BOOL archived = [NSKeyedArchiver archiveRootObject:person toFile:archivePath];
if (archived) {
    NSLog(@"Person object archived successfully.");
} else {
    NSLog(@"Failed to archive person object.");
}

解档 Person 对象的代码如下:

Person *unarchivedPerson = [NSKeyedUnarchiver unarchiveObjectWithFile:archivePath];
if (unarchivedPerson) {
    NSLog(@"Unarchived Person - Name: %@, Age: %ld", unarchivedPerson.name, (long)unarchivedPerson.age);
} else {
    NSLog(@"Failed to unarchive person object.");
}

3.3 属性列表(Property Lists)

属性列表是一种 XML 格式的文件,用于存储简单的数据类型,如 NSStringNSNumberNSArrayNSDictionary 等。NSArrayNSDictionary 类提供了方便的方法来将自身写入属性列表文件和从属性列表文件读取数据。

例如,创建一个包含一些数据的 NSDictionary 并保存为属性列表文件:

NSDictionary *dataDict = @{
    @"name": @"Alice",
    @"age": @25,
    @"hobbies": @[@"Reading", @"Swimming"]
};
NSString *plistPath = [documentsPath stringByAppendingPathComponent:@"data.plist"];
BOOL saved = [dataDict writeToFile:plistPath atomically:YES];
if (saved) {
    NSLog(@"Property list saved successfully.");
} else {
    NSLog(@"Failed to save property list.");
}

读取属性列表文件的内容:

NSDictionary *readDict = [NSDictionary dictionaryWithContentsOfFile:plistPath];
if (readDict) {
    NSLog(@"Read Property List: %@", readDict);
} else {
    NSLog(@"Failed to read property list.");
}

属性列表适用于存储简单的配置信息、用户设置等数据,其优点是格式简单、可读性强,缺点是只能存储特定的简单数据类型,对于复杂对象需要进行额外处理。

3.4 SQLite 数据库

SQLite 是一个轻量级的嵌入式数据库,在 Objective - C 开发中广泛用于数据持久化。要使用 SQLite,需要引入 SQLite 库,并使用相关的 C 函数进行数据库操作。

首先,导入 SQLite 库并在文件中包含头文件:

#import <sqlite3.h>

以下是创建一个简单 SQLite 数据库并插入数据的示例:

NSString *dbPath = [documentsPath stringByAppendingPathComponent:@"test.db"];
sqlite3 *database;
if (sqlite3_open([dbPath UTF8String], &database) == SQLITE_OK) {
    const char *createTableSQL = "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)";
    char *errorMsg;
    if (sqlite3_exec(database, createTableSQL, NULL, NULL, &errorMsg) != SQLITE_OK) {
        NSLog(@"Failed to create table: %s", errorMsg);
        sqlite3_free(errorMsg);
    } else {
        const char *insertSQL = "INSERT INTO users (name, age) VALUES ('Bob', 28)";
        if (sqlite3_exec(database, insertSQL, NULL, NULL, &errorMsg) != SQLITE_OK) {
            NSLog(@"Failed to insert data: %s", errorMsg);
            sqlite3_free(errorMsg);
        } else {
            NSLog(@"Data inserted successfully.");
        }
    }
    sqlite3_close(database);
} else {
    NSLog(@"Failed to open database.");
}

在上述代码中,首先通过 sqlite3_open 函数打开或创建数据库文件。然后使用 sqlite3_exec 函数执行 SQL 语句来创建表和插入数据。最后通过 sqlite3_close 函数关闭数据库连接。

查询数据库数据的示例如下:

if (sqlite3_open([dbPath UTF8String], &database) == SQLITE_OK) {
    const char *selectSQL = "SELECT id, name, age FROM users";
    sqlite3_stmt *statement;
    if (sqlite3_prepare_v2(database, selectSQL, -1, &statement, NULL) == SQLITE_OK) {
        while (sqlite3_step(statement) == SQLITE_ROW) {
            int id = sqlite3_column_int(statement, 0);
            const char *name = (const char *)sqlite3_column_text(statement, 1);
            int age = sqlite3_column_int(statement, 2);
            NSString *userName = [NSString stringWithUTF8String:name];
            NSLog(@"User - ID: %d, Name: %@, Age: %d", id, userName, age);
        }
        sqlite3_finalize(statement);
    } else {
        NSLog(@"Failed to prepare query: %s", sqlite3_errmsg(database));
    }
    sqlite3_close(database);
} else {
    NSLog(@"Failed to open database for query.");
}

这里使用 sqlite3_prepare_v2 函数准备 SQL 查询语句,sqlite3_step 函数逐行获取查询结果,sqlite3_column_* 函数获取每列的数据。

SQLite 适用于存储大量结构化数据,支持复杂的查询操作,但相比归档和属性列表,其操作相对复杂,需要掌握 SQL 语句和 SQLite 的 C 函数接口。

4. 应用场景与最佳实践

4.1 应用场景分析

在实际应用开发中,不同的数据持久化方式适用于不同的场景。

对于简单的用户设置,如是否开启声音、是否显示通知等,使用属性列表是一个很好的选择。因为属性列表易于创建和读取,格式简单,并且可以直接与 NSDictionary 等对象进行转换,方便在代码中操作。

如果应用程序中有自定义的复杂对象,如游戏角色、文档模型等,归档是比较合适的方式。通过遵守 NSCoding 协议,可以将对象的状态完整地保存到文件中,并在需要时准确地还原。

当数据量较大且需要进行复杂的查询、排序等操作时,SQLite 数据库是最佳选择。例如,一个新闻应用程序需要存储大量的新闻文章,并根据发布时间、分类等条件进行查询,SQLite 可以高效地处理这些需求。

4.2 最佳实践建议

在进行文件操作和数据持久化时,以下是一些最佳实践建议:

  • 错误处理:无论是文件操作还是数据持久化操作,都要进行充分的错误处理。在 Objective - C 中,许多文件和持久化相关的方法都提供了错误参数,要及时检查这些错误并进行相应的处理,如向用户提示错误信息,或者尝试恢复操作。
  • 数据备份与恢复:对于重要的数据,应该提供备份和恢复机制。可以定期将数据备份到外部存储或云服务,当数据丢失或损坏时能够恢复。
  • 性能优化:在处理大量数据时,要注意性能问题。例如,在使用 SQLite 时,合理设计数据库表结构,使用事务来批量处理操作,可以提高数据插入和更新的效率。在读取大文件时,可以采用分块读取的方式,避免一次性加载大量数据导致内存占用过高。
  • 安全性:如果存储的数据包含敏感信息,如用户密码、支付信息等,要进行加密处理。对于文件操作,要注意文件权限设置,确保敏感数据不会被非法访问。

通过遵循这些最佳实践,可以提高应用程序的稳定性、可靠性和性能,为用户提供更好的体验。同时,在选择数据持久化方式时,要根据具体的应用场景和需求进行权衡,选择最合适的方式来实现数据的有效存储和管理。