2010-10-04 8 views
5

Zauważyłem pewne dziwne zachowanie z NSBundle podczas korzystania z niego w programie wiersza poleceń . Jeśli w moim programie pobieram istniejący pakiet i robię jego kopię, a następnie spróbuję użyć pathForResource do sprawdzenia, czy coś jest w folderze Zasoby, to jest ono zawsze zwracane, chyba że pakiet znajdował się przed moim uruchomiono program. I stworzył aplikację przykładowy, który replikuje problem i odpowiedni kod jest:NSBundle pathForResource zawodzi w narzędziu powłoki

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]; 
} 

Dla celów tej aplikacji testowej, załóżmy, że TestApp.app już istnieje w tym samym katalogu co mój aplikacji testowej. Jeśli uruchomię to, wywołanie 2-ty NSLog wyświetli: COPY: (null)

Teraz, jeśli mogę wypowiedzieć się na ostateczną rozmowę removeItemAtPath w razie rachunku tak, że kiedy mój program wychodzi TestAppCopy.app nadal istnieje i następnie uruchom ponownie, program będzie działać zgodnie z oczekiwaniami.

Próbowałem tego w normalnej aplikacji kakao i nie mogę odtworzyć tego zachowania w postaci . Dzieje się tak tylko w celu narzędzia powłoki. Czy ktoś może pomyśleć o przyczynie niepowodzenia?

BTW: Próbuję to na 10.6.4 i nie próbowałem na innych wersji Mac OS X.

+0

AKTUALIZACJA: Jeśli przeniesię moje pakiety testowe do innego katalogu niż ten, w którym znajduje się moje narzędzie powłoki, wszystko działa poprawnie. Widzę więc problem tylko wtedy, gdy pakiety znajdują się w tym samym katalogu co moja aplikacja. Mogę pracować z tym wymaganiem, ale dobrze byłoby wiedzieć, dlaczego nie działa tak, jak opisałem pierwotnie. – Dustin

+0

Jak już wspomniano, pracowałem nad tym jak na razie, ale jestem naprawdę zainteresowany zrozumieniem, dlaczego tak się dzieje i co mogę zrobić, aby temu zapobiec. – Dustin

Odpowiedz

3

Mogę potwierdzić, że jest to błąd w CoreFoundation, a nie w Foundation. Błąd wynika z kodu CFBundle polegającego na pamięci podręcznej zawartości katalogu zawierającej nieaktualne dane. Kod najwyraźniej zakłada, że ​​ani katalogi pakietów, ani ich bezpośrednie katalogi nadrzędne nie ulegną zmianie w czasie wykonywania aplikacji.

Wywołanie CoreFoundation odpowiadające +[NSBundle pathForResource:ofType:inDirectory:] jest CFBundleCopyResourceURLInDirectory() i wykazuje takie samo niewłaściwe zachowanie. (Nie jest to zaskakujące, ponieważ samo to używa -pathForResource:ofType:inDirectory:).

Problem ostatecznie leży w _CFBundleCopyDirectoryContentsAtPath(). Jest to wywoływane podczas ładowania pakietu i podczas wszystkich wyszukiwań zasobów. Przechowuje w pamięci podręcznej informacje o katalogach, które wyszukuje w contentsCache.

Oto problem: Gdy przychodzi czas na uzyskanie zawartości TestAppCopy.app, zawartość pamięci podręcznej katalogu zawierającego TestApp.app nie obejmuje TestAppCopy.app. Ponieważ pamięć podręczna zawiera zawartość tego katalogu, wyszukiwane są tylko treści w pamięci podręcznej dla TestAppCopy.app. Kiedy TestAppCopy.app nie zostanie znaleziony, funkcja przyjmuje, że jako ostateczne „Ta ścieżka nie istnieje” i nie przeszkadza próbuje otworzyć katalog:

__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); 

Więc tablica zawartość pozostaje pusta, za to dostaje buforowane ścieżka i kontynuacja wyszukiwania.Teraz zapisaliśmy w pamięci podręcznej (niepoprawnie pustą) zawartość TestAppCopy.app, a podczas wyszukiwania wierszy w tym katalogu nadal trafiamy na źle zbuforowane informacje. Szukanie języka powoduje ukłucie, gdy nic nie znajduje i ma nadzieję, że w pobliżu kręci się en.lproj, ale wciąż nie znajdziemy niczego, ponieważ szukamy w nieaktualnej pamięci podręcznej.

CoreFoundation zawiera funkcje SPI do opróżniania buforów CFBundle. Jedynym miejscem, w którym publiczne API wywołuje je w CoreFoundation, jest __CFBundleDeallocate(). Spowoduje to opróżnienie wszystkich zapisanych w pamięci podręcznej informacji o samym katalogu pakietu, ale nie o jego katalogu nadrzędnym: _CFBundleFlushContentsCacheForPath(), który faktycznie usuwa dane z pamięci podręcznej, usuwa tylko klucze pasujące do zakotwiczonego, niewrażliwego na wielkość znaków, wyszukiwania ścieżki pakietu.

Wydaje się, że jedynym sposobem publicznego klientem CoreFoundation można opróżnić złe informacje o katalogu nadrzędnego TestApp.app „s byłoby zrobić katalogu nadrzędnego katalogu Bundle (tak TestApp.app żyli obok Contents) utwórz CFBundle dla wiązki dominującej katalogu, a następnie zwolnij tę CFBundle. Wydaje się jednak, że jeśli popełnisz błąd, próbując pracować z pakietem TestAppCopy.app przed jego przepłukaniem, złe dane o numerze TestAppCopy.app nie zostaną przepłukane.

+0

Świetna odpowiedź! Dzięki. Jak już wspomniałem, wprowadziłem do niego błąd, więc mam nadzieję, że w pewnym momencie zostanie on naprawiony. – Dustin

1

To brzmi jak błąd w Fundacji. Jedną z kluczowych różnic między narzędziem wiersza poleceń, takim jak to, a aplikacją Cocoa, jest pętla uruchamiania. Spróbuj refakturować powyższe w coś takiego:

@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. 
} 

Sprawdź, czy to "naprawia". Może. Może nie (oczywiście jest kilka innych znaczących różnic). W każdym razie prosimy o zgłoszenie błędu poprzez http://bugreport.apple.com/ i dodanie błędu # jako komentarza.

+0

Dzięki za odpowiedź bbum. Błąd został wprowadzony jako rdar: // 8535620. Dam to hojnie kilka dni i jeśli nikt inny nie odpowie z odpowiedzią, nagrodzę cię nagrodą. – Dustin

+0

Zapomniałem dodać, że próbowałem Twojej sugestii, ale nie zmieniło to zachowania. – Dustin

Powiązane problemy