157 votes

Catégories Objective-C dans la bibliothèque statique

Pouvez-vous me guider pour lier correctement une bibliothèque statique à un projet iPhone. J'utilise le projet de bibliothèque statique ajouté au projet d'application comme dépendance directe (cible -> général -> dépendances directes) et tout fonctionne bien, mais les catégories. Une catégorie définie dans la bibliothèque statique ne fonctionne pas dans l'application.

Ma question est donc la suivante : comment ajouter une bibliothèque statique avec certaines catégories dans un autre projet ?

Et en général, quelle est la meilleure pratique pour utiliser le code d'un projet d'application provenant d'autres projets ?

1 votes

J'ai trouvé quelques réponses et il semble que cette question ait déjà été traitée ici (désolé de l'avoir manqué). stackoverflow.com/questions/932856/ )

228voto

Vladimir Points 4045

Solution : Depuis Xcode 4.2, il vous suffit d'aller dans l'application qui fait le lien avec la bibliothèque (pas la bibliothèque elle-même) et de cliquer sur le projet dans le navigateur de projets, de cliquer sur la cible de votre application, puis sur les paramètres de construction, puis de rechercher "Other Linker Flags", de cliquer sur le bouton + et d'ajouter "-ObjC". Les options '-all_load' et '-force_load' ne sont plus nécessaires.

Détails : J'ai trouvé quelques réponses sur divers forums, blogs et docs d'Apple. J'essaie maintenant de faire un résumé de mes recherches et expériences.

