Categorie di obiettivi-C nella libreria statica


153

Puoi guidarmi su come collegare correttamente la libreria statica al progetto iPhone. Uso un progetto di libreria statica aggiunto al progetto app come dipendenza diretta (destinazione -> generale -> dipendenze dirette) e tutto funziona bene, ma le categorie. Una categoria definita nella libreria statica non funziona nell'app.

Quindi la mia domanda è come aggiungere una libreria statica con alcune categorie in altri progetti?

E in generale, quali sono le migliori pratiche da utilizzare nel codice di progetto dell'app di altri progetti?


1
beh, ho trovato delle risposte e sembra che questa domanda abbia già avuto risposta qui (mi dispiace averlo perso stackoverflow.com/questions/932856/… )
Vladimir

Risposte:


228

Soluzione: a partire da Xcode 4.2, devi solo andare all'applicazione che si collega alla libreria (non alla libreria stessa) e fare clic sul progetto nel Navigatore progetto, fare clic sulla destinazione dell'app, quindi creare le impostazioni, quindi cercare "Altro Flag linker ", fai clic sul pulsante + e aggiungi" -ObjC ". '-all_load' e '-force_load' non sono più necessari.

Dettagli: ho trovato alcune risposte su vari forum, blog e documenti Apple. Ora provo a fare un breve riassunto delle mie ricerche ed esperimenti.

Il problema è stato causato da (citazione da Domande e risposte tecniche QA1490 di Apple https://developer.apple.com/library/content/qa/qa1490/_index.html ):

Objective-C non definisce i simboli del linker per ciascuna funzione (o metodo, in Objective-C) - invece, i simboli del linker vengono generati solo per ogni classe. Se estendi una classe preesistente a categorie, il linker non sa associare il codice oggetto dell'implementazione della classe principale e dell'implementazione della categoria. Ciò impedisce agli oggetti creati nell'applicazione risultante di rispondere a un selettore definito nella categoria.

E la loro soluzione:

Per risolvere questo problema, la libreria statica dovrebbe passare l'opzione -ObjC al linker. Questo flag fa sì che il linker carichi tutti i file oggetto nella libreria che definisce una classe o categoria Objective-C. Sebbene questa opzione si traduca in genere in un eseguibile più grande (a causa del codice oggetto aggiuntivo caricato nell'applicazione), consentirà la corretta creazione di librerie statiche Objective-C efficaci che contengono categorie su classi esistenti.

e c'è anche una raccomandazione nelle Domande frequenti sullo sviluppo di iPhone:

Come collego tutte le classi Objective-C in una libreria statica? Impostare l'impostazione di creazione Altre bandiere linker su -ObjC.

e descrizioni delle bandiere:

- all_load Carica tutti i membri delle librerie di archivi statici.

- ObjC Carica tutti i membri delle librerie di archivi statici che implementano una classe o categoria Objective-C.

- force_load (path_to_archive) Carica tutti i membri della libreria dell'archivio statico specificata. Nota: -all_load forza il caricamento di tutti i membri di tutti gli archivi. Questa opzione ti consente di scegliere come target un archivio specifico.

* possiamo usare force_load per ridurre le dimensioni binarie delle app ed evitare conflitti che in alcuni casi possono causare all_load.

Sì, funziona con i file * .a aggiunti al progetto. Eppure ho avuto problemi con il progetto lib aggiunto come dipendenza diretta. Ma più tardi ho scoperto che era colpa mia - il progetto di dipendenza diretta probabilmente non è stato aggiunto correttamente. Quando lo rimuovo e aggiungo di nuovo con i passaggi:

  1. Trascina e rilascia il file di progetto lib nel progetto app (o aggiungilo con Progetto-> Aggiungi al progetto ...).
  2. Fare clic sulla freccia sull'icona del progetto lib - nome file mylib.a visualizzato, trascinare questo file mylib.a e rilasciarlo nel gruppo Destinazione -> Collega binario con libreria.
  3. Apri le informazioni sulla destinazione nella prima pagina (Generale) e aggiungi la mia lib all'elenco delle dipendenze

dopodiché tutto funziona bene. Nel mio caso la bandiera "-ObjC" è stata sufficiente.

