web-dev-qa-db-fra.com

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

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

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

Et en général, quelle est la meilleure pratique à utiliser dans le code de projet d'application à partir d'autres projets?

148
Vladimir

Solution: À partir de Xcode 4.2, il vous suffit de vous rendre dans l'application reliant la bibliothèque (pas la bibliothèque elle-même) et de cliquer sur le projet dans dans le Navigateur de projet, cliquez sur la cible de votre application, puis définissez les paramètres, puis recherchez "Autres drapeaux de l'éditeur de liens", cliquez sur le bouton +, puis ajoutez "-ObjC". '-all_load' et'force_load 'ne sont plus nécessaires.

Détails: J'ai trouvé des réponses sur divers forums, blogs et Apple docs. J'essaie maintenant de résumer brièvement mon recherches et expériences.

Le problème était dû à (citation de Apple Questions techniques QA1490 https://developer.Apple.com/library/content/qa/qa1490/_index.html) ):

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

Et leur solution:

Pour résoudre ce problème, la bibliothèque statique doit transmettre l'option -ObjC à l'éditeur de liens. Cet indicateur force l'éditeur de liens à charger chaque fichier objet de la bibliothèque qui définit une classe ou une 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), elle permet de créer avec succès des bibliothèques statiques Objective-C efficaces contenant des catégories sur des classes existantes.

et il y a aussi une recommandation dans la FAQ Développement iPhone:

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

et description des drapeaux:

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

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

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

* nous pouvons utiliser force_load pour réduire la taille 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 trouvé que c’était ma faute: le projet de dépendance directe n’a peut-être pas été ajouté correctement. Quand je l'enlève et ajoute à nouveau avec les étapes

  1. Glissez-déposez le fichier de projet lib dans le projet d'application (ou ajoutez-le avec Projet-> Ajouter au projet…).
  2. Cliquez sur la flèche située dans l'icône du projet lib - nom du fichier mylib.a affiché, faites glisser ce fichier mylib.a et déposez-le dans le groupe Cible -> Lien binaire avec bibliothèque.
  3. Ouvrir les informations sur la cible dans la première page (Général) et ajouter ma bibliothèque à la liste des dépendances

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

L’idée du blog http://iphonedevelopmentexperiences.blogspot.com/2010/03/categories-in-static-library.html m'a également intéressée. L'auteur dit qu'il peut utiliser la catégorie de lib sans définir l'indicateur -all_load ou -ObjC. Il vient d'ajouter à la catégorie h/m des fichiers vides d'interface/implémentation de classe factice pour obliger l'éditeur de liens à utiliser ce fichier. Et oui, cette astuce fait le travail.

Mais l'auteur a également dit qu'il n'avait même pas instancié d'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 classe devrait être appelée. Et nous n'avons même pas besoin de cours factices. La fonction simple c fait la même chose.

Donc, si nous écrivons des 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, puis dans n'importe quelle classe, nous pouvons utiliser la méthode de catégorie logSelf;

[self logSelf];

Et plus de 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

224
Vladimir

La réponse de Vladimir est en fait assez bonne, cependant, j'aimerais donner quelques connaissances de base ici. Peut-être qu'un jour, quelqu'un trouvera ma réponse et pourrait la trouver utile.

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

Désormais, lors de la création d'une bibliothèque dynamique (.dylib), d'un framework, d'un bundle chargeable (.bundle) ou d'un fichier binaire exécutable, ces fichiers objet sont liés ensemble par l'éditeur de liens pour produire quelque chose que le système d'exploitation considère comme "utilisable", par exemple. quelque chose qu'il peut charger directement sur 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 grand fichier d'archive, d'où l'extension des bibliothèques statiques (.a pour archive). Un fichier .a n'est donc 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 que de nombreux fichiers .o (comme dans Java, où vous regroupez des fichiers .class dans une archive .jar pour les distribuer facilement).

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

Cela fonctionne assez bien pour le code C et C++, car ces langages essaient d’en faire autant que possible au moment de la compilation (bien que C++ ait aussi des fonctionnalités d’exécution seulement). Obj-C, cependant, est un type de langage différent. Obj-C dépend fortement des fonctionnalités d'exécution et de nombreuses fonctionnalités Obj-C sont en réalité 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 temps d'exécution Obj-C actuel). Un éditeur de liens peut voir si une classe est référencée ou non, donc il peut 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, car l'éditeur de liens voit qu'un symbole est en cours d'utilisation. Les catégories sont uniquement une fonctionnalité d'exécution, les catégories ne sont pas des symboles comme des classes ou des fonctions, ce qui 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 la liaison. Donc, si un fichier objet contenant des catégories est chargé parce que tout symbole de celui-ci est considéré comme "en cours d'utilisation" (que ce soit une classe, une fonction, une variable globale), les catégories sont également chargées et seront disponibles au moment de l'exécution. . Cependant, 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 niquement catégories est jamais chargé car il contient pas de symboles le lieur aurait jamais considéré "en cours d'utilisation" ". Et c'est tout le problème ici.

