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:
Una soluzione è aggiungere -all_load
alla 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.
Un'altra soluzione è quella di aggiungere -force_load
alla 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.
La soluzione più popolare è quella di aggiungere -ObjC
alla 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).
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.
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 globaleint
una 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_load
o Perform Single-Object Prelink
perché 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 -whyload
alla 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.