Mi interessava anche l'idea del http://iphonedevelopmentexperiences.blogspot.com/2010/03/categories-in-static-library.html blog. L'autore dice che può usare la categoria da lib senza impostare -all_load o -ObjC flag. Aggiunge semplicemente alla categoria file h / m un'interfaccia / implementazione di classe fittizia vuota per forzare il linker a utilizzare questo file. E sì, questo trucco fa il lavoro.

Ma l'autore ha anche detto di non aver nemmeno istanziato un oggetto fittizio. Mm ... Come ho scoperto, dovremmo chiamare esplicitamente un codice "reale" dal file di categoria. Quindi almeno la funzione di classe dovrebbe essere chiamata. E non abbiamo nemmeno bisogno di lezioni fittizie. La funzione c singola fa lo stesso.

Quindi se scriviamo file lib come:

// mylib.h
void useMyLib();

@interface NSObject (Logger)
-(void)logSelf;
@end


// mylib.m
void useMyLib(){
    NSLog(@"do nothing, just for make mylib linked");
}


@implementation NSObject (Logger)
-(void)logSelf{
    NSLog(@"self is:%@", [self description]);
}
@end

e se chiamiamo useMyLib (); ovunque nel progetto App quindi in qualsiasi classe possiamo usare il metodo della categoria logSelf;

[self logSelf];

E altri blog sul tema:

http://t-machine.org/index.php/2009/10/13/how-to-make-an-iphone-static-library-part-1/

http://blog.costan.us/2009/12/fat-iphone-static-libraries-device-and.html


8
La nota tecnica di Apple sembra essere stata successivamente modificata per dire "Per risolvere questo problema, il collegamento di destinazione con la libreria statica deve passare l'opzione -ObjC al linker". che è l'opposto di quanto sopra citato. Abbiamo appena confermato che devi includere quando colleghi l'app e non la libreria stessa.
Ken Aspeslagh,

Secondo il documento developer.apple.com/library/mac/#qa/qa1490/_index.html , dovremmo usare il flag -all_load o -force_load. Come accennato, il linker ha un bug nell'app per Mac a 64 bit e nell'app per iPhone. "Importante: per le applicazioni per sistemi operativi a 64 bit e iPhone, esiste un bug del linker che impedisce a -ObjC di caricare file di oggetti da librerie statiche che contengono solo categorie e nessuna classe. La soluzione alternativa è utilizzare i flag -all_load o -force_load."
Robin,

2
@Ken Aspelagh: grazie, ho avuto lo stesso problema. I flag -ObjC e -all_load devono essere aggiunti all'app stessa , non alla libreria.
titaniumdecoy

3
Ottima risposta, anche se i nuovi arrivati ​​a questa domanda dovrebbero notare che ora è obsoleto. Scopri la risposta di tonklon stackoverflow.com/a/9224606/322748 (all_load / force_load non sono più necessari)
Jay Peyer

Mi sono bloccato su queste cose per quasi mezz'ora e con una prova ed errore l'ho appena capito. Comunque grazie. Questa risposta vale +1 e ce l'hai !!!
Deepukjayan,

118

La risposta di Vladimir è in realtà abbastanza buona, tuttavia, vorrei dare qualche conoscenza in più qui. Forse un giorno qualcuno troverà la mia risposta e potrebbe trovarla utile.

Il compilatore trasforma i file di origine (.c, .cc, .cpp, .m) in file di oggetti (.o). C'è un file oggetto per file sorgente. I file oggetto contengono simboli, codice e dati. I file oggetto non sono direttamente utilizzabili dal sistema operativo.

Ora quando si crea una libreria dinamica (.dylib), un framework, un bundle caricabile (.bundle) o un file binario eseguibile, questi file oggetto sono collegati tra loro dal linker per produrre qualcosa che il sistema operativo considera "utilizzabile", ad esempio qualcosa che può caricare direttamente su un indirizzo di memoria specifico.

