You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

macOS App Sandbox:文档相对书签无法正常访问问题排查

文档范围书签解析失败:NSCocoaErrorDomain Code=256 问题排查与解决

问题背景

开发基于文档的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」,Identifierpublic.plain-text,角色Viewer
    • 修改导入类型标识符:描述「Test」,扩展名「test」,Identifiercom.example.test,Conforms Topublic.data
  • 在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改变,书签无法正确解析。

解决方案

  1. 在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];
}
  1. 额外注意事项
    • 首次访问目标文件时,需先调用startAccessingSecurityScopedResource获取访问权限,使用完成后调用stopAccessingSecurityScopedResource释放权限
    • 解析书签时,确保使用当前文档的URL作为relativeToURL参数

修改后,保存到外部位置的文档即可正确解析书签,访问目标文件。


内容的提问来源于stack exchange,提问作者tajmahal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 13:09:54