Plusieurs solutions ont été proposées et maintenant que vous savez comment tout cela se joue, jetons un autre regard sur la solution proposée:

  1. Une solution consiste à ajouter -all_load À l'appel de l'éditeur de liens. Que fera réellement ce drapeau de l'éditeur de liens? En fait, il indique ce qui suit à l'éditeur de liens " Chargez tous les fichiers objets de toutes les archives, que vous voyiez un symbole en cours d'utilisation ou non '. mais peut aussi produire des binaires assez gros.

  2. Une autre solution consiste à ajouter -force_load À l'appel de l'éditeur de liens, y compris le chemin d'accès à l'archive. Cet indicateur fonctionne exactement comme -all_load, Mais uniquement pour l'archive spécifiée. Bien sûr, cela fonctionnera aussi bien.

  3. La solution la plus populaire consiste à ajouter -ObjC À l'appel de l'éditeur de liens. Que fera réellement ce drapeau de l'éditeur de liens? Cet indicateur indique à l'éditeur de liens " Chargez tous les fichiers objet 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 de fichiers objets ne contenant aucun code Obj-C (ceux-ci ne sont encore chargés que sur demande).

  4. Une autre solution est le nouveau paramètre de construction Xcode Perform Single-Object Prelink. Que fera ce réglage? Si activé, tous les fichiers d’objets (rappelez-vous qu’il en existe un par source) sont fusionnés en un seul fichier d’objet (ce n’est pas une liaison réelle, d’où le nom PreLink ) et ce fichier objet unique (parfois appelé également "fichier objet maître") est ensuite ajouté à l'archive. Si à présent tout symbole du fichier objet maître est considéré comme utilisé, le fichier objet maître entier est considéré comme étant utilisé et toutes ses parties Objective-C sont donc toujours chargées. Et comme les classes sont des symboles normaux, il suffit d’utiliser une seule classe d’une bibliothèque statique pour obtenir toutes les catégories.

  5. La solution finale est le truc ajouté par Vladimir à la toute fin de sa réponse. Placez un " faux symbole " dans tout fichier source ne déclarant que des catégories. Si vous souhaitez utiliser l'une des catégories au moment de l'exécution, veillez à référencer le symbole fictif lors de la compilation, car le fichier objet sera alors chargé par l'éditeur de liens et donc également 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 lors de l'appel) ou d'une variable globale à laquelle on accède (par exemple, un int global une fois lu ou écrit, cela suffit). Contrairement à toutes les autres solutions ci-dessus, cette solution transfère le code compilé sur les catégories disponibles au moment de l'exécution (si elle veut qu'elles soient liées et disponibles, elle accède au symbole, sinon elle n'accédera pas au symbole et l'éditeur de liens ignorera il).

C'est tous les gens.

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 font partie 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. Toutes les 100 fonctions sont toujours ajoutées au binaire, car les fichiers objet sont ajoutés dans leur ensemble ou ne le sont pas du tout. L'ajout partiel d'un fichier objet n'est généralement pas pris en charge par les lieurs.

Cependant, si vous indiquez à l'éditeur de liens "bande morte", l'éditeur de liens ajoutera d'abord tous les fichiers objets au binaire, résoudra toutes les références et analysera finalement le binaire pour rechercher les symboles inutilisés (ou uniquement utilisés par d'autres symboles ne se trouvant pas dans la liste). utilisation). Tous les symboles non utilisés sont ensuite supprimés dans le cadre de la phase 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 telles que -load_all, -force_load Ou Perform Single-Object Prelink, Car ces options peuvent facilement faire exploser les tailles binaires de façon spectaculaire dans certains cas et que l'effeuillage mort enlèvera le code inutilisé. et les données à nouveau.

La suppression morte fonctionne très bien pour le code C (par exemple, les fonctions inutilisées, les variables et les constantes sont supprimées comme prévu) et fonctionne également très bien pour 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, même s’il serait acceptable de les supprimer, mais dans la plupart des cas, cela fonctionne assez bien pour ces langues.