Tuttavia, quando si crea una libreria statica, tutti questi file oggetto vengono semplicemente aggiunti a un file di archivio di grandi dimensioni, quindi l'estensione delle librerie statiche (.a per l'archivio). Quindi un file .a non è altro che un archivio di file oggetto (.o). Pensa a un archivio TAR o un archivio ZIP senza compressione. È più semplice copiare un singolo file .a in giro rispetto a un intero gruppo di file .o (simile a Java, in cui si comprimono i file .class in un archivio .jar per una facile distribuzione).

Quando si collega un binario a una libreria statica (= archivio), il linker otterrà una tabella di tutti i simboli nell'archivio e controllerà quali di questi simboli fanno riferimento ai binari. Solo i file oggetto contenenti simboli di riferimento vengono effettivamente caricati dal linker e vengono considerati dal processo di collegamento. Ad esempio, se il tuo archivio ha 50 file oggetto, ma solo 20 contengono simboli usati dal binario, solo quelli 20 vengono caricati dal linker, gli altri 30 vengono completamente ignorati nel processo di collegamento.

Funziona abbastanza bene per il codice C e C ++, poiché questi linguaggi cercano di fare il più possibile in fase di compilazione (sebbene C ++ abbia anche alcune funzionalità di runtime). Obj-C, tuttavia, è un diverso tipo di linguaggio. Obj-C dipende fortemente dalle funzionalità di runtime e molte funzionalità di Obj-C sono in realtà solo funzioni di runtime. Le classi Obj-C in realtà hanno simboli paragonabili alle funzioni C o alle variabili C globali (almeno nel runtime Obj-C corrente). Un linker può vedere se una classe è referenziata o meno, quindi può determinare se una classe è in uso o meno. Se si utilizza una classe da un file oggetto in una libreria statica, questo file oggetto verrà caricato dal linker perché il linker vede un simbolo in uso. Le categorie sono solo funzionalità di runtime, le categorie non sono simboli come le classi o le funzioni e ciò significa anche che un linker non può determinare se una categoria è in uso o meno.

Se il linker carica un file oggetto contenente il codice Obj-C, tutte le sue parti Obj-C fanno sempre parte della fase di collegamento. Quindi se un file oggetto contenente categorie viene caricato perché qualsiasi simbolo da esso considerato viene considerato "in uso" (sia esso una classe, sia una funzione, sia una variabile globale), anche le categorie vengono caricate e saranno disponibili in fase di esecuzione . Tuttavia, se il file oggetto stesso non viene caricato, le categorie in esso contenute non saranno disponibili in fase di esecuzione. Un file oggetto contenente solo categorie non viene mai caricato perché non contiene simboli che il linker considererebbe mai "in uso". E questo è l'intero problema qui.

