iOS开发基础28-数据存储与沙盒机制全解析
iOS 数据存储与沙盒机制全解析
本文系统梳理 iOS 五种数据存储方式(plist、NSUserDefaults、NSKeyedArchiver、SQLite3、Core Data)、应用沙盒目录结构与获取方式、Keychain 敏感数据存储,并补充各方式的 Swift 对照与现代 API。
一、iOS 数据存储方式总览
| 存储方式 | 存储格式 | 能否存储自定义对象 | 适用场景 | 备份 |
|---|---|---|---|---|
| plist | XML 文件 | 否(仅基本类型) | 少量结构化配置数据 | 取决于存放目录 |
| NSUserDefaults | plist(Library/Preferences) | 否 | 应用配置、用户偏好、少量键值对 | 是 |
| NSKeyedArchiver | 二进制文件 | 是(实现 NSCoding/NSSecureCoding) | 自定义对象归档、深复制 | 取决于存放目录 |
| SQLite3 | 数据库文件 | 否(需手动映射) | 大量结构化数据、复杂查询 | 取决于存放目录 |
| Core Data | SQLite(默认) | 是(对象图) | 复杂数据模型、关系映射、大量数据 | 取决于存放目录 |
| Keychain | 加密数据库 | 否(NSData) | 密码、令牌等敏感数据 | 是(跨设备加密备份) |
二、应用沙盒机制
每个 iOS 应用运行在独立的沙盒目录中,与其他应用隔离,无法直接访问其他应用的数据。
1. 沙盒目录结构
AppSandbox/
├── MyApp.app # 应用程序包(只读,包含可执行文件和资源)
├── Documents/ # 用户生成的重要数据,iTunes/iCloud 备份
│ └── ...
├── Library/
│ ├── Preferences/ # NSUserDefaults 存储位置,备份
│ ├── Caches/ # 缓存文件,系统可能清理,不备份
│ ├── Application Support/ # 应用支持文件,备份(默认不存在,需手动创建)
│ └── ...
└── tmp/ # 临时文件,系统可能清理,不备份
| 目录 | 用途 | 备份 | 系统清理 |
|---|---|---|---|
MyApp.app |
应用程序包,只读 | 否(可从 App Store 重新下载) | 否 |
Documents/ |
用户生成的重要数据 | 是 | 否 |
Library/Preferences/ |
NSUserDefaults 数据 | 是 | 否 |
Library/Application Support/ |
应用支持文件 | 是 | 否 |
Library/Caches/ |
缓存文件(可重新下载) | 否 | 是(存储空间不足时) |
tmp/ |
临时文件 | 否 | 是(应用未运行时) |
2. 获取沙盒目录
// 沙盒根路径
NSString *homePath = NSHomeDirectory();
// tmp 目录
NSString *tmpPath = NSTemporaryDirectory();
// Documents 目录
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
// Library/Caches 目录
NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
// Library 目录
NSString *libraryPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject];
// Library/Preferences 目录(NSUserDefaults 自动管理,通常无需手动获取)
NSString *preferencesPath = [libraryPath stringByAppendingPathComponent:@"Preferences"];
// Library/Application Support 目录
NSString *appSupportPath = [libraryPath stringByAppendingPathComponent:@"Application Support"];
NSPreferencePanesDirectory是系统偏好设置面板目录,不是应用的Library/Preferences。获取应用偏好设置目录应通过NSLibraryDirectory拼接Preferences,或直接使用NSUserDefaults(自动管理路径)。
3. 路径拼接与文件操作
// 拼接文件路径
NSString *filePath = [documentsPath stringByAppendingPathComponent:@"data.plist"];
// 获取文件路径各部分
NSString *fileName = [filePath lastPathComponent]; // data.plist
NSString *extension = [filePath pathExtension]; // plist
NSString *directory = [filePath stringByDeletingLastPathComponent]; // Documents 路径
// 用 NSURL 操作路径(推荐)
NSURL *documentsURL = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject];
NSURL *fileURL = [documentsURL URLByAppendingPathComponent:@"data.plist"];
三、属性列表(plist)存储
plist 是 XML 格式文件,只能存储基本数据类型:NSString、NSNumber、NSArray、NSDictionary、NSDate、NSData,不能存储自定义对象。
1. 存储与读取数组
// 存储
NSArray *array = @[@"item1", @"item2", @"item3"];
NSString *filePath = [documentsPath stringByAppendingPathComponent:@"data.plist"];
[array writeToFile:filePath atomically:YES];
// 读取
NSArray *loadedArray = [NSArray arrayWithContentsOfFile:filePath];
2. 存储与读取字典
// 存储
NSDictionary *dict = @{@"name": @"Jane", @"phone": @"123456", @"age": @27};
[dict writeToFile:filePath atomically:YES];
// 读取
NSDictionary *loadedDict = [NSDictionary dictionaryWithContentsOfFile:filePath];
atomically:YES表示先写入临时文件,写入成功后再替换原文件,防止写入过程中应用崩溃导致文件损坏。读取失败时返回nil(文件不存在或格式错误)。
四、偏好设置(NSUserDefaults)
NSUserDefaults 用于存储应用配置和用户偏好,底层是 plist 文件,存储在 Library/Preferences/{bundleID}.plist。
1. 保存与读取
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// 保存
[defaults setObject:@"password" forKey:@"pwd"];
[defaults setBool:YES forKey:@"isOn"];
[defaults setInteger:10 forKey:@"num"];
[defaults setFloat:3.14 forKey:@"pi"];
[defaults setDouble:3.14159 forKey:@"piDouble"];
[defaults setURL:[NSURL URLWithString:@"https://example.com"] forKey:@"homepage"];
// 读取
NSString *pwd = [defaults objectForKey:@"pwd"];
BOOL isOn = [defaults boolForKey:@"isOn"];
NSInteger num = [defaults integerForKey:@"num"];
NSURL *homepage = [defaults URLForKey:@"homepage"];
2. 其他常用操作
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// 删除某个键
[defaults removeObjectForKey:@"pwd"];
// 注册默认值(首次启动时的默认值,不会覆盖已保存的值)
NSDictionary *defaultValues = @{@"isOn": @YES, @"num": @0};
[defaults registerDefaults:defaultValues];
// 强制立即写入磁盘(iOS 10+ 已不推荐,系统自动同步)
[defaults synchronize];
// 检查是否存在某个键
BOOL exists = ([defaults objectForKey:@"pwd"] != nil);
3. 注意事项
- 适合少量配置数据,不适合存储大量数据或敏感数据(密码、令牌应存 Keychain)。
- 数据不是立即写入磁盘,系统在适当时机自动同步;如需立即写入可调用
synchronize(iOS 10+ 不推荐)。 - 支持的类型:
NSString、NSNumber、NSArray、NSDictionary、NSDate、NSData、NSURL及基本类型(BOOL/NSInteger/NSFloat/NSDouble)。 NSUserDefaults是线程安全的。
五、NSKeyedArchiver 归档与解档
NSKeyedArchiver 将对象序列化为二进制数据,可存储自定义对象(需实现 NSCoding 或 NSSecureCoding 协议)。
1. 自定义对象实现 NSCoding
// Person.h
@interface Person : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end
// Person.m
@implementation Person
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:self.name forKey:@"name"];
[coder encodeInt:self.age forKey:@"age"];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super init]) {
self.name = [coder decodeObjectForKey:@"name"];
self.age = [coder decodeIntForKey:@"age"];
}
return self;
}
@end
如果父类也实现了
NSCoding,initWithCoder:中应调用[super initWithCoder:coder],encodeWithCoder:中应调用[super encodeWithCoder:coder]。
2. 简单归档与解档(单个对象)
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"person.data"];
// 归档
Person *person = [[Person alloc] init];
person.name = @"John Doe";
person.age = 30;
[NSKeyedArchiver archiveRootObject:person toFile:filePath];
// 解档
Person *decodedPerson = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
3. 归档多个对象
// 归档
NSMutableData *data = [NSMutableData data];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:person1 forKey:@"person1"];
[archiver encodeObject:person2 forKey:@"person2"];
[archiver finishEncoding];
[data writeToFile:filePath atomically:YES];
// 解档
NSData *loadedData = [NSData dataWithContentsOfFile:filePath];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:loadedData];
Person *p1 = [unarchiver decodeObjectForKey:@"person1"];
Person *p2 = [unarchiver decodeObjectForKey:@"person2"];
[unarchiver finishDecoding];
4. iOS 10+ 现代 API(推荐)
iOS 10 引入了基于 NSError 的新 API,支持安全编码:
// 归档
NSError *archiveError = nil;
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:person
requiringSecureCoding:YES
error:&archiveError];
[data writeToFile:filePath atomically:YES];
// 解档
NSData *loadedData = [NSData dataWithContentsOfFile:filePath];
NSError *unarchiveError = nil;
Person *decodedPerson = [NSKeyedUnarchiver unarchivedObjectOfClass:[Person class]
fromData:loadedData
error:&unarchiveError];
5. NSSecureCoding(推荐替代 NSCoding)
NSSecureCoding 继承自 NSCoding,在解档时校验对象类型,防止替换攻击(攻击者替换归档中的对象类型):
// Person.h
@interface Person : NSObject <NSSecureCoding>
@end
// Person.m
@implementation Person
+ (BOOL)supportsSecureCoding {
return YES;
}
// encodeWithCoder: 和 initWithCoder: 与 NSCoding 相同
@end
使用
NSSecureCoding时,解档应使用unarchivedObjectOfClass:fromData:error:(指定期望的类),而非unarchiveObjectWithData:。
6. 归档实现深复制
通过归档+解档可实现对象的深复制(完全独立的副本):
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:originalObject requiringSecureCoding:YES error:nil];
id deepCopy = [NSKeyedUnarchiver unarchivedObjectOfClass:[originalObject class] fromData:data error:nil];
六、SQLite3 与 Core Data 简介
1. SQLite3
SQLite3 是轻量级嵌入式关系数据库,通过 C 函数库操作,适合大量结构化数据和复杂查询。
#import <sqlite3.h>
// 打开数据库
sqlite3 *db = NULL;
NSString *dbPath = [documentsPath stringByAppendingPathComponent:@"app.db"];
if (sqlite3_open(dbPath.UTF8String, &db) != SQLITE_OK) {
NSLog(@"打开数据库失败");
}
// 创建表
const char *createSQL = "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER);";
char *error = NULL;
sqlite3_exec(db, createSQL, NULL, NULL, &error);
// 插入数据
const char *insertSQL = "INSERT INTO users (name, age) VALUES (?, ?);";
sqlite3_stmt *stmt = NULL;
sqlite3_prepare_v2(db, insertSQL, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, "John".UTF8String, -1, NULL);
sqlite3_bind_int(stmt, 2, 30);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
// 查询数据
const char *selectSQL = "SELECT id, name, age FROM users;";
sqlite3_stmt *queryStmt = NULL;
sqlite3_prepare_v2(db, selectSQL, -1, &queryStmt, NULL);
while (sqlite3_step(queryStmt) == SQLITE_ROW) {
int userId = sqlite3_column_int(queryStmt, 0);
const unsigned char *name = sqlite3_column_text(queryStmt, 1);
int age = sqlite3_column_int(queryStmt, 2);
NSLog(@"id=%d, name=%s, age=%d", userId, name, age);
}
sqlite3_finalize(queryStmt);
// 关闭数据库
sqlite3_close(db);
实际开发中推荐使用 FMDB(对 SQLite3 的 Objective-C 封装)或 GRDB.swift(Swift 封装),避免手动管理 C 函数和内存。
2. Core Data
Core Data 是 Apple 提供的对象图管理和持久化框架,底层默认使用 SQLite,支持对象关系映射、数据迁移、增量更新、iCloud 同步等高级功能。
// 1. 创建模型(.xcdatamodeld 文件中定义 Entity)
// 2. 初始化 Core Data 栈
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"DataModel" withExtension:@"momd"];
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
NSURL *storeURL = [[documentsPath stringByAppendingPathComponent:@"DataModel.sqlite"]];
[coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:nil];
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
context.persistentStoreCoordinator = coordinator;
// 3. 插入数据
NSManagedObject *user = [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:context];
[user setValue:@"John" forKey:@"name"];
[user setValue:@30 forKey:@"age"];
[context save:nil];
// 4. 查询数据
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"User"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age > %d", 18];
request.predicate = predicate;
NSArray *results = [context executeFetchRequest:request error:nil];
iOS 13+ 推荐使用
NSPersistentContainer简化 Core Data 栈初始化;SwiftUI 中通过@Environment(\.managedObjectContext)获取上下文。
七、Keychain 存储敏感数据
Keychain 用于存储密码、令牌、证书等敏感数据,系统加密存储,支持跨设备加密备份,应用删除后数据仍保留(同一开发者账号下的应用可通过 Keychain Group 共享)。
#import <Security/Security.h>
// 保存数据到 Keychain
- (void)savePassword:(NSString *)password forAccount:(NSString *)account {
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: @"com.example.app", // 服务标识
(__bridge id)kSecAttrAccount: account, // 账号
(__bridge id)kSecValueData: passwordData // 密码数据
};
// 先删除旧数据,再添加新数据
SecItemDelete((__bridge CFDictionaryRef)query);
SecItemAdd((__bridge CFDictionaryRef)query, NULL);
}
// 从 Keychain 读取数据
- (NSString *)passwordForAccount:(NSString *)account {
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: @"com.example.app",
(__bridge id)kSecAttrAccount: account,
(__bridge id)kSecReturnData: (__bridge id)kCFBooleanTrue,
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne
};
CFDataRef passwordData = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&passwordData);
if (status == errSecSuccess && passwordData) {
NSString *password = [[NSString alloc] initWithData:(__bridge NSData *)passwordData encoding:NSUTF8StringEncoding];
CFRelease(passwordData);
return password;
}
return nil;
}
实际开发中推荐使用 KeychainAccess、SAMKeychain 等第三方库简化 Keychain 操作。
八、Swift 版本对照
沙盒路径
let homePath = NSHomeDirectory()
let tmpPath = NSTemporaryDirectory()
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let cachesPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0]
let libraryPath = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true)[0]
// 推荐使用 FileManager 的 URL 方式
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = documentsURL.appendingPathComponent("data.plist")
plist 存储
let array = ["item1", "item2", "item3"]
let filePath = (documentsPath as NSString).appendingPathComponent("data.plist")
(array as NSArray).write(toFile: filePath, atomically: true)
let loaded = NSArray(contentsOfFile: filePath) as? [String]
UserDefaults
let defaults = UserDefaults.standard
defaults.set("password", forKey: "pwd")
defaults.set(true, forKey: "isOn")
defaults.set(10, forKey: "num")
let pwd = defaults.string(forKey: "pwd")
let isOn = defaults.bool(forKey: "isOn")
let num = defaults.integer(forKey: "num")
defaults.removeObject(forKey: "pwd")
defaults.register(defaults: ["isOn": true])
NSKeyedArchiver(NSSecureCoding)
class Person: NSObject, NSSecureCoding {
static var supportsSecureCoding: Bool { true }
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func encode(with coder: NSCoder) {
coder.encode(name, forKey: "name")
coder.encode(age, forKey: "age")
}
required init?(coder: NSCoder) {
name = coder.decodeObject(forKey: "name") as? String ?? ""
age = coder.decodeInteger(forKey: "age")
}
}
// 归档
let person = Person(name: "John", age: 30)
let data = try NSKeyedArchiver.archivedData(withRootObject: person, requiringSecureCoding: true)
try data.write(to: fileURL)
// 解档
let loadedData = try Data(contentsOf: fileURL)
let decoded = try NSKeyedUnarchiver.unarchivedObject(ofClass: Person.self, from: loadedData)
Swift 原生编码(Codable)
Swift 中推荐使用 Codable 协议进行对象序列化,比 NSCoding 更轻量、更 Swifty:
struct Person: Codable {
let name: String
let age: Int
}
// 编码为 JSON
let person = Person(name: "John", age: 30)
let encoder = JSONEncoder()
let jsonData = try encoder.encode(person)
// 解码
let decoder = JSONDecoder()
let decoded = try decoder.decode(Person.self, from: jsonData)
// 也可以编码为 PropertyList(plist)
let plistEncoder = PropertyListEncoder()
let plistData = try plistEncoder.encode(person)
九、总结
- 沙盒机制:每个应用独立沙盒,包含
MyApp.app(只读)、Documents/(重要数据,备份)、Library/Preferences/(NSUserDefaults,备份)、Library/Caches/(缓存,不备份,可清理)、Library/Application Support/(支持文件,备份)、tmp/(临时,不备份,可清理)。 - plist:XML 格式,仅存基本类型,适合少量结构化数据,
writeToFile:atomically:原子写入防损坏。 - NSUserDefaults:底层 plist,存配置偏好,不适合大量数据和敏感数据,系统自动同步,
registerDefaults:注册默认值。 - NSKeyedArchiver:二进制归档,自定义对象需实现
NSCoding/NSSecureCoding,iOS 10+ 推荐archivedDataWithRootObject:requiringSecureCoding:error:新 API,可实现深复制。 - SQLite3:C 函数库操作的轻量数据库,适合大量结构化数据,推荐 FMDB/GRDB 封装。
- Core Data:对象图持久化框架,底层默认 SQLite,支持关系映射、数据迁移、iCloud 同步,iOS 13+ 用
NSPersistentContainer。 - Keychain:系统加密存储,适合密码令牌等敏感数据,应用删除后保留,推荐第三方库简化操作。
- Swift:推荐
Codable协议替代 NSCoding 进行轻量序列化,FileManagerURL 方式操作路径。

浙公网安备 33010602011771号