NSBundle pathForResource在shell工具中失败

时间:2010-10-04 19:57:06

标签: objective-c cocoa macos

我注意到在NSBundle中使用它时有一些奇怪的行为 命令行程序。如果,在我的程序中,我采用现有的捆绑包 制作它的副本然后尝试使用pathForResource来查找 在Resources文件夹中的某些内容,除非是,否则始终返回nil 捆绑我在程序启动之前就已经存在了。我创造了一个 复制问题的示例应用程序和相关代码是:

int main(int argc, char *argv[]) 
{ 
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 
    NSString *exePath = [NSString stringWithCString:argv[0]
                                           encoding:NSASCIIStringEncoding]; 
    NSString *path = [exePath stringByDeletingLastPathComponent]; 
    NSString *templatePath = [path stringByAppendingPathComponent:@"TestApp.app"];

    // This call works because TestApp.app exists before this program is run 
    NSString *resourcePath = [NSBundle pathForResource:@"InfoPlist" 
                                                ofType:@"strings"
                                           inDirectory:templatePath]; 
    NSLog(@"NOCOPY: %@", resourcePath); 

    NSString *copyPath = [path stringByAppendingPathComponent:@"TestAppCopy.app"]; 
    [[NSFileManager defaultManager] removeItemAtPath:copyPath 
                                               error:nil]; 
    if ([[NSFileManager defaultManager] copyItemAtPath:templatePath 
                                                toPath:copyPath 
                                                 error:nil]) 
    { 
        // This call will fail if TestAppCopy.app does not exist before 
        // this program is run
        NSString *resourcePath2 = [NSBundle pathForResource:@"InfoPlist"
                                                     ofType:@"strings"
                                                inDirectory:copyPath]; 
        NSLog(@"COPY: %@", resourcePath2); 
        [[NSFileManager defaultManager] removeItemAtPath:copyPath 
                                                   error:nil]; 
    } 
    [pool release]; 
} 

出于此测试应用程序的目的,我们假设TestApp.app 已存在于与我的测试应用程序相同的目录中。如果我运行这个, 第二个NSLog调用将输出:COPY:(null)

现在,如果我在if中注释掉最后的removeItemAtPath调用 声明,以便当我的程序退出TestAppCopy.app时仍然存在 然后重新运行,程序将按预期工作。

我在普通的Cocoa应用程序中尝试了这个,但我无法重现 行为。它只发生在shell工具目标中。 谁能想到这个失败的原因?

顺便说一句:我在10.6.4尝试这个,我没有尝试过其他任何一个 Mac OS X的版本。

2 个答案:

答案 0 :(得分:3)

我可以确认它是CoreFoundation中的错误,而不是Foundation。该错误是由于CFBundle代码依赖于包含陈旧数据的目录内容缓存。代码显然假设bundle目录及其直接父目录在应用程序运行时都不会改变。

+[NSBundle pathForResource:ofType:inDirectory:]对应的CoreFoundation调用是CFBundleCopyResourceURLInDirectory(),它表现出相同的错误行为。 (这并不奇怪,因为-pathForResource:ofType:inDirectory:本身使用此调用。)

问题最终在于_CFBundleCopyDirectoryContentsAtPath()。在捆绑加载期间和所有资源查找期间调用此方法。它缓存有关在contentsCache中查找的目录的信息。

问题在于:当需要获取TestAppCopy.app的内容时,包含TestApp.app的目录的缓存内容不包含TestAppCopy.app。因为缓存表面上具有该目录的内容,所以只搜索缓存的内容TestAppCopy.app。如果找不到TestAppCopy.app,则该函数将其作为明确的“此路径不存在”并且不打算尝试打开目录:

__CFSpinLock(&CFBundleResourceGlobalDataLock);
if (contentsCache) dirDirContents = (CFArrayRef)CFDictionaryGetValue(contentsCache, dirName);
if (dirDirContents) {
    Boolean foundIt = false;
    CFIndex dirDirIdx, dirDirLength = CFArrayGetCount(dirDirContents);
    for (dirDirIdx = 0; !foundIt && dirDirIdx < dirDirLength; dirDirIdx++) if (kCFCompareEqualTo == CFStringCompare(name, CFArrayGetValueAtIndex(dirDirContents, dirDirIdx), kCFCompareCaseInsensitive)) foundIt = true;
    if (!foundIt) tryToOpen = false;
}
__CFSpinUnlock(&CFBundleResourceGlobalDataLock);

因此,内容数组保持为空,为此路径缓存,继续查找。我们现在已经缓存了TestAppCopy.app的(错误的空)内容,并且当查找向下钻入此目录时,我们会不断地访问错误的缓存信息。语言查找在没有找到任何内容时会被刺穿,希望有en.lproj闲置,但我们仍然找不到任何东西,因为我们正在寻找陈旧的缓存。

CoreFoundation包含用于刷新CFBundle缓存的SPI函数。公共API在CoreFoundation中调用它们的唯一位置是__CFBundleDeallocate()。这将刷新有关bundle目录本身的所有缓存信息,但不刷新其父目录:_CFBundleFlushContentsCacheForPath(),它实际上从缓存中删除数据,仅删除与束路径的锚定,不区分大小写搜索匹配的键。

似乎CoreFoundation的客户端可以刷新有关TestApp.app父目录的错误信息的唯一公共方式是将父目录设为捆绑目录(因此TestApp.app与{{1}并存}),为父包目录创建一个CFBundle,然后释放该CFBundle。但是,似乎如果您在刷新之前尝试使用Contents捆绑包时出错,那么关于TestAppCopy.app的错误数据将不会被刷新。

答案 1 :(得分:1)

这听起来像基金会的一个错误。像那样的命令行工具和Cocoa应用程序之间的一个关键区别是运行循环。尝试将上述内容重构为:

@interface Foo:NSObject
@end
@implementation Foo
- (void) doIt { .... your code from main() here .... }
@end

... main(...) {
    Foo *f = [Foo new];
    [f performSelector: @selector(doIt) withObject: nil afterDelay: 0.1 ...];
    [[NSRunLoop currentRunLoop] run];
    return 0; // not reached, I'd bet.
}

看看是否“修复”了它。它可能。它可能不会(显然还有其他几个显着的差异)。在任何情况下,请通过http://bugreport.apple.com/提交错误并将错误#添加为评论。