Sono state proposte diverse soluzioni e ora che sai come tutto ciò gioca insieme, diamo un altro sguardo alla soluzione proposta:

  1. Una soluzione è aggiungere -all_loadalla chiamata del linker. Cosa farà effettivamente quella bandiera linker? In realtà dice al linker il seguente " Carica tutti i file oggetto di tutti gli archivi a prescindere se vedi un simbolo in uso o meno ". Naturalmente, funzionerà, ma può anche produrre binari piuttosto grandi.

  2. Un'altra soluzione è quella di aggiungere -force_loadalla chiamata del linker incluso il percorso dell'archivio. Questo flag funziona esattamente come -all_load, ma solo per l'archivio specificato. Naturalmente funzionerà anche questo.

  3. La soluzione più popolare è quella di aggiungere -ObjCalla chiamata del linker. Cosa farà effettivamente quella bandiera linker? Questo flag indica al linker " Carica tutti i file oggetto da tutti gli archivi se vedi che contengono un codice Obj-C ". E "qualsiasi codice Obj-C" include categorie. Anche questo funzionerà e non forzerà il caricamento di file oggetto che non contengono codice Obj-C (questi sono ancora caricati solo su richiesta).

  4. Un'altra soluzione è l'impostazione di build Xcode piuttosto nuova Perform Single-Object Prelink. Cosa farà questa impostazione? Se abilitato, tutti i file oggetto (ricorda, ce n'è uno per file sorgente) vengono uniti in un singolo file oggetto (che non è un vero collegamento, da cui il nome PreLink ) e questo singolo file oggetto (a volte chiamato anche "oggetto master" file ") viene quindi aggiunto all'archivio. Se ora viene considerato in uso qualsiasi simbolo del file oggetto master, viene considerato in uso l'intero file oggetto master e quindi tutte le sue parti Objective-C vengono sempre caricate. E poiché le classi sono simboli normali, è sufficiente utilizzare una singola classe da una libreria statica per ottenere anche tutte le categorie.

  5. La soluzione finale è il trucco che Vladimir ha aggiunto proprio alla fine della sua risposta. Inserisci un " simbolo falso " in qualsiasi file sorgente che dichiari solo categorie. Se si desidera utilizzare una delle categorie in fase di esecuzione, assicurarsi di fare in qualche modo riferimento al simbolo falso in fase di compilazione, poiché ciò causa il caricamento del file oggetto da parte del linker e quindi anche tutto il codice Obj-C in esso. Ad esempio potrebbe essere una funzione con un corpo di funzione vuoto (che non farà nulla quando viene chiamato) o potrebbe essere una variabile globale a cui si accede (ad esempio un globaleintuna volta letto o una volta scritto, questo è sufficiente). A differenza di tutte le altre soluzioni sopra, questa soluzione sposta il controllo su quali categorie sono disponibili in fase di esecuzione al codice compilato (se vuole che siano collegate e disponibili, accede al simbolo, altrimenti non accede al simbolo e il linker ignorerà esso).

È tutto gente.

Oh, aspetta, c'è un'altra cosa:
il linker ha un'opzione chiamata -dead_strip. Cosa fa questa opzione? Se il linker ha deciso di caricare un file oggetto, tutti i simboli del file oggetto diventano parte del file binario collegato, indipendentemente dal fatto che vengano utilizzati o meno. Ad esempio, un file oggetto contiene 100 funzioni, ma solo una di esse viene utilizzata dal file binario, tutte le 100 funzioni vengono comunque aggiunte al file binario perché i file oggetto vengono aggiunti nel loro insieme o non vengono aggiunti affatto. L'aggiunta parziale di un file oggetto non è generalmente supportata dai linker.

Tuttavia, se dici al linker di "dead strip", il linker prima aggiungerà tutti i file oggetto al binario, risolverà tutti i riferimenti e infine scansionerà il binario per simboli non in uso (o solo in uso da altri simboli non in uso). Tutti i simboli trovati non in uso vengono quindi rimossi come parte della fase di ottimizzazione. Nell'esempio sopra, le 99 funzioni non utilizzate vengono nuovamente rimosse. Questo è molto utile se usi opzioni come -load_all, -force_loado Perform Single-Object Prelinkperché queste opzioni possono facilmente far esplodere le dimensioni binarie in alcuni casi e lo stripping morto rimuoverà di nuovo codice e dati inutilizzati.

Dead stripping funziona molto bene per il codice C (ad es. Le funzioni non utilizzate, le variabili e le costanti vengono rimosse come previsto) e funziona anche abbastanza bene per C ++ (ad es. Le classi non utilizzate vengono rimosse). Non è perfetto, in alcuni casi alcuni simboli non vengono rimossi anche se sarebbe bene rimuoverli, ma nella maggior parte dei casi funziona abbastanza bene per queste lingue.

Che dire di Obj-C? Dimenticalo! Non esiste uno stripping morto per Obj-C. Poiché Obj-C è un linguaggio di funzionalità runtime, il compilatore non può dire in fase di compilazione se un simbolo è realmente in uso o meno. Ad esempio una classe Obj-C non è in uso se non esiste un codice che fa riferimento direttamente ad essa, giusto? Sbagliato! È possibile creare dinamicamente una stringa contenente un nome di classe, richiedere un puntatore di classe per quel nome e allocare dinamicamente la classe. Ad esempio invece di

MyCoolClass * mcc = [[MyCoolClass alloc] init];

Potrei anche scrivere

NSString * cname = @"CoolClass";
NSString * cnameFull = [NSString stringWithFormat:@"My%@", cname];
Class mmcClass = NSClassFromString(cnameFull);
id mmc = [[mmcClass alloc] init];

In entrambi i casi mmcè un riferimento a un oggetto della classe "MyCoolClass", ma non vi è alcun riferimento diretto a questa classe nel secondo esempio di codice (nemmeno il nome della classe come stringa statica). Tutto accade solo in fase di esecuzione. E questo anche se le classi sono in realtà simboli reali. È ancora peggio per le categorie, in quanto non sono nemmeno simboli reali.

Quindi, se hai una libreria statica con centinaia di oggetti, ma la maggior parte dei tuoi binari ne ha bisogno solo alcuni, potresti preferire non usare le soluzioni da (1) a (4) sopra. Altrimenti si finisce con binari molto grandi che contengono tutte queste classi, anche se la maggior parte di esse non viene mai utilizzata. Per le classi di solito non hai bisogno di alcuna soluzione speciale poiché le classi hanno simboli reali e fintanto che le fai riferimento direttamente (non come nel secondo esempio di codice), il linker identificherà il loro uso abbastanza bene da solo. Per le categorie, tuttavia, considera la soluzione (5), in quanto consente di includere solo le categorie di cui hai veramente bisogno.

Ad esempio, se si desidera una categoria per NSData, ad esempio aggiungendo un metodo di compressione / decompressione, è necessario creare un file di intestazione:

// NSData+Compress.h
@interface NSData (Compression)
    - (NSData *)compressedData;
    - (NSData *)decompressedData;
@end

void import_NSData_Compression ( );

e un file di implementazione

// NSData+Compress
@implementation NSData (Compression)
    - (NSData *)compressedData 
    {
        // ... magic ...
    }

    - (NSData *)decompressedData
    {
        // ... magic ...
    }
@end

void import_NSData_Compression ( ) { }

Ora assicurati solo che import_NSData_Compression()venga chiamato ovunque nel tuo codice . Non importa dove viene chiamato o quanto spesso viene chiamato. In realtà non deve essere chiamato affatto, è sufficiente che il linker la pensi così. Ad esempio, è possibile inserire il seguente codice in qualsiasi punto del progetto:

__attribute__((used)) static void importCategories ()
{
    import_NSData_Compression();
    // add more import calls here
}

Non devi mai chiamare il importCategories()tuo codice, l'attributo farà credere al compilatore e al linker che sia chiamato, anche nel caso in cui non lo sia.

E un suggerimento finale:
se aggiungi -whyloadalla chiamata del collegamento finale, il linker stampa nel registro di build quale file oggetto da quale libreria ha caricato a causa del simbolo in uso. Stampa solo il primo simbolo considerato in uso, ma non è necessariamente l'unico simbolo in uso di quel file oggetto.


1
Grazie per averlo menzionato -whyload, provare a eseguire il debug del motivo per cui il linker sta facendo qualcosa può essere piuttosto difficile!
Ben S,

C'è un'opzione Dead Code Strippingin Build Settings>Linking. È uguale a quello -dead_stripaggiunto in Other Linker Flags?
Xiao,

1
@Sean Sì, è lo stesso. Basta leggere il "Quick Help" che esiste per ogni impostazione di build, la risposta è proprio qui: postimg.org/image/n7megftnr/full
Mecki

@Mecki Grazie. Ho provato a sbarazzarmi di -ObjC, quindi ho provato il tuo hack ma si lamenta "import_NSString_jsonObject()", referenced from: importCategories() in main.o ld: symbol(s) not found. Ho inserito il import_NSString_jsonObjectmio Framework incorporato con nome Utilitye l' ho aggiunto #import <Utility/Utility.h>con una __attribute__dichiarazione alla fine del mio AppDelegate.h.
Xiao,

@Sean Se il linker non riesce a trovare il simbolo, non si sta collegando alla libreria statica che contiene il simbolo. L'importazione di un file ah da un framework non renderà il collegamento Xcode al framework. Il framework deve essere esplicitamente collegato al collegamento con la fase di creazione dei framework. Potresti voler aprire una tua domanda per il tuo problema di collegamento, rispondere nei commenti è ingombrante e non puoi anche fornire informazioni come l'output del registro di build.
Mecki,

24

Questo problema è stato risolto in LLVM . La correzione viene fornita come parte di LLVM 2.9 La prima versione di Xcode a contenere la correzione è Xcode 4.2 fornito con LLVM 3.0. L'uso di -all_loado -force_loadnon è più necessario quando si lavora con XCode 4.2 -ObjC è ancora necessario.


Sei sicuro di questo? Sto lavorando a un progetto iOS usando Xcode 4.3.2, compilando con LLVM 3.1 e questo è stato ancora un problema per me.
Ashley Mills,

Ok, è stato un po 'impreciso. La -ObjCbandiera è ancora necessaria e lo sarà sempre. La soluzione alternativa era l'uso di -all_loado -force_load. E questo non è più necessario. Ho corretto la mia risposta sopra.
Tonklon,

C'è qualche svantaggio nell'includere il flag -all_load (anche se non è necessario)? Influisce in qualche modo sui tempi di compilazione / avvio?
ZS

Sto lavorando con Xcode versione 4.5 (4G182) e il flag -ObjC sposta il mio errore di selettore non riconosciuto dalla dipendenza di terze parti che sto cercando di utilizzare in quello che assomiglia alle profondità del runtime di Objective C: "- [__ NSArrayM map :]: selettore non riconosciuto inviato all'istanza ... ". Qualche indizio?
Robert Atkins,

16

Ecco cosa devi fare per risolvere completamente questo problema quando compili la tua libreria statica:

Andare su Impostazioni build Xcode e impostare Esegui preludio oggetto singolo su YES o GENERATE_MASTER_OBJECT_FILE = YESnel file di configurazione della build.

Per impostazione predefinita, il linker genera un file .o per ogni file .m. Quindi le categorie ottengono file .o diversi. Quando il linker esamina i file .o di una libreria statica, non crea un indice di tutti i simboli per classe (il runtime lo farà, non importa quale).

Questa direttiva chiederà al linker di raggruppare tutti gli oggetti in un unico grande file .o e in questo modo impone al linker che elabora la libreria statica di ottenere l'indicizzazione di tutte le categorie di classe.

Spero che questo lo chiarisca.


Questo mi ha risolto senza dover aggiungere -ObjC alla destinazione del collegamento.
Matthew Crenshaw,

Dopo l'aggiornamento all'ultima versione della libreria BlocksKit , ho dovuto utilizzare questa impostazione per risolvere il problema (stavo già usando il flag -ObjC ma continuavo a vedere il problema).
Rakmoh,

1
In realtà la tua risposta non è del tutto giusta. Non "chiedo al linker di raggruppare tutte le categorie della stessa classe insieme in un unico file .o", chiede al linker di collegare tutti i file oggetto (.o) in un singolo file oggetto grande prima di creare una libreria statica da li / lo. Dopo aver fatto riferimento a qualsiasi simbolo dalla libreria, vengono caricati tutti i simboli. Tuttavia, questo non funzionerà se non viene fatto riferimento a nessun simbolo (ad es. Se non funzionerà se ci sono solo categorie nella libreria).
Mecki

Non penso che funzionerà se aggiungi categorie a classi esistenti, come NSData.
Bob Whiteman,

Anch'io ho problemi ad aggiungere categorie alle classi esistenti. Il mio plugin non li riconosce in fase di esecuzione.
David Dunham,

9

Un fattore che viene raramente menzionato ogni volta che si presenta la discussione sul collegamento della libreria statica è il fatto che è necessario includere anche le categorie stesse nelle fasi di compilazione-> copiare i file e compilare le fonti della libreria statica stessa .

Inoltre, Apple non sottolinea questo fatto nemmeno nelle loro Librerie statiche su iOS pubblicate di recente .

Ho passato un'intera giornata a provare ogni sorta di variazione di -objC e -all_load ecc. Ma non ne è uscito nulla .. questa domanda ha portato questo problema alla mia attenzione. (Non fraintendetemi .. dovete ancora fare le cose -objC .. ma è molto più di questo).

anche un'altra azione che mi ha sempre aiutato è che ho sempre creato la libreria statica inclusa prima da sola .. poi ho creato l'applicazione allegata ..


-1

Probabilmente devi avere la categoria nell'intestazione "pubblica" della libreria statica: #import "MyStaticLib.h"

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.