2010-10-04 17 views
5

Ho notato alcuni comportamenti strani con NSBundle quando lo si utilizza in un programma di riga comandi . Se, nel mio programma, prendo un pacchetto esistente e ne faccio una copia e poi provo a usare pathForResource per cercare qualcosa nella cartella Risorse, nil è sempre restituito a meno che il pacchetto che sto cercando esistesse prima del mio programma avviato. Ho creato un'applicazione campione che replica il problema e il codice di riferimento è:NSBundle pathForResource non riuscito nello strumento shell

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

Ai fini di questa applicazione di test, supponiamo che TestApp.app esiste già nella stessa directory come la mia applicazione di test. Se ho eseguito questo, la chiamata 2 ° NSLog sarà in uscita: COPY: (null)

Ora, se io commento la chiamata removeItemAtPath finale nel caso dichiarazione in modo che quando il mio programma esce TestAppCopy.app esiste ancora e quindi rieseguire, il programma funzionerà come previsto.

Ho provato questo in una normale applicazione Cocoa e non riesco a riprodurre il comportamento . Succede solo in un obiettivo dello strumento shell. Qualcuno può pensare a un motivo per cui questo sta fallendo?

BTW: sto cercando questo su 10.6.4 e non ho provato su altri versioni di Mac OS X.

+0

AGGIORNAMENTO: Se trasferisco i miei gruppi di test in un'altra directory oltre a quella in cui si trova il mio strumento shell, tutto funziona correttamente. Quindi, vedo il problema solo se i bundle si trovano nella stessa directory della mia app. Posso lavorare con questo requisito, ma sarebbe bello sapere perché non funziona come ho descritto in origine. – Dustin

+0

Come già detto, per il momento ho lavorato a questo problema, ma sono davvero interessato a capire perché questo sta accadendo e cosa posso fare per evitarlo. – Dustin

risposta

3

Posso confermare che si tratta di un bug in CoreFoundation, non in Foundation. Il bug è dovuto al codice CFBundle che si basa su una cache del contenuto della directory contenente dati obsoleti. Apparentemente il codice presuppone che né le directory del pacchetto né le loro directory madri immediate cambino durante il runtime dell'applicazione.

La chiamata CoreFoundation corrispondente a +[NSBundle pathForResource:ofType:inDirectory:] è CFBundleCopyResourceURLInDirectory() e presenta lo stesso comportamento scorretto. (Questo non è sorprendente, come lo stesso -pathForResource:ofType:inDirectory: utilizza questa chiamata.)

Il problema si trova in definitiva con _CFBundleCopyDirectoryContentsAtPath(). Questo viene chiamato durante il caricamento del pacchetto e durante la ricerca di tutte le risorse. Memorizza nella cache le informazioni sulle directory che cerca in contentsCache.

Ecco il problema: quando arriva il momento di ottenere il contenuto delle TestAppCopy.app, i contenuti memorizzati nella cache della directory contenente TestApp.app non includono TestAppCopy.app. Poiché la cache presenta apparentemente il contenuto di tale directory, vengono ricercati solo i contenuti memorizzati nella cache per TestAppCopy.app. Quando TestAppCopy.app non viene trovato, la funzione prende che come un definitivo "questo percorso non esiste" e non si preoccupa cercando di aprire la directory:

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

Quindi, la matrice contenuti rimane vuoto, viene memorizzata nella cache per questo percorso e la ricerca continua.Ora abbiamo memorizzato nella cache il contenuto (erroneamente vuoto) di TestAppCopy.app, e come drilling di ricerca in questa directory, continuiamo a colpire le informazioni memorizzate nella cache. La ricerca della lingua prende una piega quando non trova nulla e spera che ci sia un en.lproj in giro, ma non troveremo ancora nulla, perché stiamo cercando in una cache stantia.

CoreFoundation include funzioni SPI per svuotare le cache CFBundle. L'unico posto in cui le API pubbliche chiamano in CoreFoundation è __CFBundleDeallocate(). Questo svuota tutte le informazioni memorizzate nella cache della stessa directory del bundle, ma non la sua directory principale: _CFBundleFlushContentsCacheForPath(), che rimuove effettivamente i dati dalla cache, rimuove solo le chiavi che corrispondono a una ricerca ancorata, senza distinzione tra maiuscole e minuscole per il percorso del pacchetto.

Sembrerebbe l'unico modo pubblico un cliente della CoreFoundation potrebbe scovare male informazioni sulla directory padre s' TestApp.app sarebbe quello di rendere la directory padre una directory fascio (così TestApp.app vissuto al fianco di Contents), creare un CFBundle per il pacchetto genitore directory, quindi rilasciare CFBundle. Ma, sembra che se hai commesso l'errore di provare a lavorare con il pacchetto TestAppCopy.app prima di svuotarlo, i dati non validi su TestAppCopy.app non verrebbero svuotati.

+0

Ottima risposta! Grazie. Come ho detto sopra, ho inserito un bug per questo, quindi spero che verrà risolto ad un certo punto. – Dustin

1

Che suona come un bug nella Fondazione. L'unica differenza chiave tra uno strumento da riga di comando come quello e un'applicazione Cocoa è il ciclo di esecuzione. Prova refactoring del sopra in qualcosa di simile:

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

e vedere se questo "correzioni" esso. Potrebbe. Potrebbe non (ci sono un paio di altre differenze significative, ovviamente). In ogni caso, si prega di presentare un bug tramite http://bugreport.apple.com/ e aggiungere il bug # come commento.

+0

Grazie per la risposta bbum. Il bug è stato inserito come rdar: // 8535620. Darò questo premio ancora un paio di giorni e se nessun altro risponde con una risposta, ti assegnerò la taglia. – Dustin

+0

Ho dimenticato di aggiungere che ho provato il tuo suggerimento ma non ha modificato il comportamento. – Dustin

Problemi correlati