NSObject + load et + initialize - Que font-ils?

115

Je suis intéressé à comprendre les circonstances qui poussent un développeur à remplacer + initialiser ou + charger. La documentation indique clairement que ces méthodes sont appelées pour vous par le runtime Objective-C, mais c'est vraiment tout ce qui ressort de la documentation de ces méthodes. :-)

Ma curiosité vient de regarder l'exemple de code d'Apple - MVCNetworking. Leur classe de modèle a une +(void) applicationStartupméthode. Il fait un peu de ménage sur le système de fichiers, lit NSDefaults, etc. etc.

J'ai modifié le projet MVCNetworking, en supprimant l'appel dans App Delegate à + applicationStartup, et en mettant les bits de ménage dans + load ... mon ordinateur n'a pas pris feu, mais cela ne veut pas dire que c'est correct! J'espère comprendre les subtilités, les pièges et tout ce qui concerne une méthode de configuration personnalisée que vous devez appeler versus + load ou + initialize.


Pour + charger la documentation dit:

Le message de chargement est envoyé aux classes et catégories qui sont à la fois chargées dynamiquement et liées statiquement, mais uniquement si la classe ou la catégorie nouvellement chargée implémente une méthode qui peut répondre.

Cette phrase est extravagante et difficile à analyser si vous ne connaissez pas le sens précis de tous les mots. Aidez-moi!

  • Que signifie "à la fois chargé dynamiquement et lié statiquement?" Quelque chose peut-il être chargé dynamiquement ET lié statiquement, ou sont-ils mutuellement exclusifs?

  • "... la classe ou la catégorie nouvellement chargée implémente une méthode qui peut répondre" Quelle méthode? Répondez comment?


Quant à + initialiser, la documentation dit:

initialize, il n'est appelé qu'une seule fois par classe. Si vous souhaitez effectuer une initialisation indépendante pour la classe et pour les catégories de la classe, vous devez implémenter des méthodes de chargement.

Je suppose que cela signifie, "si vous essayez de configurer la classe ... n'utilisez pas initialize." D'accord, très bien. Quand ou pourquoi devrais-je remplacer initialiser alors?

edelaney05
la source

Réponses:

184

Le loadmessage

Le moteur d'exécution envoie le loadmessage à chaque objet de classe, très peu de temps après le chargement de l'objet de classe dans l'espace d'adressage du processus. Pour les classes qui font partie du fichier exécutable du programme, le runtime envoie le loadmessage très tôt dans la durée de vie du processus. Pour les classes qui se trouvent dans une bibliothèque partagée (chargée dynamiquement), le moteur d'exécution envoie le message de chargement juste après que la bibliothèque partagée est chargée dans l'espace d'adressage du processus.

De plus, le runtime n'envoie loadà un objet de classe que si cet objet de classe lui-même implémente la loadméthode. Exemple:

@interface Superclass : NSObject
@end

@interface Subclass : Superclass
@end

@implementation Superclass

+ (void)load {
    NSLog(@"in Superclass load");
}

@end

@implementation Subclass

// ... load not implemented in this class

@end

Le runtime envoie le loadmessage à l' Superclassobjet de classe. Il n'envoie pas le loadmessage à l' Subclassobjet de classe, même s'il Subclasshérite de la méthode de Superclass.

Le moteur d'exécution envoie le loadmessage à un objet de classe après qu'il a envoyé le loadmessage à tous les objets de superclasse de la classe (si ces objets de superclasse sont implémentés load) et à tous les objets de classe dans les bibliothèques partagées vers lesquelles vous créez un lien. Mais vous ne savez pas encore quelles autres classes de votre propre exécutable ont été reçues load.

Chaque classe que votre processus charge dans son espace d'adressage recevra un loadmessage si elle implémente la loadméthode, que votre processus utilise ou non la classe à une autre fin.

Vous pouvez voir comment le runtime recherche la loadméthode en tant que cas spécial dans le _class_getLoadMethodof objc-runtime-new.mm, et l'appelle directement depuis call_class_loadsin objc-loadmethod.mm.

Le runtime exécute également la loadméthode de chaque catégorie qu'il charge, même si plusieurs catégories sur la même classe implémentent load. C'est inhabituel. Normalement, si deux catégories définissent la même méthode sur la même classe, l'une des méthodes "gagnera" et sera utilisée, et l'autre méthode ne sera jamais appelée.

La initializeméthode