Le problème a été causé par (citation de la question technique Q&A QA1490 d'Apple). https://developer.apple.com/library/content/qa/qa1490/_index.html ) :

Objective-C ne définit pas de symboles de liaison pour chaque fonction (ou méthode, en Objective-C) - à la place, les symboles de liaison ne sont générés que pour chaque classe. Si vous étendez une classe préexistante préexistante avec des catégories, le linker ne sait pas associer le code objet de l'implémentation de la classe principale et l'implémentation de la catégorie. Ce site empêche les objets créés dans l'application l'application résultante de répondre à un sélecteur qui est défini dans la catégorie.

Et leur solution :

Pour résoudre ce problème, la bibliothèque statique statique doit passer l'option -ObjC à l'éditeur de liens. Cette option fait en sorte que le linker de charger chaque fichier objet de la la bibliothèque qui définit une classe classe ou catégorie Objective-C. Bien que cette option donne généralement lieu à un exécutable plus volumineux (en raison du code objet supplémentaire chargé dans l'application l'application), elle permet de création réussie de bibliothèques statiques bibliothèques statiques Objective-C qui contiennent des catégories sur des classes existantes.

et il y a aussi une recommandation dans la FAQ du développement de l'iPhone :

Comment lier toutes les classes Objective-C dans une bibliothèque statique ? Définissez le paramètre de construction Other Linker Flags build setting à -ObjC.

et la description des drapeaux :

- tout_chargement Charge tous les membres des bibliothèques d'archives statiques.

- ObjC Charge tous les membres des bibliothèques d'archives statiques qui implémentent une classe ou une catégorie Objective-C. classe ou catégorie Objective-C.

- force_load (path_to_archive) Charge tous les membres de la bibliothèque d'archives spécifiée. Remarque : -all_load force tous les membres de toutes les archives à être chargés. Cette option vous permet de cibler une archive spécifique.

*On peut utiliser force_load pour réduire la taille du binaire de l'application et éviter les conflits que all_load peut provoquer dans certains cas.

Oui, cela fonctionne avec les fichiers *.a ajoutés au projet. Pourtant, j'ai eu des problèmes avec le projet lib ajouté comme dépendance directe. Mais plus tard, j'ai découvert que c'était ma faute - le projet de dépendance directe n'a peut-être pas été ajouté correctement. Lorsque je l'ai supprimé et ajouté à nouveau avec des étapes :

  1. Glissez-déposez le fichier du projet lib dans le projet d'application (ou ajoutez-le avec Projet->Ajouter au projet ).
  2. Cliquez sur la flèche de l'icône du projet lib - le nom du fichier mylib.a est affiché, faites glisser ce fichier mylib.a et déposez-le dans le groupe Target -> Link Binary With Library.
  3. Ouvrez les informations sur la cible dans la page du poing (Général) et ajoutez ma librairie à la liste des dépendances.

Après cela, tout fonctionne bien. Le drapeau "-ObjC" était suffisant dans mon cas.

J'étais également intéressé par l'idée de http://iphonedevelopmentexperiences.blogspot.com/2010/03/categories-in-static-library.html blog. L'auteur dit qu'il peut utiliser la catégorie de la bibliothèque sans définir le drapeau -all_load ou -ObjC. Il suffit d'ajouter aux fichiers h/m de la catégorie une interface/implémentation de classe vide factice pour forcer le linker à utiliser ce fichier. Et oui, cette astuce fait l'affaire.

Mais l'auteur dit aussi qu'il n'a même pas instancié l'objet factice. Mm Comme je l'ai constaté, nous devrions explicitement appeler du "vrai" code depuis le fichier de catégorie. Donc au moins la fonction de la classe doit être appelée. Et nous n'avons même pas besoin d'une classe factice. Une seule fonction c fait la même chose.

Donc, si nous écrivons les fichiers lib comme :

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

et si nous appelons useMyLib() ; n'importe où dans le projet App alors dans n'importe quelle classe nous pouvons utiliser la méthode de catégorie logSelf ;

[self logSelf];

Et d'autres blogs sur le thème :

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 votes

La note technique d'Apple semble avoir été modifiée depuis pour dire "Pour résoudre ce problème, la cible liant la bibliothèque statique doit passer l'option -ObjC au linker", ce qui est l'opposé de ce qui est cité ci-dessus. Nous venons de confirmer que vous devez inclure l'application dans le lien et non la bibliothèque elle-même.

0 votes

Selon le document developer.apple.com/library/mac/#qa/qa1490/_index.html nous devrions utiliser le drapeau -all_load ou -force_load. Comme mentionné, le linker a un bug dans les applications Mac et iPhone 64bit. "Important : Pour les applications Mac 64 bits et iPhone OS, il existe un bug de l'éditeur de liens qui empêche -ObjC de charger les fichiers d'objets des bibliothèques statiques qui ne contiennent que des catégories et aucune classe. La solution de contournement consiste à utiliser les drapeaux -all_load ou -force_load."

2 votes

@Ken Aspelagh : Merci, j'avais le même problème. Les drapeaux -ObjC et -all_load doivent être ajoutés à la commande l'application elle-même et non la bibliothèque.

122voto

Mecki Points 35351

La réponse de Vladimir est en fait assez bonne, mais j'aimerais donner un peu plus d'informations de base ici. Peut-être qu'un jour quelqu'un trouvera ma réponse et la trouvera utile.

Le compilateur transforme les fichiers sources (.c, .cc, .cpp, .m) en fichiers objets (.o). Il y a un fichier objet par fichier source. Les fichiers objets contiennent des symboles, du code et des données. Les fichiers objets ne sont pas directement utilisables par le système d'exploitation.

Maintenant, lors de la construction d'une bibliothèque dynamique (.dylib), d'un framework, d'un bundle chargeable (.bundle) ou d'un binaire exécutable, ces fichiers objets sont liés ensemble par l'éditeur de liens pour produire quelque chose que le système d'exploitation considère comme "utilisable", c'est-à-dire quelque chose qu'il peut charger directement à une adresse mémoire spécifique.

Cependant, lors de la construction d'une bibliothèque statique, tous ces fichiers objets sont simplement ajoutés à un gros fichier d'archive, d'où l'extension des bibliothèques statiques (.a pour archive). Un fichier .a n'est donc rien d'autre qu'une archive de fichiers objets (.o). Pensez à une archive TAR ou à une archive ZIP sans compression. Il est simplement plus facile de copier un seul fichier .a qu'un grand nombre de fichiers .o (comme en Java, où l'on regroupe les fichiers .class dans une archive .jar pour faciliter la distribution).

Lors de la liaison d'un binaire à une bibliothèque statique (= archive), l'éditeur de liens obtiendra une table de tous les symboles de l'archive et vérifiera lesquels de ces symboles sont référencés par les binaires. Seuls les fichiers objets contenant les symboles référencés sont effectivement chargés par le linker et sont pris en compte par le processus de liaison. Par exemple, si votre archive contient 50 fichiers objets, mais que seuls 20 contiennent des symboles utilisés par le binaire, seuls ces 20 sont chargés par l'éditeur de liens, les 30 autres sont entièrement ignorés dans le processus de liaison.

Cela fonctionne très bien pour le code C et C++, car ces langages essaient d'en faire le plus possible au moment de la compilation (bien que le C++ ait aussi des caractéristiques réservées à l'exécution). Obj-C, cependant, est un autre type de langage. Obj-C dépend fortement des fonctionnalités d'exécution et de nombreuses fonctionnalités Obj-C sont en fait des fonctionnalités d'exécution uniquement. Les classes Obj-C ont en fait des symboles comparables aux fonctions C ou aux variables C globales (du moins dans le runtime Obj-C actuel). Un linker peut voir si une classe est référencée ou non, il peut donc déterminer si une classe est utilisée ou non. Si vous utilisez une classe à partir d'un fichier objet dans une bibliothèque statique, ce fichier objet sera chargé par l'éditeur de liens parce que l'éditeur de liens voit qu'un symbole est utilisé. Les catégories sont une fonctionnalité d'exécution uniquement, les catégories ne sont pas des symboles comme les classes ou les fonctions et cela signifie également qu'un éditeur de liens ne peut pas déterminer si une catégorie est utilisée ou non.

Si l'éditeur de liens charge un fichier objet contenant du code Obj-C, toutes les parties Obj-C de celui-ci font toujours partie de l'étape de liaison. Ainsi, si un fichier objet contenant des catégories est chargé parce qu'un de ses symboles est considéré comme "utilisé" (qu'il s'agisse d'une classe, d'une fonction ou d'une variable globale), les catégories sont également chargées et seront disponibles au moment de l'exécution. En revanche, si le fichier objet lui-même n'est pas chargé, les catégories qu'il contient ne seront pas disponibles au moment de l'exécution. Un fichier objet contenant sólo Les catégories sont jamais chargé parce qu'il contient aucun symbole l'éditeur de liens jamais considérer comme "en service". Et c'est là tout le problème.

