wake-up-neo.com

Objective-C-Kategorien in der statischen Bibliothek

Können Sie mir erklären, wie man eine statische Bibliothek richtig mit einem iPhone-Projekt verknüpft? Ich verwende ein statisches Bibliotheksprojekt, das zu einem App-Projekt hinzugefügt wurde, als direkte Abhängigkeit (Ziel -> Allgemein -> direkte Abhängigkeiten) und alles funktioniert in Ordnung, aber Kategorien. Eine in der statischen Bibliothek definierte Kategorie funktioniert in der App nicht.

Meine Frage ist also, wie man eine statische Bibliothek mit einigen Kategorien zu einem anderen Projekt hinzufügt.

Und was ist im Allgemeinen die beste Vorgehensweise für die Verwendung von App-Projektcode aus anderen Projekten?

148
Vladimir

Lösung: Ab Xcode 4.2 müssen Sie nur zu der Anwendung wechseln, die mit der Bibliothek verknüpft ist (nicht mit der Bibliothek selbst) und das Projekt anklicken Klicken Sie im Projektnavigator auf das Ziel Ihrer App, erstellen Sie die Einstellungen, suchen Sie nach "Other Linker Flags", klicken Sie auf die Schaltfläche "+" und fügen Sie "-ObjC" hinzu. '-all_load' und '-force_load' werden nicht mehr benötigt.

Details: Ich habe einige Antworten in verschiedenen Foren, Blogs und Apple Dokumenten gefunden. Jetzt versuche ich eine kurze Zusammenfassung meiner Suchen und Experimente zu machen.

