Creo que esta es una gran pregunta, y es una pena que, en lugar de abordar la pregunta real, la mayoría de las respuestas hayan eludido el problema y simplemente hayan dicho que no utilicen swizzling.
Usar el método chisporrotear es como usar cuchillos afilados en la cocina. Algunas personas tienen miedo de los cuchillos afilados porque piensan que se cortarán mucho, pero la verdad es que los cuchillos afilados son más seguros .
El método swizzling se puede utilizar para escribir un código mejor, más eficiente y más fácil de mantener. También puede ser abusado y provocar errores horribles.
Antecedentes
Al igual que con todos los patrones de diseño, si somos plenamente conscientes de las consecuencias del patrón, podemos tomar decisiones más informadas sobre si usarlo o no. Los singletons son un buen ejemplo de algo que es bastante controvertido y, por una buena razón, son realmente difíciles de implementar correctamente. Sin embargo, muchas personas aún optan por usar singletons. Lo mismo se puede decir sobre el swizzling. Debe formarse su propia opinión una vez que comprenda completamente tanto lo bueno como lo malo.
Discusión
Estas son algunas de las dificultades del método swizzling:
- Método swizzling no es atómico
- Cambia el comportamiento del código no propiedad
- Posibles conflictos de nombres
- Swizzling cambia los argumentos del método
- El orden de las olas es importante
- Difícil de entender (se ve recursivo)
- Difícil de depurar
Todos estos puntos son válidos, y al abordarlos podemos mejorar tanto nuestra comprensión del método swizzling como la metodología utilizada para lograr el resultado. Tomaré cada uno a la vez.
Método swizzling no es atómico
Todavía tengo que ver una implementación del método swizzling que sea seguro de usar simultáneamente 1 . En realidad, esto no es un problema en el 95% de los casos en los que desea utilizar el método swizzling. Por lo general, simplemente desea reemplazar la implementación de un método y desea que esa implementación se use durante toda la vida útil de su programa. Esto significa que debes hacer que tu método se mezcle +(void)load
. El load
método de clase se ejecuta en serie al inicio de su aplicación. No tendrás problemas con la concurrencia si haces swizzling aquí. Si se va a swizzle en +(void)initialize
, sin embargo, usted podría terminar con una condición de carrera en su aplicación y el tiempo de ejecución swizzling podría terminar en un estado extraño.
Cambia el comportamiento del código no propiedad
Este es un problema con swizzling, pero es una especie de punto. El objetivo es poder cambiar ese código. La razón por la que las personas señalan que esto es un gran problema es porque no solo está cambiando las cosas para la única instancia para la NSButton
que desea cambiar las cosas, sino para todas las NSButton
instancias de su aplicación. Por esta razón, debe ser cauteloso cuando se bañe, pero no necesita evitarlo por completo.
Piénselo de esta manera ... si anula un método en una clase y no llama al método de superclase, puede causar problemas. En la mayoría de los casos, la superclase espera que se llame a ese método (a menos que se documente lo contrario). Si aplica este mismo pensamiento al swizzling, ha cubierto la mayoría de los problemas. Siempre llame a la implementación original. Si no lo hace, probablemente esté cambiando demasiado para estar seguro.
Posibles conflictos de nombres
Los conflictos de nombres son un problema en todo Cocoa. Frecuentemente prefijamos nombres de clases y nombres de métodos en categorías. Desafortunadamente, los conflictos de nombres son una plaga en nuestro idioma. Sin embargo, en el caso de swizzling, no tienen que serlo. Solo necesitamos cambiar ligeramente la forma en que pensamos sobre el método. La mayor cantidad de swizzling se hace así:
@interface NSView : NSObject
- (void)setFrame:(NSRect)frame;
@end
@implementation NSView (MyViewAdditions)
- (void)my_setFrame:(NSRect)frame {
// do custom work
[self my_setFrame:frame];
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}
@end
Esto funciona bien, pero ¿qué pasaría si my_setFrame:
se definiera en otro lugar? Este problema no es exclusivo de swizzling, pero podemos solucionarlo de todos modos. La solución alternativa tiene un beneficio adicional de abordar también otras dificultades. Esto es lo que hacemos en su lugar:
@implementation NSView (MyViewAdditions)
static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);
static void MySetFrame(id self, SEL _cmd, NSRect frame) {
// do custom work
SetFrameIMP(self, _cmd, frame);
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}
@end
Si bien esto se parece un poco menos a Objective-C (ya que utiliza punteros de función), evita cualquier conflicto de nombres. En principio, está haciendo exactamente lo mismo que el swizzling estándar. Esto puede ser un poco un cambio para las personas que han estado usando swizzling como se ha definido durante un tiempo, pero al final, creo que es mejor. El método de swizzling se define así:
typedef IMP *IMPPointer;
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
IMP imp = NULL;
Method method = class_getInstanceMethod(class, original);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(class, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}
}
if (imp && store) { *store = imp; }
return (imp != NULL);
}
@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end
El cambio de nombre al cambiar de método cambia los argumentos del método
Este es el más grande en mi mente. Esta es la razón por la que no se debe hacer un método estándar. Está cambiando los argumentos pasados a la implementación del método original. Aquí es donde sucede:
[self my_setFrame:frame];
Lo que hace esta línea es:
objc_msgSend(self, @selector(my_setFrame:), frame);
Que utilizará el tiempo de ejecución para buscar la implementación de my_setFrame:
. Una vez que se encuentra la implementación, invoca la implementación con los mismos argumentos que se dieron. La implementación que encuentra es la implementación original setFrame:
, por lo que continúa y llama a eso, pero el _cmd
argumento no es setFrame:
como debería ser. Es ahora my_setFrame:
. Se llama a la implementación original con un argumento que nunca esperó recibir. Esto no está bien.
Hay una solución simple: use la técnica alternativa de swizzling definida anteriormente. ¡Los argumentos permanecerán sin cambios!
El orden de las olas es importante
El orden en que los métodos se mezclan importa. Suponiendo setFrame:
que solo se define en NSView
, imagine este orden de cosas:
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
¿Qué sucede cuando el método NSButton
está activado? Bueno, la mayor cantidad de swizzling asegurará que no esté reemplazando la implementación de setFrame:
todas las vistas, por lo que extraerá el método de instancia. Esto usará la implementación existente para redefinir setFrame:
en la NSButton
clase para que el intercambio de implementaciones no afecte a todas las vistas. La implementación existente es la definida en NSView
. Lo mismo sucederá cuando se encienda NSControl
(nuevamente usando la NSView
implementación).
Cuando llama setFrame:
a un botón, llamará a su método swizzled y luego saltará directamente al setFrame:
método originalmente definido NSView
. No se llamarán las implementaciones NSControl
y NSView
swizzled.
Pero, ¿y si el orden fuera:
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
Desde el punto de vista swizzling se lleva a cabo en primer lugar, la swizzling control será capaz de tirar hacia arriba el método correcto. Del mismo modo, dado que el control de swizzling fue antes del botón de swizzling, el botón levantará la implementación de control de swizzled setFrame:
. Esto es un poco confuso, pero este es el orden correcto. ¿Cómo podemos asegurar este orden de cosas?
Nuevamente, solo usa load
para mezclar cosas. Si te sumerges load
y solo haces cambios en la clase que se está cargando, estarás a salvo. El load
método garantiza que se llamará al método de carga de superclase antes de cualquier subclase. ¡Obtendremos el orden exacto correcto!
Difícil de entender (se ve recursivo)
Mirando un método swizzled definido tradicionalmente, creo que es realmente difícil saber qué está pasando. Pero mirando la forma alternativa en que hemos hecho swizzling arriba, es bastante fácil de entender. Este ya ha sido resuelto!
Difícil de depurar
Una de las confusiones durante la depuración es ver un extraño retroceso donde los nombres mezclados se mezclan y todo se confunde en tu cabeza. Nuevamente, la implementación alternativa aborda esto. Verá funciones claramente nombradas en las trazas inversas. Aún así, el swizzling puede ser difícil de depurar porque es difícil recordar qué impacto está teniendo el swizzling. Documente bien su código (incluso si cree que es el único que lo verá). Sigue las buenas prácticas y estarás bien. No es más difícil de depurar que el código multiproceso.
Conclusión
El método swizzling es seguro si se usa correctamente. Una medida de seguridad simple que puede tomar es solo entrar load
. Al igual que muchas cosas en la programación, puede ser peligroso, pero comprender las consecuencias le permitirá usarlo correctamente.
1 Usando el método swizzling definido anteriormente, puede hacer que las cosas sean seguras si usa trampolines. Necesitarías dos trampolines. Al comienzo del método, tendría que asignar el puntero de función store
, a una función que girara hasta que la dirección a la que store
apuntaba cambiara. Esto evitaría cualquier condición de carrera en la que se llamara al método swizzled antes de que pudiera establecer el store
puntero de función. Debería usar un trampolín en el caso de que la implementación no esté ya definida en la clase y tener la búsqueda en el trampolín y llamar al método de superclase correctamente. La definición del método para que busque dinámicamente la súper implementación garantizará que el orden de las llamadas no tenga importancia.