Plusieurs solutions ont été proposées et maintenant que vous savez comment tout cela s'articule, examinons à nouveau la solution proposée :

  1. Une solution consiste à ajouter -all_load à l'appel de l'éditeur de liens. Que va faire ce drapeau de l'éditeur de liens ? En fait, il dit à l'éditeur de liens ce qui suit : " Chargement de tous les fichiers objets de toutes les archives, qu'un symbole soit utilisé ou non. '. Bien sûr, cela fonctionnera, mais cela peut aussi produire des binaires plutôt gros.

  2. Une autre solution consiste à ajouter -force_load à l'appel de l'éditeur de liens incluant le chemin vers l'archive. Ce drapeau fonctionne exactement comme -all_load mais uniquement pour l'archive spécifiée. Bien sûr, cela fonctionnera également.

  3. La solution la plus courante consiste à ajouter -ObjC à l'appel de l'éditeur de liens. Que fait réellement ce drapeau de l'éditeur de liens ? Ce drapeau dit à l'éditeur de liens " Chargez tous les fichiers objets de toutes les archives si vous voyez qu'ils contiennent du code Obj-C. ". Et "tout code Obj-C" inclut les catégories. Cela fonctionnera également et ne forcera pas le chargement des fichiers d'objets ne contenant pas de code Obj-C (ceux-ci ne sont toujours chargés que sur demande).

  4. Une autre solution consiste à utiliser le nouveau paramètre de construction de Xcode. Perform Single-Object Prelink . Que va faire ce paramètre ? S'il est activé, tous les fichiers d'objets (rappelez-vous, il y en a un par fichier source) sont fusionnés en un seul fichier d'objets (qui n'est pas une véritable liaison, d'où le nom de PreLink ) et ce fichier objet unique (parfois aussi appelé "fichier objet maître") est alors ajouté à l'archive. Si un symbole du fichier objet maître est considéré comme utilisé, c'est l'ensemble du fichier objet maître qui est considéré comme utilisé, et donc toutes les parties Objective-C de ce fichier sont toujours chargées. Et comme les classes sont des symboles normaux, il suffit d'utiliser une seule classe d'une telle bibliothèque statique pour obtenir également toutes les catégories.

  5. La solution finale est l'astuce que Vladimir a ajoutée à la toute fin de sa réponse. Placez un " faux symbole "dans tout fichier source ne déclarant que des catégories. Si vous voulez utiliser l'une des catégories au moment de l'exécution, assurez-vous de faire référence d'une manière ou d'une autre à l'élément faux symbole au moment de la compilation, car cela entraîne le chargement du fichier objet par l'éditeur de liens et donc aussi de tout le code Obj-C qu'il contient. Par exemple, il peut s'agir d'une fonction avec un corps de fonction vide (qui ne fera rien lorsqu'elle sera appelée) ou d'un accès à une variable globale (par exemple, une variable globale int une fois lu ou une fois écrit, c'est suffisant). Contrairement à toutes les autres solutions ci-dessus, cette solution transfère le contrôle des catégories disponibles à l'exécution au code compilé (s'il veut qu'elles soient liées et disponibles, il accède au symbole, sinon il n'accède pas au symbole et l'éditeur de liens l'ignorera).

C'est tout, les gars.

Oh, attends, il y a encore une chose :
L'éditeur de liens a une option nommée -dead_strip . Que fait cette option ? Si l'éditeur de liens a décidé de charger un fichier objet, tous les symboles du fichier objet deviennent partie intégrante du binaire lié, qu'ils soient utilisés ou non. Par exemple, un fichier objet contient 100 fonctions, mais une seule d'entre elles est utilisée par le binaire, les 100 fonctions sont quand même ajoutées au binaire car les fichiers objets sont soit ajoutés dans leur ensemble, soit pas ajoutés du tout. L'ajout partiel d'un fichier objet n'est généralement pas pris en charge par les éditeurs de liens.

Cependant, si vous demandez à l'éditeur de liens d'effectuer un "dead strip", l'éditeur de liens ajoutera d'abord tous les fichiers objets au binaire, résoudra toutes les références et enfin recherchera dans le binaire les symboles non utilisés (ou seulement utilisés par d'autres symboles non utilisés). Tous les symboles jugés inutiles sont ensuite supprimés dans le cadre de l'étape d'optimisation. Dans l'exemple ci-dessus, les 99 fonctions inutilisées sont à nouveau supprimées. Ceci est très utile si vous utilisez des options comme -load_all , -force_load o Perform Single-Object Prelink car ces options peuvent facilement faire exploser la taille des binaires dans certains cas et le dépouillement des données supprimera à nouveau le code et les données inutilisés.

Le dépouillement fonctionne très bien pour le code C (par exemple, les fonctions, variables et constantes inutilisées sont supprimées comme prévu) et il fonctionne également très bien pour le C++ (par exemple, les classes inutilisées sont supprimées). Ce n'est pas parfait, dans certains cas certains symboles ne sont pas supprimés alors qu'il serait correct de les supprimer, mais dans la plupart des cas cela fonctionne assez bien pour ces langages.

Et Obj-C ? Oubliez-le ! Il n'y a pas de dead stripping pour Obj-C. Comme Obj-C est un langage à fonctionnalité d'exécution, le compilateur ne peut pas dire au moment de la compilation si un symbole est vraiment utilisé ou non. Par exemple, une classe Obj-C n'est pas utilisée s'il n'y a pas de code qui y fait directement référence, n'est-ce pas ? C'est faux ! Vous pouvez construire dynamiquement une chaîne contenant un nom de classe, demander un pointeur de classe pour ce nom et allouer dynamiquement la classe. Par exemple, au lieu de

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

Je pourrais aussi écrire

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

Dans les deux cas mmc est une référence à un objet de la classe "MyCoolClass", mais il y a pas de référence directe à cette classe dans le deuxième exemple de code (même pas le nom de la classe comme chaîne statique). Tout se passe uniquement au moment de l'exécution. Et ce, même si les classes sont sont en fait de vrais symboles. C'est encore pire pour les catégories, car elles ne sont même pas de vrais symboles.

Ainsi, si vous avez une bibliothèque statique avec des centaines d'objets, mais que la plupart de vos binaires n'ont besoin que de quelques-uns d'entre eux, vous préférerez peut-être ne pas utiliser les solutions (1) à (4) ci-dessus. Sinon, vous vous retrouvez avec de très gros binaires contenant toutes ces classes, même si la plupart d'entre elles ne sont jamais utilisées. Pour les classes, vous n'avez généralement pas besoin d'une solution spéciale puisque les classes ont des symboles réels et tant que vous les référencez directement (pas comme dans le deuxième exemple de code), le linker identifiera leur utilisation assez bien tout seul. Pour les catégories, cependant, considérez la solution (5), car elle permet d'inclure seulement les catégories dont vous avez vraiment besoin.

Par exemple, si vous souhaitez créer une catégorie pour NSData, par exemple en lui ajoutant une méthode de compression/décompression, vous créerez un fichier d'en-tête :

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

void import_NSData_Compression ( );

et un fichier de mise en œuvre

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

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

void import_NSData_Compression ( ) { }

Maintenant, assurez-vous que n'importe où dans votre code import_NSData_Compression() est appelé. Peu importe où il est appelé ou combien de fois il est appelé. En fait, il n'a pas besoin d'être appelé du tout, il suffit que l'éditeur de liens le pense. Par exemple, vous pouvez mettre le code suivant n'importe où dans votre projet :

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

Vous n'avez pas besoin d'appeler importCategories() dans votre code, l'attribut fera croire au compilateur et à l'éditeur de liens qu'il est appelé, même s'il ne l'est pas.

Et un dernier conseil :
Si vous ajoutez -whyload jusqu'à l'appel de lien final, l'éditeur de liens imprimera dans le journal de construction quel fichier objet de quelle bibliothèque il a chargé à cause de quel symbole utilisé. Il n'imprimera que le premier symbole considéré comme utilisé, mais ce n'est pas nécessairement le seul symbole utilisé pour ce fichier objet.

1 votes

Merci de mentionner -whyload En revanche, il peut s'avérer difficile d'expliquer pourquoi l'éditeur de liens fait quelque chose !

0 votes

Il existe une option Dead Code Stripping en Build Settings>Linking . Est-ce la même chose que -dead_strip ajouté en Other Linker Flags ?

1 votes

@Sean Oui, c'est la même chose. Il suffit de lire le "Quick Help" qui existe pour chaque paramètre de construction, la réponse est juste là : postimg.org/image/n7megftnr/full

24voto

tonklon Points 5687

Cette question a été corrigé dans LLVM . Le correctif est livré avec LLVM 2.9 La première version de Xcode à contenir le correctif est Xcode 4.2 livré avec LLVM 3.0. L'utilisation de -all_load o -force_load n'est plus nécessaire lorsqu'on travaille avec XCode 4.2 -ObjC est toujours nécessaire.

0 votes

Etes-vous sûr de cela ? Je travaille sur un projet iOS en utilisant Xcode 4.3.2, en compilant avec LLVM 3.1 et cela reste un problème pour moi.

0 votes

Ok, c'était un peu imprécis. Le site -ObjC est toujours nécessaire et le sera toujours. La solution de contournement était l'utilisation de -all_load o -force_load . Et ce n'est plus nécessaire. J'ai corrigé ma réponse ci-dessus.

0 votes

Y a-t-il un inconvénient à inclure le drapeau -all_load (même s'il est inutile) ? Cela affecte-t-il le temps de compilation / lancement de quelque manière que ce soit ?

17voto

amosel Points 354

Voici ce que vous devez faire pour résoudre complètement ce problème lors de la compilation de votre bibliothèque statique :

Soit aller dans Xcode Build Settings et mettre Perform Single-Object Prelink sur YES ou GENERATE_MASTER_OBJECT_FILE = YES dans votre fichier de configuration de construction.

Par défaut, le linker génère un fichier .o pour chaque fichier .m. Les catégories obtiennent donc des fichiers .o différents. Lorsque l'éditeur de liens regarde les fichiers .o d'une bibliothèque statique, il ne crée pas d'index de tous les symboles par classe (le Runtime le fera, peu importe la classe).

Cette directive demandera à l'éditeur de liens d'empaqueter tous les objets ensemble dans un grand fichier .o et par cela il force l'éditeur de liens qui traite la bibliothèque statique à obtenir l'index de toutes les catégories de classe.

J'espère que c'est plus clair.

0 votes

Cela a réglé le problème pour moi sans avoir à ajouter -ObjC à la cible de liaison.

0 votes

Après avoir mis à jour la dernière version de BlocksKit j'ai dû utiliser ce paramètre pour résoudre le problème (j'utilisais déjà l'indicateur -ObjC mais le problème persistait).

2 votes

En fait, votre réponse n'est pas tout à fait correcte. Je ne "demande pas à l'éditeur de liens de regrouper toutes les catégories de la même classe dans un seul fichier .o", mais de lier tous les fichiers objets (.o) en un seul gros fichier objet avant de créer une bibliothèque statique à partir de ceux-ci. Dès qu'un symbole est référencé dans la bibliothèque, tous les symboles sont chargés. Cependant, cela ne fonctionnera pas si aucun symbole n'est référencé (par exemple, s'il n'y a que des catégories dans la bibliothèque).

9voto

abbood Points 5959

Un facteur qui est rarement mentionné à chaque fois que la discussion sur la liaison de bibliothèque statique est abordée est le fait que vous doit également inclure les catégories elles-mêmes dans les phases de construction->copier les fichiers et compiler les sources de la bibliothèque statique elle-même .

Apple ne met pas non plus l'accent sur ce fait dans sa récente publication. Utilisation des bibliothèques statiques dans iOS soit.

J'ai passé une journée entière à essayer toutes sortes de variations de -objC et -all_load etc mais rien n'en est sorti . ce Cette question a attiré mon attention sur ce problème. (ne vous méprenez pas vous devez toujours faire le truc -objC mais c'est plus que cela).

Une autre action qui m'a toujours aidé est que je construis toujours la bibliothèque statique incluse d'abord seule puis je construis l'application qui l'entoure

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X