我注意到在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的版本。答案 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/提交错误并将错误#添加为评论。