macOS App Sandbox:文档相对书签无法正常访问问题排查
问题背景
开发基于文档的macOS应用,需要处理MP3文件,因此将用户选择的MP3文件引用存储在应用文档中。受App Sandbox限制,应用重启后无法访问该MP3文件,于是尝试使用文档相对书签机制解决。按官方方案在dataOfType:error:/readFromURL:ofType:error:中通过NSKeyedArchiver/NSKeyedUnarchiver存储书签,但出现异常:应用容器内的自动保存文件可正常工作,保存到桌面的文件调用URLByResolvingBookmarkData:...时抛出错误:
Error Domain=NSCocoaErrorDomain Code=256 "Failed to retrieve collection-scope key" UserInfo={NSDebugDescription=Failed to retrieve collection-scope key}
已在entitlements文件中添加com.apple.security.files.bookmarks.document-scope = YES,但无效果。
复现步骤(最小示例)
运行环境:Apple M2、macOS 12.6.6、Xcode 14.2
- 使用macOS Document App模板创建新项目(XIB界面,Obj-C语言)
- 目标配置「Info」页修改:
- 将默认文档类型Identifier改为
com.example.test - 添加新文档类型:名称「Text」,Identifier
public.plain-text,角色Viewer - 修改导入类型标识符:描述「Test」,扩展名「test」,Identifier
com.example.test,Conforms Topublic.data
- 将默认文档类型Identifier改为
- 在entitlements文件中添加布尔键
com.apple.security.files.bookmarks.document-scope,值为YES - 替换Document.m代码:
#import "Document.h" @interface Document () @property (strong, nonatomic) NSURL *txtFileURL; @property (strong, nonatomic) NSData *bookmark; @end @implementation Document + (BOOL)autosavesInPlace { return YES; } - (NSString *)windowNibName { return @"Document"; } - (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError { if (self.bookmark == nil) { NSError *error = nil; self.bookmark = [self.txtFileURL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:self.fileURL error:&error]; if (error != nil) { NSLog(@"error creating bookmark: %@", error); } } return [NSKeyedArchiver archivedDataWithRootObject:self.bookmark]; } - (BOOL)readFromURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError *__autoreleasing _Nullable *)outError { if ([typeName isEqual:@"public.plain-text"]) { self.fileURL = nil; self.fileType = @"com.example.test"; self.txtFileURL = url; } else if ([typeName isEqual:@"com.example.test"]) { self.bookmark = [NSKeyedUnarchiver unarchiveObjectWithData:[NSData dataWithContentsOfURL:url]]; NSError *error = nil; BOOL isStale = NO; self.txtFileURL = [[NSURL alloc] initByResolvingBookmarkData:self.bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:self.fileURL bookmarkDataIsStale:&isStale error:&error]; NSLog(@"isStale = %@", isStale ? @"YES" : @"NO"); if (error != nil) { NSLog(@"error resolving bookmark: %@", error); } } else { return NO; } [self updateChangeCount:NSChangeDone]; NSLog(@"txtFileURL = %@", self.txtFileURL); [self.txtFileURL startAccessingSecurityScopedResource]; NSString *str = [NSString stringWithContentsOfURL:self.txtFileURL]; [self.txtFileURL stopAccessingSecurityScopedResource]; NSLog(@"txt file contents: %@", str); return YES; } @end
- 在桌面创建内容为「bar」的foo.txt文件
- 运行应用,将foo.txt拖到Dock图标创建新文档,可正常读取内容
- 不保存退出应用,重启后可恢复自动保存的文档并正常读取
- 将文档保存到桌面为test.test
- 关闭后重新打开test.test,控制台报错,无法解析书签
补充:替换dataOfType:error:为writeToURL:ofType:error:的代码
- (BOOL)writeToURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError *__autoreleasing _Nullable *)outError { NSString *temp = @"Temp"; NSError *error; [temp writeToURL:url atomically:YES encoding:NSUTF8StringEncoding error:&error]; if (error != nil) { NSLog(@"error creating temp file: %@", error); } if (self.bookmark == nil) { NSError *error = nil; self.bookmark = [self.txtFileURL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:url error:&error]; if (error != nil) { NSLog(@"error creating bookmark: %@", error); } } NSLog(@"writeToURL: url: %@", url); return [[NSKeyedArchiver archivedDataWithRootObject:self.bookmark] writeToURL:url atomically:YES]; }
问题原因与解决方案
原因
文档范围书签要求创建书签时的relativeToURL必须是最终保存的文档URL,但在dataOfType:error:方法中,self.fileURL此时是自动保存的临时路径,而非用户最终选择的外部保存路径,导致书签绑定了错误的相对基准。自动保存文件能正常工作是因为其路径始终在应用容器内,相对基准未发生变化;而保存到外部位置时,相对基准URL改变,书签无法正确解析。
解决方案
- 在
writeToURL:ofType:error:中创建书签
该方法的url参数是用户最终选择的保存路径,用它作为relativeToURL创建书签,确保相对基准正确。同时移除dataOfType:error:中的书签创建逻辑,改为返回已有的书签数据。
修改后的关键代码:
writeToURL:ofType:error:
- (BOOL)writeToURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError **)outError { // 创建基于最终保存路径的文档相对书签 NSError *bookmarkError = nil; self.bookmark = [self.txtFileURL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:url error:&bookmarkError]; if (bookmarkError != nil) { NSLog(@"error creating bookmark: %@", bookmarkError); if (outError) *outError = bookmarkError; return NO; } // 将书签数据写入文档 NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject:self.bookmark]; return [archivedData writeToURL:url atomically:YES]; }
dataOfType:error:
- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError { // 自动保存时直接返回现有书签数据 return [NSKeyedArchiver archivedDataWithRootObject:self.bookmark]; }
- 额外注意事项
- 首次访问目标文件时,需先调用
startAccessingSecurityScopedResource获取访问权限,使用完成后调用stopAccessingSecurityScopedResource释放权限 - 解析书签时,确保使用当前文档的URL作为
relativeToURL参数
- 首次访问目标文件时,需先调用
修改后,保存到外部位置的文档即可正确解析书签,访问目标文件。
内容的提问来源于stack exchange,提问作者tajmahal

