我注意到在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.appContents并存),为父捆绑目录创建CFBundle,然后释放那个CFBundle。但是,似乎如果您在刷新之前尝试使用TestAppCopy.app软件包时犯了错误,则不会刷新有关TestAppCopy.app的不良数据。

关于objective-c - NSBundle pathForResource在Shell工具中失败,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/3858647/

10-08 23:39