Das Problem wurde verursacht durch (Zitat von Apple Technische Fragen und Antworten QA1490 https: //developer.Apple.com/library/content/qa/qa1490/_index.html ):

Objective-C definiert keine Linkersymbole für jede Funktion (oder Methode in Objective-C). Stattdessen werden Linkersymbole nur für jede Klasse generiert. Wenn Sie eine bereits vorhandene Klasse mit Kategorien erweitern, kann der Linker den Objektcode der Kernklassenimplementierung nicht mit der Kategorieimplementierung verknüpfen. Dadurch wird verhindert, dass in der resultierenden Anwendung erstellte Objekte auf einen in der Kategorie definierten Selektor reagieren.

Und ihre Lösung:

Um dieses Problem zu beheben, sollte die statische Bibliothek die Option -ObjC an den Linker übergeben. Dieses Flag bewirkt, dass der Linker jede Objektdatei in der Bibliothek lädt, die eine Objective-C-Klasse oder -Kategorie definiert. Diese Option führt in der Regel zu einer größeren ausführbaren Datei (aufgrund des in die Anwendung geladenen zusätzlichen Objektcodes), ermöglicht jedoch die erfolgreiche Erstellung effektiver statischer Objective-C-Bibliotheken, die Kategorien für vorhandene Klassen enthalten.

und es gibt auch Empfehlungen in den FAQ zur iPhone-Entwicklung:

Wie verbinde ich alle Objective-C-Klassen in einer statischen Bibliothek? Setzen Sie die Buildeinstellung Other Linker Flags auf -ObjC.

und Flaggenbeschreibungen:

- all_load Lädt alle Mitglieder der statischen Archivbibliotheken.

- ObjC Lädt alle Mitglieder statischer Archivbibliotheken, die eine Objective-C-Klasse oder -Kategorie implementieren.

- force_load (path_to_archive) Lädt alle Mitglieder der angegebenen statischen Archivbibliothek. Hinweis: -all_load erzwingt das Laden aller Mitglieder aller Archive. Mit dieser Option können Sie ein bestimmtes Archiv als Ziel festlegen.

* Wir können force_load verwenden, um die App-Binärgröße zu reduzieren und Konflikte zu vermeiden, die all_load in einigen Fällen verursachen kann.

Ja, es funktioniert mit * .a-Dateien, die dem Projekt hinzugefügt wurden. Dennoch hatte ich Probleme mit dem lib-Projekt, das als direkte Abhängigkeit hinzugefügt wurde. Aber später stellte ich fest, dass es mein Fehler war - das direkte Abhängigkeitsprojekt wurde möglicherweise nicht richtig hinzugefügt. Wenn ich es entferne und wieder mit Schritten hinzufüge:

  1. Ziehen Sie die lib-Projektdatei per Drag & Drop in das App-Projekt (oder fügen Sie sie mit Project-> Add to project… hinzu).
  2. Klicken Sie auf den Pfeil neben dem Symbol lib project - der Name der Datei mylib.a wird angezeigt. Ziehen Sie diese Datei mylib.a und legen Sie sie in der Gruppe Target -> Link Binary With Library ab.
  3. Öffnen Sie die Zielinformationen auf der ersten Seite (Allgemein) und fügen Sie meine Bibliothek zur Abhängigkeitsliste hinzu

danach funktioniert alles in Ordnung. "-ObjC" -Flag war in meinem Fall ausreichend.

Ich war auch an einer Idee aus dem http: //iphonedevelopmentexperiences.blogspot.com/2010/03/categories-in-static-library.html Blog interessiert. Der Autor gibt an, dass er die Kategorie aus lib verwenden kann, ohne das Flag -all_load oder -ObjC zu setzen. Er fügt nur der Kategorie h/m-Dateien eine leere Dummy-Klassen-Schnittstelle/-Implementierung hinzu, um den Linker zu zwingen, diese Datei zu verwenden. Und ja, dieser Trick macht den Job.

Der Autor sagte aber auch, er habe nicht einmal ein Dummy-Objekt instanziiert. Mm… Wie ich festgestellt habe, sollten wir explizit "echten" Code aus der Kategoriedatei aufrufen. Also sollte zumindest die Klassenfunktion aufgerufen werden. Und wir brauchen auch keine Dummy-Klasse. Single-C-Funktion machen das gleiche.

Wenn wir also lib-Dateien schreiben als:

// 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

und wenn wir useMyLib () aufrufen; Überall im App-Projekt und in jeder Klasse können wir die logSelf-Kategoriemethode verwenden.

[self logSelf];

Und noch mehr Blogs zum Thema:

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

224
Vladimir

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.

Wenn Sie nun eine dynamische Bibliothek (.dylib), ein Framework, ein ladbares Bundle (.bundle) oder eine ausführbare Binärdatei erstellen, werden diese Objektdateien durch den Linker miteinander verknüpft, um etwas zu erzeugen, das das Betriebssystem als "verwendbar" erachtet, z. Dies kann direkt in eine bestimmte Speicheradresse geladen werden.

Wenn Sie jedoch eine statische Bibliothek erstellen, werden alle diese Objektdateien einfach zu einer großen Archivdatei hinzugefügt, daher die Erweiterung der statischen Bibliotheken (.a für Archivierung). 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 in 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 prüft, auf welche dieser Symbole die Binärdateien verweisen. Nur die Objektdateien, die referenzierte Symbole enthalten, werden vom Linker geladen und vom Verknüpfungsprozess berücksichtigt. Z.B. Wenn Ihr Archiv 50 Objektdateien enthält, aber nur 20 von der Binärdatei verwendete Symbole enthalten, werden nur diese 20 vom Linker geladen, die anderen 30 werden beim Linken vollständig ignoriert.

Dies funktioniert sehr gut für C- und C++ - Code, da diese Sprachen versuchen, beim Kompilieren so viel wie möglich zu leisten (obwohl C++ auch einige Nur-Laufzeit-Funktionen hat). 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 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 feststellen kann, ob eine 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 und das 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 davon 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 ist, sind die darin enthaltenen Kategorien zur Laufzeit nicht verfügbar. Eine Objektdatei, die nur Kategorien enthält, wird nie geladen, da sie keine Symbole enthält, die der Linker immer als "in Verwendung" betrachten würde ". Und das ist das ganze Problem hier.

Es wurden mehrere Lösungen vorgeschlagen. Nachdem Sie nun wissen, wie sich das alles zusammenfügt, schauen wir uns die vorgeschlagene Lösung noch einmal an:

  1. Eine Lösung besteht darin, dem Linker-Aufruf -all_load Hinzuzufügen. Was wird diese Linker-Flagge tatsächlich tun? Tatsächlich sagt es dem Linker Folgendes: " Lade alle Objektdateien aller Archive, unabhängig davon, ob ein Symbol verwendet wird oder nicht '. Natürlich wird das funktionieren, aber es kann auch ziemlich groß werden Binärdateien.

  2. Eine andere Lösung besteht darin, dem Linker-Aufruf -force_load Mit dem Pfad zum Archiv hinzuzufügen. Dieses Flag funktioniert genau wie -all_load, Jedoch nur für das angegebene Archiv. Das wird natürlich auch funktionieren.

  3. Die beliebteste Lösung ist das Hinzufügen von -ObjC Zum Linker-Aufruf. Was wird diese Linker-Flagge tatsächlich tun? Dieses Flag sagt dem Linker " Lade alle Objektdateien aus allen Archiven, wenn du siehst, dass sie Obj-C-Code enthalten". Und "jeder Obj-C-Code" enthält Kategorien. Dies funktioniert auch und erzwingt kein Laden von Objektdateien, die keinen Obj-C-Code enthalten (diese werden immer noch nur auf Anforderung geladen).

  4. 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 (das ist keine echte Verknüpfung, daher der Name PreLink) und diesem einzelnen Objekt zusammengeführt Die Datei (manchmal auch als "Master-Objektdatei" bezeichnet) wird dann dem Archiv hinzugefügt. Wenn jetzt ein Symbol der Master-Objektdatei als verwendet betrachtet wird, wird die gesamte Master-Objektdatei als verwendet betrachtet und daher werden immer alle Objective-C-Teile davon geladen. Und da Klassen normale Symbole sind, ist es ausreichend, eine einzelne Klasse aus einer solchen statischen Bibliothek zu verwenden, um auch alle Kategorien abzurufen.

  5. Die endgültige Lösung ist der Trick, den Vladimir ganz am Ende seiner Antwort hinzugefügt hat. Fügen Sie in jede Quelldatei ein " falsches Symbol" ein, das nur Kategorien deklariert. Wenn Sie zur Laufzeit eine der Kategorien verwenden möchten, stellen Sie sicher, dass Sie beim Kompilieren auf das Fake-Symbol verweisen, da dies dazu führt, dass die Objektdatei vom Linker und damit auch alle Objekte geladen werden -C Code drin. Z.B. Es kann sich um eine Funktion mit einem leeren Funktionskörper handeln (der beim Aufrufen nichts bewirkt), oder um eine globale Variable, auf die zugegriffen wird (z. B. eine globale int, sobald sie gelesen oder geschrieben wurde, ist dies ausreichend). Im Gegensatz zu allen anderen oben genannten Lösungen verschiebt diese Lösung die Kontrolle darüber, welche Kategorien zur Laufzeit verfügbar sind, in 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, da ist noch eine Sache:
Der Linker hat eine Option mit dem Namen -dead_strip. Was macht diese Option? Wenn der Linker eine Objektdatei lädt, werden alle Symbole der Objektdatei Teil der verknüpften Binärdatei, unabhängig davon, ob sie verwendet werden oder nicht. Z.B. Eine Objektdatei enthält 100 Funktionen, von denen jedoch nur eine von der Binärdatei verwendet wird. Alle 100 Funktionen werden weiterhin zur Binärdatei hinzugefügt, da die 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" anweisen, fügt der Linker zuerst alle Objektdateien zur Binärdatei hinzu, löst alle Verweise auf und durchsucht die Binärdatei schließlich nach Symbolen, die nicht verwendet werden (oder nur von anderen Symbolen, die nicht in Verwendung sind) verwenden). Alle nicht verwendeten Symbole werden dann im Rahmen der Optimierung entfernt. Im obigen Beispiel werden die 99 nicht verwendeten Funktionen wieder entfernt. Dies ist sehr nützlich, wenn Sie Optionen wie -load_all, -force_load Oder Perform Single-Object Prelink Verwenden, da diese Optionen in einigen Fällen Binärgrößen dramatisch in die Luft jagen können und das Dead-Stripping nicht verwendeten Code entfernt und wieder Daten.

Dead Stripping funktioniert sehr gut für C-Code (z. B. nicht verwendete Funktionen, Variablen und Konstanten werden wie erwartet entfernt) und es funktioniert auch recht gut für C++ (z. B. nicht verwendete Klassen werden 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 Dead Stripping. Da es sich bei Obj-C um eine Laufzeitsprache handelt, kann der Compiler beim Kompilieren nicht sagen, ob ein Symbol tatsächlich verwendet wird oder nicht. Z.B. Eine Obj-C-Klasse wird nicht verwendet, wenn kein Code direkt darauf verweist. Falsch! Sie können dynamisch eine Zeichenfolge mit einem Klassennamen erstellen, einen Klassenzeiger für diesen Namen anfordern und die Klasse dynamisch zuweisen. Z.B. anstatt

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

Ich würde auch schreiben

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

In beiden Fällen ist mmc ein Verweis auf ein Objekt der Klasse "MyCoolClass", aber es gibt keinen direkten Verweis auf diese Klasse im zweiten Codebeispiel (nicht einmal den Klassennamen als eine statische Zeichenfolge). Alles passiert nur zur Laufzeit. Und das obwohl Klassen sind eigentlich echte Symbole sind. Für Kategorien ist es noch schlimmer, da sie nicht einmal echte Symbole sind.

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 mit all diesen Klassen, obwohl die meisten von ihnen nie verwendet werden. Für Klassen benötigen Sie normalerweise überhaupt keine spezielle Lösung, da Klassen über echte Symbole verfügen. Solange Sie direkt auf diese verweisen (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 eingeschlossen werden können, die Sie wirklich benötigen.

Z.B. Wenn Sie eine Kategorie für NSData möchten, z. Wenn Sie 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 nun sicher, dass irgendwo in Ihrem Code import_NSData_Compression() aufgerufen wird. Es spielt keine Rolle, wo es heißt oder wie oft es heißt. Eigentlich muss es gar nicht aufgerufen werden, es reicht, wenn der Linker das meint. Z.B. Sie können den folgenden Code an einer beliebigen Stelle in Ihr Projekt einfügen:

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

Sie müssen in Ihrem Code niemals importCategories() aufrufen. Das Attribut lässt den Compiler und den Linker glauben, dass es aufgerufen wird, auch wenn dies nicht der Fall ist.

Und noch ein letzter Tipp:
Wenn Sie dem letzten Linkaufruf -whyload Hinzufügen, druckt der Linker im Erstellungsprotokoll, 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.

112
Mecki

Dieses Problem wurde in LLVM behoben . Der Fix wird als Teil von LLVM 2.9 ausgeliefert. Die erste Xcode-Version, die den Fix enthält, ist Xcode 4.2, das mit LLVM 3.0 ausgeliefert wird. Die Verwendung von -all_load oder -force_load wird bei der Arbeit mit XCode 4.2 nicht mehr benötigt -ObjC wird noch benötigt.

24
tonklon

Folgendes müssen Sie tun, um dieses Problem beim Kompilieren Ihrer statischen Bibliothek vollständig zu beheben:

Gehen Sie entweder zu Xcode Build Settings und setzen Sie Perform Single-Object Prelink auf YES oder GENERATE_MASTER_OBJECT_FILE = YES in Ihrer Build-Konfigurationsdatei.

Standardmäßig generiert der Linker für jede .m-Datei eine .o-Datei. Kategorien erhalten also unterschiedliche .o-Dateien. Wenn der Linker eine statische Bibliothek mit .o-Dateien betrachtet, erstellt er keinen Index aller Symbole pro Klasse (Runtime wird, egal was).

Diese Direktive fordert den Linker auf, alle Objekte in eine große .o-Datei zu packen. Dadurch wird der Linker, der die statische Bibliothek verarbeitet, gezwungen, alle Klassenkategorien zu indexieren.

Hoffe das klärt es.

16
amosel

Ein Faktor, der selten erwähnt wird, wenn die Diskussion über die Verknüpfung statischer Bibliotheken auftaucht, ist die Tatsache, dass Sie muss auch die Kategorien selbst in die Erstellungsphasen einbeziehen -> Dateien kopieren und Quellen der statischen Bibliothek selbst kompilieren.

Apple betont dies auch nicht in seinem kürzlich veröffentlichten sing Static Libraries in iOS .

Ich habe einen ganzen Tag damit verbracht, alle möglichen Variationen von -objC und -all_load usw. auszuprobieren, aber nichts kam dabei heraus. diese Frage machte mich auf dieses Problem aufmerksam. (Versteh mich nicht falsch .. du musst noch das -objC Zeug machen .. aber es ist mehr als nur das).

eine weitere Aktion, die mir immer weitergeholfen hat, ist, dass ich die mitgelieferte statische Bibliothek immer zuerst selbst erstellte. Dann erst die umschließende Anwendung.

9
abbood