我注意到在NSBundle中使用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 :(空)
现在,如果我注释掉if中最后的removeItemAtPath调用
语句,以便当我的程序退出时TestAppCopy.app仍然存在
然后重新运行,该程序将按预期运行。
我已经在普通的 cocoa 应用程序中尝试过此方法,但无法复制
行为。它仅在shell工具目标中发生。
谁能想到失败的原因吗?
顺便说一句:我正在10.6.4上尝试过,但没有尝试过其他任何功能
版本的Mac OSX。
最佳答案
我可以确认这是CoreFoundation中的错误,而不是Foundation中的错误。该错误是由于CFBundle代码依赖于包含过时数据的目录内容缓存所致。该代码显然假定在应用程序运行时,捆绑包目录或其直接父目录都不会更改。
与+[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包含SPI函数以刷新CFBundle缓存。在CoreFoundation中唯一对它们进行公共(public)API调用的地方是
__CFBundleDeallocate()
。这将刷新所有与包目录本身有关的已缓存信息,但不刷新其父目录:_CFBundleFlushContentsCacheForPath()
,实际上从缓存中删除数据,仅删除与 anchor 定的,不区分大小写的包路径搜索匹配的键。似乎CoreFoundation客户端清除有关
TestApp.app
父目录的错误信息的唯一公共(public)方法是将父目录设为捆绑目录(因此TestApp.app
与Contents
并存),为父捆绑目录创建CFBundle,然后释放那个CFBundle。但是,似乎如果您在刷新之前尝试使用TestAppCopy.app
软件包时犯了错误,则不会刷新有关TestAppCopy.app
的不良数据。关于objective-c - NSBundle pathForResource在Shell工具中失败,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/3858647/