NSObject + cargar e + inicializar - ¿Qué hacen?

115

Me interesa comprender las circunstancias que llevan a un desarrollador a anular + inicializar o + cargar. La documentación deja en claro que estos métodos los solicita el tiempo de ejecución de Objective-C, pero eso es realmente todo lo que queda claro en la documentación de esos métodos. :-)

Mi curiosidad proviene de mirar el código de ejemplo de Apple: MVCNetworking. Su clase modelo tiene un +(void) applicationStartupmétodo. Hace algunas tareas de limpieza en el sistema de archivos, lee NSDefaults, etc., etc. y, después de intentar asimilar los métodos de clase de NSObject, parece que este trabajo de limpieza podría estar bien para + cargar.

Modifiqué el proyecto MVCNetworking, eliminé la llamada en App Delegate a + applicationStartup y puse los bits de limpieza en + load ... mi computadora no se incendió, ¡pero eso no significa que sea correcto! Espero comprender las sutilezas, trampas y otras cosas relacionadas con un método de configuración personalizado al que debe llamar versus + cargar o + inicializar.


Para + cargar la documentación dice:

El mensaje de carga se envía a clases y categorías que están cargadas dinámicamente y vinculadas estáticamente, pero solo si la clase o categoría recién cargada implementa un método que puede responder.

Esta oración es torpe y difícil de analizar si no conoce el significado preciso de todas las palabras. ¡Ayuda!

  • ¿Qué se entiende por "tanto cargado dinámicamente como vinculado estáticamente"? ¿Se puede cargar algo dinámicamente Y enlazar estáticamente, o son mutuamente excluyentes?

  • "... la clase o categoría recién cargada implementa un método que puede responder" ¿Qué método? ¿Responder cómo?


En cuanto a + inicializar, la documentación dice:

initialize se invoca solo una vez por clase. Si desea realizar una inicialización independiente para la clase y para las categorías de la clase, debe implementar métodos de carga.

Entiendo que esto significa "si estás intentando configurar la clase ... no uses initialize". Bien vale. ¿Cuándo o por qué anularía la inicialización entonces?

edelaney05
fuente

Respuestas:

184

El loadmensaje

El tiempo de ejecución envía el loadmensaje a cada objeto de clase, muy poco después de que el objeto de clase se cargue en el espacio de direcciones del proceso. Para las clases que forman parte del archivo ejecutable del programa, el tiempo de ejecución envía el loadmensaje muy temprano en la vida útil del proceso. Para las clases que están en una biblioteca compartida (cargada dinámicamente), el tiempo de ejecución envía el mensaje de carga justo después de que la biblioteca compartida se cargue en el espacio de direcciones del proceso.

Además, el tiempo de ejecución solo envía loada un objeto de clase si ese objeto de clase en sí mismo implementa el loadmétodo. Ejemplo:

@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

El tiempo de ejecución envía el loadmensaje al Superclassobjeto de clase. No , no enviar el loadmensaje al Subclassobjeto de clase, a pesar de que Subclasshereda el método de Superclass.

El tiempo de ejecución envía el loadmensaje a un objeto de clase después de haber enviado el loadmensaje a todos los objetos de superclase de la clase (si esos objetos de superclase se implementan load) y todos los objetos de clase en las bibliotecas compartidas a las que se vincula. Pero aún no sabe qué otras clases de su propio ejecutable han recibido load.

Cada clase que su proceso carga en su espacio de direcciones recibirá un loadmensaje, si implementa el loadmétodo, independientemente de si su proceso hace algún otro uso de la clase.

Puede ver cómo el tiempo de ejecución busca el loadmétodo como un caso especial en _class_getLoadMethodof objc-runtime-new.mmy lo llama directamente desde call_class_loadsadentro objc-loadmethod.mm.

El tiempo de ejecución también ejecuta el loadmétodo de cada categoría que carga, incluso si se implementan varias categorías en la misma clase load. Esto es inusual. Normalmente, si dos categorías definen el mismo método en la misma clase, uno de los métodos "ganará" y se usará, y el otro método nunca será llamado.

