¿Cuáles son los peligros del método Swizzling en Objective-C?

235

He escuchado a personas decir que el método swizzling es una práctica peligrosa. Incluso el nombre swizzling sugiere que es un poco tramposo.

El método Swizzling está modificando la asignación para que el selector de llamadas A invoque la implementación B. Un uso de esto es extender el comportamiento de las clases de código cerrado.

¿Podemos formalizar los riesgos para que cualquiera que decida si usar swizzling pueda tomar una decisión informada sobre si vale la pena por lo que está tratando de hacer?

P.ej

  • Nombrar colisiones : si la clase luego amplía su funcionalidad para incluir el nombre del método que ha agregado, causará una gran cantidad de problemas. Reduzca el riesgo al nombrar con sensatez métodos swizzled
Robert
fuente
Ya no es muy bueno obtener algo más que esperas del código que has leído
Martin Babacaev
Esto suena como una forma impura de hacer lo que los decoradores de Python de forma muy limpia
Claudiu

Respuestas:

439

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 loadmé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 NSButtonque desea cambiar las cosas, sino para todas las NSButtoninstancias 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 _cmdargumento 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 NSButtonestá 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 NSButtonclase 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 NSViewimplementació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 NSControly NSViewswizzled.

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 loadpara mezclar cosas. Si te sumerges loady solo haces cambios en la clase que se está cargando, estarás a salvo. El loadmé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 storeapuntaba cambiara. Esto evitaría cualquier condición de carrera en la que se llamara al método swizzled antes de que pudiera establecer el storepuntero 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.

wbyoung
fuente
24
Sorprendentemente informativa y técnicamente sólida respuesta. La discusión es realmente interesante. Gracias por tomarte el tiempo de escribir esto.
Ricardo Sanchez-Saez
¿Podría señalar si la experiencia de intercambio es aceptable en las tiendas de aplicaciones? Te estoy dirigiendo a stackoverflow.com/questions/8834294 también.
Dickey Singh
3
Swizzling se puede utilizar en la tienda de aplicaciones. Muchas aplicaciones y marcos lo hacen (incluido el nuestro). Todo lo anterior aún se mantiene, y no puede mezclar métodos privados (en realidad, técnicamente puede, pero correrá el riesgo de rechazo y los peligros de esto son para un hilo diferente).
wbyoung
Respuesta asombrosa Pero, ¿por qué hacer swizzling en class_swizzleMethodAndStore () en lugar de directamente en + swizzle: with: store :? ¿Por qué esta función adicional?
Frizlab
2
@Frizlab buena pregunta! Realmente es solo una cuestión de estilo. Si está escribiendo un montón de código que funciona directamente con la API de tiempo de ejecución de Objective-C (en C), entonces es bueno poder llamarlo estilo C por coherencia. La única razón por la que puedo pensar además de esto es si escribiste algo en C puro, entonces aún es más invocable. Sin embargo, no hay razón para que no pueda hacerlo todo en el método Objective-C.
wbyoung
12

Primero definiré exactamente lo que quiero decir con método swizzling:

  • Reenrutar todas las llamadas que se enviaron originalmente a un método (llamado A) a un nuevo método (llamado B).
  • Somos dueños del Método B
  • No poseemos el método A
  • El método B funciona y luego llama al método A.

El método swizzling es más general que esto, pero este es el caso que me interesa.

Peligros

  • Cambios en la clase original . No somos dueños de la clase que estamos intercambiando. Si la clase cambia, nuestro swizzle puede dejar de funcionar.

  • Difícil de mantener . No solo tienes que escribir y mantener el método swizzled. tienes que escribir y mantener el código que preforma el swizzle

  • Difícil de depurar . Es difícil seguir el flujo de una llovizna, es posible que algunas personas ni siquiera se den cuenta de que la tormenta se ha formado previamente. Si hay errores introducidos desde la tormenta (quizás debido a cambios en la clase original) serán difíciles de resolver.

En resumen, debe mantener el swizzling al mínimo y considerar cómo los cambios en la clase original podrían afectar su swizzle. También debe comentar y documentar claramente lo que está haciendo (o simplemente evitarlo por completo).

Robert
fuente
@ Todos combiné sus respuestas en un formato agradable. Siéntase libre de editar / agregarle. Gracias por tu contribución.
Robert
Soy fanático de tu psicología. "Con un gran poder swizzling viene una gran responsabilidad swizzling".
Jacksonkr
7

No es el swizzling en sí lo que es realmente peligroso. El problema es, como usted dice, que a menudo se usa para modificar el comportamiento de las clases de marco. Supone que sabes algo sobre cómo funcionan esas clases privadas que es "peligroso". Incluso si sus modificaciones funcionan hoy, siempre existe la posibilidad de que Apple cambie la clase en el futuro y haga que su modificación se rompa. Además, si muchas aplicaciones diferentes lo hacen, es mucho más difícil para Apple cambiar el marco sin romper una gran cantidad de software existente.