Qu'en est-il d'Obj-C? Oublie ça! Il n'y a pas de décapage mort pour Obj-C. Obj-C étant un langage fonctionnant à l'exécution, le compilateur ne peut pas dire lors de la compilation si un symbole est réellement utilisé ou non. Par exemple. une classe Obj-C n'est pas utilisée s'il n'y a pas de code qui la référence directement, correct? Faux! Vous pouvez créer dynamiquement une chaîne contenant un nom de classe, demander un pointeur de classe pour ce nom et allouer de manière dynamique la classe. Par exemple. au lieu de

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

J'écrirais aussi

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 (pas même le nom de la classe une chaîne statique). Tout se passe uniquement à l'exécution. Et c'est même si les classes sont réellement des symboles. C'est encore pire pour les catégories, car ce ne sont même pas de vrais symboles.

Donc, si vous avez une bibliothèque statique avec des centaines d’objets alors que la plupart de vos fichiers binaires n’en ont besoin que de quelques-uns, vous préférerez peut-être ne pas utiliser les solutions (1) à (4) ci-dessus. Sinon, vous vous retrouvez avec de très gros fichiers binaires contenant toutes ces classes, même si la plupart d'entre eux ne sont jamais utilisés. Pour les classes, vous n'avez généralement besoin d'aucune solution spéciale, car elles ont de vrais symboles et tant que vous les référencerez directement (pas comme dans le deuxième exemple de code), l'éditeur de liens identifiera assez facilement leur utilisation. Pour les catégories, cependant, envisagez la solution (5), car elle permet d’inclure uniquement les catégories dont vous avez réellement besoin.

Par exemple. si vous voulez une catégorie pour NSData, par exemple en y ajoutant une méthode de compression/décompression, vous créez un fichier d’en-tête:

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

void import_NSData_Compression ( );

et un fichier d'implémentation

// 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ée. Peu importe où il est appelé ou à quelle fréquence. En fait, il n'est pas nécessaire d'appeler du tout, c'est suffisant si 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 à appeler importCategories() dans votre code, l'attribut fera croire au compilateur et à l'éditeur de liens qu'il est appelé, même si ce n'est pas le cas.

Et un dernier conseil:
Si vous ajoutez -whyload Au dernier appel de lien, l'éditeur de liens imprimera dans le journal de construction le fichier objet de quelle bibliothèque il a été chargé à cause du symbole utilisé. Il imprimera uniquement le premier symbole considéré en cours d'utilisation, mais ce n'est pas nécessairement le seul symbole en utilisation de ce fichier objet.

112
Mecki

Ce problème a été corrigé dans LLVM . Le correctif est inclus dans LLVM 2.9 La première version de Xcode à contenir le correctif est Xcode 4.2 fourni avec LLVM 3.0. L'utilisation de -all_load ou -force_load n'est plus nécessaire avec XCode 4.2 -ObjC est toujours nécessaire.

24
tonklon

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

Accédez aux paramètres de construction Xcode et définissez Perform Single Prelink sur YES ou GENERATE_MASTER_OBJECT_FILE = YES dans votre fichier de configuration de construction.

Par défaut, l'éditeur de liens génère un fichier .o pour chaque fichier .m. Ainsi, les catégories obtiennent différents fichiers .o. Lorsque l'éditeur de liens examine des fichiers .o de bibliothèques statiques, il ne crée pas d'index de tous les symboles par classe (Runtime dépendra peu importe).

Cette directive demandera à l'éditeur de liens de regrouper tous les objets dans un seul et même fichier .o, obligeant ainsi l'éditeur de liens qui traite la bibliothèque statique à obtenir l'index de toutes les catégories de classe.

J'espère que ça le clarifie.

16
amosel

Un facteur qui est rarement mentionné à chaque fois que la discussion sur la bibliothèque statique est évoqué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 n'insiste pas non plus sur ce fait dans leur récente publication Utilisation de bibliothèques statiques sous iOS .

J'ai passé une journée entière à essayer toutes sortes de variantes de -objC et -all_load, etc., mais rien n'en est ressorti. this Cette question a été portée à mon attention. (ne vous méprenez pas… vous devez toujours faire le travail -objC… mais c'est plus que ça).

une autre action qui m'a toujours aidé est que je construis toujours la bibliothèque statique incluse tout d'abord .. puis je construis l'application englobante ..

9
abbood