Swift y Objective-C

+load frente a +initialize: momento de invocación y trampas de herencia

¿Por qué el código de method swizzling siempre está dentro de +load? ¿No se puede poner en el parecido +initialize?

5 min de lectura
Imagen de portada de +load frente a +initialize: momento de invocación y trampas de herencia

¿Por qué el código de method swizzling siempre está dentro de +load? ¿No se puede poner en el parecido +initialize?

+load y +initialize parecen métodos que se «invocan una vez cuando la clase está preparada», pero difieren por completo en el momento y la forma de invocación y en las reglas de herencia. También es un tema habitual en entrevistas de Objective-C y, si se entiende mal, es fácil acabar preguntándose «¿por qué este código se ejecuta dos veces?». Merece la pena resumirlo.


Tabla comparativa rápida

+load +initialize
Momento de invocación Cuando el runtime carga la clase (antes de main) Justo antes de que la clase reciba su primer mensaje (lazy)
Forma de invocación Llamada directa mediante puntero a función A través de objc_msgSend
Categorías Las implementaciones de la clase y la categoría se invocan por separado La implementación de la categoría sobrescribe la de la clase
Herencia Solo se invoca la clase que lo implementa Se hereda; puede invocarse varias veces por las subclases
¿Si no se usa? Se invoca igualmente Nunca se invoca si no se usa la clase

Veamos paso a paso los puntos que no resultan evidentes solo con la tabla.


+load: antes de main, siempre

+load se invoca cuando el runtime carga el binario que contiene la clase o categoría, es decir, antes de que se ejecute la función main. Se invoca aunque la aplicación nunca use esa clase.

Su forma de invocación es peculiar. Se llama directamente mediante un puntero a función, sin pasar por objc_msgSend. Por eso no se aplican las reglas habituales de sobrescritura.

  • Si una subclase no implementa +load, la implementación del padre no se invoca en su lugar.
  • La +load de la clase y la +load de la categoría se invocan por separado; no se sobrescriben.
  • El orden está garantizado: la +load de la clase padre antes que la de la hija, y la +load de la clase antes que la de la categoría.

Estas propiedades encajan exactamente con el swizzling. Aunque +load se implemente en una categoría, no interfiere con la +load de la clase original y se ejecuta una vez de forma fiable en el momento más temprano de la vida de la aplicación.

Pero tiene un coste. +load se carga directamente al tiempo de inicio de la aplicación. Como la +load de todas las clases se ejecuta secuencialmente antes de main, el trabajo pesado aquí retrasa la primera pantalla. Por eso Apple recomienda desde hace años «evitar +load siempre que sea posible». De hecho, dentro de +load es posible que aún no se hayan cargado otras clases fuera de la imagen a la que pertenece self, lo que limita las operaciones disponibles.

Diagrama que muestra los momentos de invocación de +load y +initialize en la línea temporal de ejecución de la aplicación
La única bifurcación es si la llamada pasa por msgSend

+initialize: justo antes del primer mensaje, de forma perezosa

+initialize adopta la estrategia opuesta. El runtime lo invoca justo antes de que la clase reciba su primer mensaje. Si la aplicación nunca usa la clase, nunca se invoca. Es un punto de inicialización lazy sin coste para el tiempo de inicio.

Como pasa por objc_msgSend, se aplican las reglas de herencia como en un método normal. Aquí aparece la famosa trampa.

@implementation Animal
+ (void)initialize {
    NSLog(@"initialize: %@", self);
}
@end

@interface Dog : Animal
@end
@implementation Dog
@end

Al enviar el primer mensaje a Dog, el registro queda así.

initialize: Animal
initialize: Dog

El +initialize de Animal se ejecuta dos veces: una por Animal y otra porque Dog, que no implementa +initialize, hereda y ejecuta la implementación del padre. Por eso la implementación convencional de +initialize incluye una comprobación de clase.

+ (void)initialize {
    if (self == [Animal class]) {
        // Solo la inicialización que corresponde realmente a  Animal 
    }
}

Como referencia, +initialize es seguro para subprocesos por sí mismo, ya que el runtime bloquea por clase antes de invocarlo. No hace falta añadir dispatch_once.


Criterios prácticos de elección

El criterio es sencillo.

  • Tareas que deben ejecutarse «siempre y lo antes posible», como el swizzling o el registro de clases+load (con una implementación mínima)
  • Preparación necesaria solo cuando se usa la clase+initialize (la comprobación de self es obligatoria)
  • La mayoría de las inicializaciones → en realidad ninguna de las dos; basta con un dispatch_oncesingleton o una propiedad lazy

Swift no tiene este dilema. Swift no ofrece nada equivalente a +load. Tampoco existe un mecanismo oficial para insertar código ejecutable global antes de main. En su lugar, las propiedades de tipo (static let) garantizan lazy y seguridad de subprocesos a nivel del lenguaje, sustituyendo el papel de +initialize. Es una de las ventajas estructurales de Swift para el rendimiento de inicio de la aplicación.

Ilustración que representa +load antes de main y +initialize lazy como una pista de carreras
Uno empieza a correr antes de la salida; el otro duerme hasta la primera llamada

Resumen

  • +load se invoca antes de main, siempre y directamente mediante un puntero a función; se invocan por separado las implementaciones de la clase y la categoría
  • +initialize se invoca justo antes del primer mensaje, de forma lazy y mediante msgSend; nunca se invoca en clases no utilizadas
  • +initialize puede ejecutarse varias veces por la herencia, por lo que la if (self == [MyClass class]) comprobación es convencional
  • Por qué el swizzling vive en +load: momento más temprano + llamada independiente de la categoría + ejecución única garantizada
  • Abusar de +load retrasa directamente el inicio de la aplicación; la inicialización pesada debe posponerse con lazy

Al leerlo junto con otros artículos de la serie sobre el runtime (objc_msgSend, reenvío de mensajes, swizzling y KVO), se ve que la diferencia entre +load y +initialize acaba separándose en un único eje: si la llamada pasa por msgSend. Al entender la ruta de invocación, ya no hace falta memorizar las reglas.

Seguir leyendo