El initializemétodo

El tiempo de ejecución llama al initializemétodo en un objeto de clase justo antes de enviar el primer mensaje (distinto de loado initialize) al objeto de clase o cualquier instancia de la clase. Este mensaje se envía utilizando el mecanismo normal, por lo que si su clase no se implementa initialize, pero hereda de una clase que lo hace, entonces su clase usará su superclase initialize. El tiempo de ejecución enviará initializeprimero a todas las superclases de una clase (si las superclases aún no se han enviado initialize).

Ejemplo:

@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;
}

Este programa imprime dos líneas de salida:

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

Dado que el sistema envía el initializemétodo con pereza, una clase no recibirá el mensaje a menos que su programa realmente envíe mensajes a la clase (o una subclase, o instancias de la clase o subclases). Y para cuando lo reciba initialize, todas las clases en su proceso ya deberían haber recibido load(si corresponde).

La forma canónica de implementar initializees esta:

@implementation Someclass

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

El objetivo de este patrón es evitar Someclassreinicializarse cuando tiene una subclase que no se implementa initialize.

El motor de ejecución envía el initializemensaje en la _class_initializefunción en formato objc-initialize.mm. Puede ver que utiliza objc_msgSendpara enviarlo, que es la función normal de envío de mensajes.

Otras lecturas

Consulte las preguntas y respuestas del viernes de Mike Ash sobre este tema.

Rob Mayoff
fuente
25
Debe tener en cuenta que +loadse envía por separado para categorías; es decir, cada categoría de una clase puede contener su propio +loadmétodo.
Jonathan Grynspan
1
También tenga en cuenta que initializese invocará correctamente mediante un loadmétodo, si es necesario, debido a la loadreferencia a la entidad no inicializada. ¡Esto puede (extrañamente, pero sensiblemente) llevar a initializecorrer antes load! Eso es lo que he observado, de todos modos. Esto parece ser contrario a "Y para cuando lo reciba initialize, todas las clases en su proceso ya deberían haber recibido load(si corresponde)".
Benjohn
5
Recibes loadprimero. A continuación, puede recibir initializemientras loadaún se está ejecutando.
rob mayoff
1
@robmayoff ¿no es necesario agregar líneas [super initialize] y [super load], dentro de los métodos respectivos?
DamithH
1
Eso suele ser una mala idea, porque el motor de ejecución ya ha enviado ambos mensajes a todas sus superclases antes de enviárselos a usted.
rob mayoff
17

Lo que significa es no anular +initializeen una categoría, probablemente romperá algo.

+loadse llama una vez por clase o categoría que implementa +load, tan pronto como se carga esa clase o categoría. Cuando dice "vinculado estáticamente" significa compilado en el binario de su aplicación. Los +loadmétodos de las clases así compiladas se ejecutarán cuando se inicie la aplicación, probablemente antes de que entre main(). Cuando dice "cargado dinámicamente", significa cargado a través de paquetes de complementos o una llamada a dlopen(). Si está en iOS, puede ignorar ese caso.

+initializese llama la primera vez que se envía un mensaje a la clase, justo antes de que maneje ese mensaje. Esto (obviamente) solo ocurre una vez. Si anula +initializeen una categoría, sucederá una de estas tres cosas:

  • se llama a la implementación de su categoría y la implementación de la clase no
  • se llama a la implementación de la categoría de otra persona; nada de lo que escribiste hace
  • su categoría aún no se ha cargado y nunca se llama a su implementación.

Esta es la razón por la que nunca debe anular +initializeuna categoría; de hecho, es bastante peligroso intentar reemplazar cualquier método en una categoría porque nunca está seguro de qué está reemplazando o si su propio reemplazo será reemplazado por otra categoría.

Por cierto, otro tema a considerar +initializees que si alguien te subclasifica, potencialmente te llamarán una vez para tu clase y una vez para cada subclase. Si está haciendo algo como configurar staticvariables, querrá protegerse contra eso: ya sea con dispatch_once()o mediante pruebas self == [MyClass class].


fuente