Die Antwort von Vladimir ist eigentlich ziemlich gut, aber ich möchte hier etwas mehr Hintergrundwissen geben. Vielleicht findet eines Tages jemand meine Antwort und findet sie hilfreich.
Der Compiler wandelt Quelldateien (.c, .cc, .cpp, .m) in Objektdateien (.o) um. Es gibt eine Objektdatei pro Quelldatei. Objektdateien enthalten Symbole, Code und Daten. Objektdateien können vom Betriebssystem nicht direkt verwendet werden.
Beim Erstellen einer dynamischen Bibliothek (.dylib), eines Frameworks, eines ladbaren Bundles (.bundle) oder einer ausführbaren Binärdatei werden diese Objektdateien vom Linker miteinander verknüpft, um etwas zu erzeugen, das das Betriebssystem als "verwendbar" erachtet, z. B. etwas, das es kann direkt in eine bestimmte Speicheradresse laden.
Beim Erstellen einer statischen Bibliothek werden jedoch alle diese Objektdateien einfach zu einer großen Archivdatei hinzugefügt, daher die Erweiterung der statischen Bibliotheken (.a für Archiv). Eine .a-Datei ist also nichts anderes als ein Archiv von Objektdateien (.o). Stellen Sie sich ein TAR-Archiv oder ein ZIP-Archiv ohne Komprimierung vor. Es ist einfach einfacher, eine einzelne .a-Datei zu kopieren als eine ganze Reihe von .o-Dateien (ähnlich wie bei Java, wo Sie .class-Dateien zur einfachen Verteilung in ein .jar-Archiv packen).
Beim Verknüpfen einer Binärdatei mit einer statischen Bibliothek (= Archiv) erhält der Linker eine Tabelle aller Symbole im Archiv und überprüft, auf welche dieser Symbole die Binärdateien verweisen. Nur die Objektdateien, auf die verwiesen wird, werden vom Linker tatsächlich geladen und vom Verknüpfungsprozess berücksichtigt. Wenn Ihr Archiv beispielsweise 50 Objektdateien enthält, aber nur 20 Symbole enthalten, die von der Binärdatei verwendet werden, werden nur die 20 vom Linker geladen, die anderen 30 werden beim Verknüpfungsprozess vollständig ignoriert.
Dies funktioniert recht gut für C- und C ++ - Code, da diese Sprachen versuchen, beim Kompilieren so viel wie möglich zu tun (obwohl C ++ auch einige Nur-Laufzeit-Funktionen bietet). Obj-C ist jedoch eine andere Art von Sprache. Obj-C hängt stark von den Laufzeitfunktionen ab, und viele Obj-C-Funktionen sind eigentlich nur Laufzeitfunktionen. Obj-C-Klassen haben tatsächlich Symbole, die mit C-Funktionen oder globalen C-Variablen vergleichbar sind (zumindest in der aktuellen Obj-C-Laufzeit). Ein Linker kann sehen, ob auf eine Klasse verwiesen wird oder nicht, sodass er bestimmen kann, welche Klasse verwendet wird oder nicht. Wenn Sie eine Klasse aus einer Objektdatei in einer statischen Bibliothek verwenden, wird diese Objektdatei vom Linker geladen, da der Linker ein verwendetes Symbol sieht. Kategorien sind nur zur Laufzeit verfügbar. Kategorien sind keine Symbole wie Klassen oder Funktionen. Dies bedeutet auch, dass ein Linker nicht feststellen kann, ob eine Kategorie verwendet wird oder nicht.
Wenn der Linker eine Objektdatei mit Obj-C-Code lädt, sind alle Obj-C-Teile immer Teil der Verknüpfungsstufe. Wenn also eine Objektdatei mit Kategorien geladen wird, weil ein Symbol daraus als "in Verwendung" betrachtet wird (sei es eine Klasse, sei es eine Funktion, sei es eine globale Variable), werden die Kategorien ebenfalls geladen und sind zur Laufzeit verfügbar . Wenn die Objektdatei selbst jedoch nicht geladen wird, sind die darin enthaltenen Kategorien zur Laufzeit nicht verfügbar. Eine Objektdatei, die nur Kategorien enthält, wird niemals geladen, da sie keine Symbole enthält, die der Linker jemals als "in Verwendung" betrachten würde. Und das ist das ganze Problem hier.
Es wurden mehrere Lösungen vorgeschlagen. Nachdem Sie nun wissen, wie dies alles zusammenspielt, werfen wir einen weiteren Blick auf die vorgeschlagene Lösung:
Eine Lösung besteht darin -all_load
, dem Linker-Aufruf etwas hinzuzufügen . Was wird diese Linker-Flagge tatsächlich tun? Tatsächlich teilt es dem Linker Folgendes mit: " Alle Objektdateien aller Archive laden, unabhängig davon, ob ein Symbol verwendet wird oder nicht ". Das funktioniert natürlich, kann aber auch ziemlich große Binärdateien erzeugen.
Eine andere Lösung besteht darin -force_load
, dem Linker-Aufruf den Pfad zum Archiv hinzuzufügen . Dieses Flag funktioniert genauso -all_load
, jedoch nur für das angegebene Archiv. Das wird natürlich auch funktionieren.
Die beliebteste Lösung ist das Hinzufügen -ObjC
zum Linker-Aufruf. Was wird diese Linker-Flagge tatsächlich tun? Dieses Flag teilt dem Linker mit, dass alle Objektdateien aus allen Archiven geladen werden sollen, wenn Sie feststellen, dass sie Obj-C-Code enthalten . Und "jeder Obj-C-Code" enthält Kategorien. Dies funktioniert auch und erzwingt nicht das Laden von Objektdateien, die keinen Obj-C-Code enthalten (diese werden immer noch nur bei Bedarf geladen).
Eine andere Lösung ist die ziemlich neue Xcode-Build-Einstellung Perform Single-Object Prelink
. Was macht diese Einstellung? Wenn diese Option aktiviert ist, werden alle Objektdateien (denken Sie daran, es gibt eine pro Quelldatei) zu einer einzelnen Objektdatei (die keine echte Verknüpfung darstellt, daher der Name PreLink ) und dieser einzelnen Objektdatei (manchmal auch als "Master-Objekt" bezeichnet) zusammengeführt Datei ") wird dann zum Archiv hinzugefügt. Wenn nun ein Symbol der Master-Objektdatei als verwendet betrachtet wird, wird die gesamte Master-Objektdatei als verwendet betrachtet und somit werden alle Objective-C-Teile davon immer geladen. Und da Klassen normale Symbole sind, reicht es aus, eine einzelne Klasse aus einer solchen statischen Bibliothek zu verwenden, um auch alle Kategorien abzurufen.
Die endgültige Lösung ist der Trick, den Vladimir ganz am Ende seiner Antwort hinzugefügt hat. Platzieren Sie ein " falsches Symbol " in einer Quelldatei, die nur Kategorien deklariert. Wenn Sie zur Laufzeit eine der Kategorien verwenden möchten, stellen Sie sicher, dass Sie zur Kompilierungszeit auf das gefälschte Symbol verweisen , da dies dazu führt, dass die Objektdatei vom Linker und damit auch der gesamte darin enthaltene Obj-C-Code geladen wird. Beispielsweise könnte es sich um eine Funktion mit einem leeren Funktionskörper handeln (die beim Aufruf nichts bewirkt), oder es kann sich um eine globale Variable handeln, auf die zugegriffen wird (z. B. eine globale Variable)int
einmal gelesen oder einmal geschrieben, ist dies ausreichend). Im Gegensatz zu allen anderen oben genannten Lösungen verlagert diese Lösung die Kontrolle darüber, welche Kategorien zur Laufzeit verfügbar sind, auf den kompilierten Code (wenn sie verknüpft und verfügbar sein sollen, greift sie auf das Symbol zu, andernfalls greift sie nicht auf das Symbol zu und der Linker ignoriert sie es).
Das war's Leute.
Oh, warte, es gibt noch eine Sache:
Der Linker hat eine Option namens -dead_strip
. Was macht diese Option? Wenn der Linker beschlossen hat, eine Objektdatei zu laden, werden alle Symbole der Objektdatei Teil der verknüpften Binärdatei, unabhängig davon, ob sie verwendet werden oder nicht. Beispiel: Eine Objektdatei enthält 100 Funktionen, aber nur eine davon wird von der Binärdatei verwendet. Alle 100 Funktionen werden weiterhin zur Binärdatei hinzugefügt, da Objektdateien entweder als Ganzes oder gar nicht hinzugefügt werden. Das teilweise Hinzufügen einer Objektdatei wird von Linkern normalerweise nicht unterstützt.
Wenn Sie dem Linker jedoch "Dead Strip" mitteilen, fügt der Linker zuerst alle Objektdateien zur Binärdatei hinzu, löst alle Referenzen auf und durchsucht die Binärdatei schließlich nach Symbolen, die nicht verwendet werden (oder nur von anderen Symbolen verwendet werden, die nicht verwendet werden) verwenden). Alle nicht verwendeten Symbole werden dann im Rahmen der Optimierungsphase entfernt. Im obigen Beispiel werden die 99 nicht verwendeten Funktionen wieder entfernt. Dies ist sehr nützlich , wenn Sie Optionen verwenden , wie -load_all
, -force_load
oder Perform Single-Object Prelink
weil diese Optionen können leicht binäre Größen dramatisch in einigen Fällen und die Toten Strippen wieder nicht verwendeten Code und Daten entfernen sprengen.
Dead Stripping funktioniert sehr gut für C-Code (z. B. werden nicht verwendete Funktionen, Variablen und Konstanten wie erwartet entfernt) und funktioniert auch für C ++ recht gut (z. B. werden nicht verwendete Klassen entfernt). Es ist nicht perfekt, in einigen Fällen werden einige Symbole nicht entfernt, obwohl es in Ordnung wäre, sie zu entfernen, aber in den meisten Fällen funktioniert es für diese Sprachen recht gut.
Was ist mit Obj-C? Vergiss es! Für Obj-C gibt es kein totes Strippen. Da Obj-C eine Laufzeit-Feature-Sprache ist, kann der Compiler zur Kompilierungszeit nicht sagen, ob ein Symbol wirklich verwendet wird oder nicht. ZB wird eine Obj-C-Klasse nicht verwendet, wenn kein Code vorhanden ist, der direkt darauf verweist, richtig? Falsch! Sie können dynamisch eine Zeichenfolge erstellen, die einen Klassennamen enthält, einen Klassenzeiger für diesen Namen anfordern und die Klasse dynamisch zuweisen. ZB statt
MyCoolClass * mcc = [[MyCoolClass alloc] init];
Ich könnte auch schreiben
NSString * cname = @"CoolClass";
NSString * cnameFull = [NSString stringWithFormat:@"My%@", cname];
Class mmcClass = NSClassFromString(cnameFull);
id mmc = [[mmcClass alloc] init];
In beiden Fällen mmc
handelt es sich um einen Verweis auf ein Objekt der Klasse "MyCoolClass", im zweiten Codebeispiel gibt es jedoch keinen direkten Verweis auf diese Klasse (nicht einmal den Klassennamen als statische Zeichenfolge). Alles passiert nur zur Laufzeit. Und das ist , obwohl Klassen sind tatsächlich echte Symbole. Für Kategorien ist es noch schlimmer, da es sich nicht einmal um echte Symbole handelt.
Wenn Sie also eine statische Bibliothek mit Hunderten von Objekten haben, die meisten Ihrer Binärdateien jedoch nur wenige benötigen, ziehen Sie es möglicherweise vor, die obigen Lösungen (1) bis (4) nicht zu verwenden. Andernfalls erhalten Sie sehr große Binärdateien, die alle diese Klassen enthalten, obwohl die meisten von ihnen nie verwendet werden. Für Klassen benötigen Sie normalerweise überhaupt keine spezielle Lösung, da Klassen echte Symbole haben. Solange Sie diese direkt referenzieren (nicht wie im zweiten Codebeispiel), erkennt der Linker ihre Verwendung ziemlich gut. Berücksichtigen Sie für Kategorien jedoch Lösung (5), da nur die Kategorien berücksichtigt werden können, die Sie wirklich benötigen.
Wenn Sie beispielsweise eine Kategorie für NSData möchten, z. B. eine Komprimierungs- / Dekomprimierungsmethode hinzufügen, erstellen Sie eine Header-Datei:
// NSData+Compress.h
@interface NSData (Compression)
- (NSData *)compressedData;
- (NSData *)decompressedData;
@end
void import_NSData_Compression ( );
und eine Implementierungsdatei
// NSData+Compress
@implementation NSData (Compression)
- (NSData *)compressedData
{
// ... magic ...
}
- (NSData *)decompressedData
{
// ... magic ...
}
@end
void import_NSData_Compression ( ) { }
Stellen Sie jetzt sicher, dass irgendwo in Ihrem Code import_NSData_Compression()
aufgerufen wird. Es spielt keine Rolle, wo es aufgerufen wird oder wie oft es aufgerufen wird. Eigentlich muss es gar nicht aufgerufen werden, es reicht, wenn der Linker das glaubt. Sie können beispielsweise den folgenden Code an einer beliebigen Stelle in Ihrem Projekt einfügen:
__attribute__((used)) static void importCategories ()
{
import_NSData_Compression();
// add more import calls here
}
Sie müssen importCategories()
Ihren Code nie aufrufen , das Attribut lässt den Compiler und Linker glauben, dass er aufgerufen wird, auch wenn dies nicht der Fall ist.
Und noch ein letzter Tipp:
Wenn Sie -whyload
dem letzten Linkaufruf hinzufügen , druckt der Linker im Build-Protokoll, welche Objektdatei aus welcher Bibliothek aufgrund des verwendeten Symbols geladen wurde. Es wird nur das erste Symbol gedruckt, das als verwendet betrachtet wird. Dies ist jedoch nicht unbedingt das einzige Symbol, das für diese Objektdatei verwendet wird.