Caleb
fuente
Yo diría que tales desarrolladores deberían cosechar lo que siembran: unieron estrechamente su código a una implementación particular. Por lo tanto, es su culpa si esa implementación cambia de una manera sutil que no debería haber roto su código si no fuera por su decisión explícita de acoplar su código tan estrictamente como lo hicieron.
Arafangion
Claro, pero digamos que una aplicación muy popular se rompe bajo la próxima versión de iOS. Es fácil decir que es culpa del desarrollador y que deberían saberlo mejor, pero hace que Apple se vea mal a los ojos del usuario. No veo ningún daño en mezclar tus propios métodos o incluso clases si quieres hacer eso, aparte de tu punto de que puede hacer que el código sea algo confuso. Comienza a ser un poco más arriesgado cuando se intercambia el código controlado por otra persona, y esa es exactamente la situación en la que la vibración parece más tentadora.
Caleb
Apple no es el único proveedor de clases base que se pueden modificar a través de siwzzling. Su respuesta debe ser editada para incluir a todos.
AR
@AR Quizás, pero la pregunta está etiquetada con iOS y iPhone, por lo que deberíamos considerarla en ese contexto. Dado que las aplicaciones de iOS deben vincular estáticamente cualquier biblioteca y framework que no sea el proporcionado por iOS, Apple es, de hecho, la única entidad que puede cambiar la implementación de las clases de framework sin alguna participación de los desarrolladores de aplicaciones. Pero sí estoy de acuerdo con el punto de que problemas similares afectan a otras plataformas, y diría que el consejo también puede generalizarse más allá de la confusión. En general, el consejo es: mantén tus manos para ti mismo. Evite modificar componentes que otras personas mantienen.
Caleb
Método swizzling puede ser tan peligroso. pero a medida que salen los marcos, no omiten por completo los anteriores. aún debe actualizar el marco base que su aplicación espera. y esa actualización rompería tu aplicación. es probable que no sea un problema tan grande cuando se actualiza el ios. Mi patrón de uso era anular el reuseIdentifier predeterminado. Como no tenía acceso a la variable _reuseIdentifier y no quería reemplazarla a menos que no se proporcionara, mi única solución era subclasificar cada una y proporcionar el identificador de reutilización, o cambiar y anular si era nula. El tipo de implementación es clave ...
The Lazy Coder
5

Utilizado con cuidado y prudencia, puede conducir a un código elegante, pero por lo general, solo conduce a un código confuso.

Digo que debe prohibirse, a menos que sepa que presenta una oportunidad muy elegante para una tarea de diseño en particular, pero necesita saber claramente por qué se aplica bien a la situación y por qué las alternativas no funcionan de manera elegante para la situación. .

Por ejemplo, una buena aplicación del método swizzling es isa swizzling, que es cómo ObjC implementa la Observación de valor clave.

Un mal ejemplo podría ser confiar en el método swizzling como un medio para extender sus clases, lo que conduce a un acoplamiento extremadamente alto.

Arafangion
fuente
1
-1 por la mención de KVO, en el contexto del método swizzling . isa swizzling es solo otra cosa.
Nikolai Ruhe
@NikolaiRuhe: Por favor, siéntase libre de proporcionar referencias para que pueda iluminarme.
Arafangion
Aquí hay una referencia a los detalles de implementación de KVO . El método swizzling se describe en CocoaDev.
Nikolai Ruhe
5

Aunque he usado esta técnica, me gustaría señalar que:

  • Oculta su código porque puede causar efectos secundarios no documentados, aunque deseados. Cuando uno lee el código, puede desconocer el comportamiento de los efectos secundarios que se requieren a menos que recuerde buscar en la base del código para ver si ha sido rociado. No estoy seguro de cómo solucionar este problema porque no siempre es posible documentar todos los lugares donde el código depende del comportamiento de efecto secundario.
  • Puede hacer que su código sea menos reutilizable porque alguien que encuentra un segmento de código que depende del comportamiento de swizzled que le gustaría usar en otro lugar no puede simplemente cortarlo y pegarlo en alguna otra base de código sin también encontrar y copiar el método swizzled.
usuario3288724
fuente
4

Siento que el mayor peligro es crear muchos efectos secundarios no deseados, completamente por accidente. Estos efectos secundarios pueden presentarse como 'errores' que a su vez lo llevan por el camino equivocado para encontrar la solución. En mi experiencia, el peligro es un código ilegible, confuso y frustrante. Algo así como cuando alguien abusa de los punteros de función en C ++.

Arkansas
fuente
Ok, entonces el problema es que es difícil de depurar. Los problemas se deben a que los revisores no se dan cuenta / olvidan que el código ha sido modificado y están buscando en el lugar equivocado al depurar.
Robert
3
Si mas o menos. Además, cuando se usa en exceso, puede entrar en una cadena interminable o red de olas. ¿Imagina lo que podría suceder cuando cambias solo uno de ellos en algún momento en el futuro? Comparo la experiencia de apilar tazas de té o jugar 'code jenga'
AR
4

Puede terminar con un código de aspecto extraño como

- (void)addSubview:(UIView *)view atIndex:(NSInteger)index {
    //this looks like an infinite loop but we're swizzling so default will get called
    [self addSubview:view atIndex:index];

del código de producción real relacionado con alguna magia de UI.

cuantumpotato
fuente
3

El método swizzling puede ser muy útil en las pruebas unitarias.

Le permite escribir un objeto simulado y utilizar ese objeto simulado en lugar del objeto real. Su código para permanecer limpio y su prueba de unidad tiene un comportamiento predecible. Digamos que desea probar algún código que usa CLLocationManager. Su prueba unitaria podría confundir startUpdatingLocation para que alimente un conjunto predeterminado de ubicaciones a su delegado y su código no tenga que cambiar.

usuario3288724
fuente
isa-swizzling sería una mejor opción.
vikingosegundo