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 进行轻量序列化,FileManager URL 方式操作路径。

posted @ 2015-08-01 23:37  Mr.陳  阅读(1196)  评论(0)    收藏  举报