Le runtime appelle la initializeméthode sur un objet de classe juste avant d'envoyer le premier message (autre que loadou initialize) à l'objet de classe ou à toute instance de la classe. Ce message est envoyé en utilisant le mécanisme normal, donc si votre classe n'implémente pas initialize, mais hérite d'une classe qui le fait, alors votre classe utilisera sa superclasse initialize. Le runtime enverra d'abord le initializeà toutes les superclasses d'une classe (si les superclasses n'ont pas déjà été envoyées initialize).

Exemple:

@interface Superclass : NSObject
@end

@interface Subclass : Superclass
@end

@implementation Superclass

+ (void)initialize {
    NSLog(@"in Superclass initialize; self = %@", self);
}

@end

@implementation Subclass

// ... initialize not implemented in this class

@end

int main(int argc, char *argv[]) {
    @autoreleasepool {
        Subclass *object = [[Subclass alloc] init];
    }
    return 0;
}

Ce programme imprime deux lignes de sortie:

2012-11-10 16:18:38.984 testApp[7498:c07] in Superclass initialize; self = Superclass
2012-11-10 16:18:38.987 testApp[7498:c07] in Superclass initialize; self = Subclass

Puisque le système envoie la initializeméthode paresseusement, une classe ne recevra le message que si votre programme envoie réellement des messages à la classe (ou à une sous-classe, ou à des instances de la classe ou des sous-classes). Et au moment où vous recevez initialize, chaque classe de votre processus devrait déjà avoir reçu load(le cas échéant).

La manière canonique de mettre en œuvre initializeest la suivante:

@implementation Someclass

+ (void)initialize {
    if (self == [Someclass class]) {
        // do whatever
    }
}

Le but de ce modèle est d'éviter de Someclassse réinitialiser quand il a une sous-classe qui ne s'implémente pas initialize.

Le runtime envoie le initializemessage dans la _class_initializefonction dans objc-initialize.mm. Vous pouvez voir qu'il utilise objc_msgSendpour l'envoyer, qui est la fonction normale d'envoi de messages.

Lectures complémentaires

Consultez les questions-réponses de Mike Ash sur ce sujet.

Rob Mayoff
la source
25
Vous devez noter qu'il +loadest envoyé séparément pour les catégories; c'est-à-dire que chaque catégorie d'une classe peut contenir sa propre +loadméthode.
Jonathan Grynspan
1
Notez également que initializesera correctement invoqué par une loadméthode, si nécessaire, du fait de la loadréférence à l'entité non initialisée. Cela peut (étrangement, mais raisonnablement) conduire à initializecourir avant load! C'est ce que j'ai observé, de toute façon. Cela semble être contraire à "Et au moment où vous recevez initialize, chaque classe de votre processus devrait déjà avoir reçu load(le cas échéant)."
Benjohn
5
Vous recevez en loadpremier. Vous pouvez alors recevoir initializependant qu'il loadest toujours en cours d'exécution.
rob mayoff
1
@robmayoff n'avons-nous pas besoin d'ajouter des lignes [super initialize] et [super load], à l'intérieur des méthodes respectives?
damithH
1
C'est généralement une mauvaise idée, car le runtime a déjà envoyé ces deux messages à toutes vos superclasses avant de vous les envoyer.
rob mayoff
17

Cela signifie ne pas remplacer +initializedans une catégorie, vous casserez probablement quelque chose.

+loadest appelée une fois par classe ou catégorie implémentée +load, dès que cette classe ou catégorie est chargée. Quand il dit "lié statiquement", cela signifie compilé dans le binaire de votre application. Les +loadméthodes sur les classes ainsi compilées seront exécutées au lancement de votre application, probablement avant son entrée main(). Quand il dit "chargé dynamiquement", cela signifie chargé via des bundles de plugins ou un appel à dlopen(). Si vous êtes sur iOS, vous pouvez ignorer ce cas.

+initializeest appelée la première fois qu'un message est envoyé à la classe, juste avant qu'elle ne traite ce message. Cela ne se produit (évidemment) qu'une seule fois. Si vous remplacez +initializeune catégorie, l'une des trois choses suivantes se produira:

  • votre implémentation de catégorie est appelée et l'implémentation de la classe ne le fait pas
  • l'implémentation de la catégorie de quelqu'un d'autre est appelée; rien de ce que tu as écrit ne fait
  • votre catégorie n'a pas encore été chargée et son implémentation n'est jamais appelée.

C'est pourquoi vous ne devriez jamais remplacer +initializedans une catégorie - en fait, il est assez dangereux d'essayer de remplacer une méthode dans une catégorie parce que vous n'êtes jamais sûr de ce que vous remplacez ou si votre propre remplaçant sera lui-même remplacé par une autre catégorie.

BTW, un autre problème à considérer +initializeest que si quelqu'un vous sous-classe, vous serez potentiellement appelé une fois pour votre classe et une fois pour chaque sous-classe. Si vous faites quelque chose comme la configuration de staticvariables, vous voudrez vous prémunir contre cela: avec dispatch_once()ou en testant self == [MyClass class].


la source