¿Cuáles son los peligros de la Oscilación del Método en Objective C?


He oído a la gente decir que el método swizzling es una práctica peligrosa. Incluso el nombre swizzling sugiere que es un poco tramposo.

Method Swizzling está modificando la asignación para que llamar al selector A realmente invoque la implementación B. Un uso de esto es para extender el comportamiento de las clases de código cerrado.

¿Podemos formalizar los riesgos para que cualquiera que esté decidiendo si usar swizzling pueda tomar una decisión informada si vale la pena por lo que estamos tratando de hacer.

P. Ej.

  • Naming Collisions: Si la clase extiende más tarde su funcionalidad para incluir el nombre del método que ha agregado, causará una gran cantidad de problemas. Reduzca el riesgo nombrando con sensatez los métodos swizzled.
Author: James Webster, 2011-03-17

8 answers

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 han eludido el tema y simplemente han dicho que no use swizzling.

Usar el método sizzling es como usar cuchillos afilados en la cocina. Algunas personas tienen miedo de los cuchillos afilados porque piensan que se cortarán mal, pero la verdad es que los cuchillos afilados son más seguros.

Método swizzling se puede utilizar para escribir mejor, más eficiente, más código mantenible. También puede ser abusado y conducir a errores horribles.

Antecedentes

Al igual que con todos los patrones de diseño, si somos plenamente conscientes de las consecuencias del patrón, somos capaces de 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 todavía optan por usar singletons. Lo mismo se puede decir acerca de swizzling. Usted debe formar su propia opinión una vez que entienda completamente lo bueno y lo malo.

Discusión

Aquí están algunas de las trampas del método swizzling:

  • El método swizzling no es atómico
  • Cambia el comportamiento del código no poseído
  • Posibles conflictos de nombres
  • Swizzling cambia los argumentos del método
  • El orden de los swizzles importa{[58]]}
  • Difícil de entender (parece 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.

El método swizzling no es atómico

Todavía no he visto una implementación del método swizzling que sea seguro de usar simultáneamente1. En realidad, esto no es un problema en el 95% de los casos en los que te gustaría usar el método swizzling. Por lo general, simplemente quieres reemplace 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 usted debe hacer su método swizzling en +(void)load. El método de clase load se ejecuta en serie al inicio de su aplicación. No tendrá ningún problema con la concurrencia si hace su swizzling aquí. Sin embargo, si tuviera que swizzle en +(void)initialize, podría terminar con una condición de carrera en su implementación swizzling y el tiempo de ejecución podría terminar en un extraño estado.

Cambia el comportamiento del código no poseído

Este es un problema con swizzling, pero es más o menos el punto. El objetivo es poder cambiar ese código. La razón por la que la gente señala esto como un gran problema es porque no solo está cambiando las cosas para la única instancia de NSButton para la que desea cambiar las cosas, sino para todas las instancias de NSButton en su aplicación. Por esta razón, debe ser cauteloso cuando se balancea, pero no es necesario evitarlo en total.

Piénsalo de esta manera... si anula un método en una clase y no llama al método 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 aplicas este mismo pensamiento a swizzling, has cubierto la mayoría de los problemas. Siempre llame a la implementación original. Si no lo haces, probablemente estés cambiando demasiado para estar seguro.

Posibles conflictos de nombres

Denominación los conflictos son un problema en todo el Cacao. Con frecuencia ponemos prefijos a nombres de clases y nombres de métodos en categorías. Desafortunadamente, los conflictos de nombres son una plaga en nuestro idioma. En el caso de swizzling, sin embargo, no tienen que ser. Solo tenemos que cambiar la forma en que pensamos sobre el método girando ligeramente. La mayoría de los 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 trabajar alrededor de él de todos modos. La solución tiene un beneficio adicional de abordar otros escollos también. 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

Mientras que esto se parece un poco menos a Objective-C (ya que está usando 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 de 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 swizzling es 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

Cambiar el nombre de los métodos cambia los argumentos del método

Este es el grande en mi mente. Esta es la razón por la que el método estándar swizzling no debe hacerse. 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 el implementación con los mismos argumentos que se dieron. La implementación que encuentra es la implementación original de setFrame:, por lo que sigue adelante y llama a eso, pero el argumento _cmd no es setFrame: como debería ser. Ahora es my_setFrame:. La implementación original está siendo llamada con un argumento que nunca esperó que recibiría. Esto no es bueno.

Hay una solución simple: use la técnica alternativa de swizzling definida anteriormente. ¡Los argumentos permanecerán sin cambios!

El orden de los asuntos swizzles

El orden en el que los métodos se mueven importa. Suponiendo que setFrame: 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 en NSButton se balancea? Bueno, la mayoría de swizzling se asegurará de que no está reemplazando la implementación de setFrame: para todas las vistas, por lo que mostrará el método de instancia. Esto utilizará la implementación existente para redefinir setFrame: en la clase NSButton para que el intercambio las implementaciones no afectan a todas las vistas. La implementación existente es la definida en NSView. Lo mismo sucederá cuando se haga swizzling en NSControl (de nuevo usando la implementación NSView).

Cuando llame a setFrame: en un botón, por lo tanto, llamará a su método swizzled, y luego saltará directamente al método setFrame: definido originalmente en NSView. Las implementaciones swizzled NSControl y NSView no serán llamadas.

Pero ¿y si la orden fuera: {[41]]}

[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 ver swizzling se lleva a cabo primero, el control swizzling será capaz de pull up el método correcto. Del mismo modo, dado que el control swizzling estaba antes del botón swizzling, el botón se abrirá la implementación swizzled del control de setFrame:. Esto es un poco confuso, pero este es el orden correcto. ¿Cómo podemos asegurar este orden de cosas?

Una vez más, simplemente use load para balancear las cosas. Si hace swizzle en load y solo realiza cambios en la clase que se está cargando, estarás a salvo. El método load garantiza que el método super class load será llamado antes de cualquier subclase. ¡Conseguiremos el orden exacto!

Difícil de entender (parece recursivo)

Mirando un método swizzled tradicionalmente definido, creo que es muy difícil saber qué está pasando. Pero mirando la forma alternativa que hemos hecho swizzling arriba, es bastante fácil de entender. ¡Esto ya está resuelto!

Difícil de depurar

Uno de las confusiones durante la depuración es ver un extraño backtrace donde los nombres swizzled se mezclan y todo se confunde en su cabeza. Una vez más, la aplicación alternativa aborda esta cuestión. Verás claramente las funciones con nombre en trazas. Aún así, 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 más difícil de depurar que el código multihilo.

Conclusión

El método swizzling es seguro si se usa correctamente. Una simple medida de seguridad que puede tomar es solo balancearse en load. Como muchas cosas en la programación, puede ser peligroso, pero entender las consecuencias te permitirá usarlo correctamente.


1 Usando el método de swizzling definido anteriormente, podría hacer que las cosas sean seguras si utilizara camas elásticas. Necesitarías dos camas elásticas. Al principio del método, tendría que asignar el puntero de función, store, a una función que giró hasta que la dirección a la que store apuntó cambió. Esto evitaría cualquier condición de carrera en la que se llamara al método swizzled antes de que pudiera establecer el puntero de la función store. A continuación, tendría que utilizar un trampolín en el caso de que la implementación no está ya definida en la clase y tener la búsqueda de trampolín y llamar al método super clase correctamente. Definiendo el método para que dinámicamente busca la súper implementación se asegurará de que el orden de las llamadas swizzling no importa.

 409
Author: wbyoung,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-21 16:30:28

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

  • Redirigir todas las llamadas que se enviaron originalmente a un método (llamado A) a un nuevo método (llamado B).
  • Tenemos el Método B
  • No tenemos método A
  • El método B hace algún trabajo y luego llama al método A.

El método swizzling es más general que esto, pero este es el caso en el que estoy interesado.

Peligros:

  • Cambios en la clase original . No somos dueños del clase que estamos girando. 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 un swizzle, algunas personas pueden ni siquiera darse cuenta de que el swizzle ha sido preformado. Si hay errores introducidos desde el swizzle (tal vez las cuotas a los 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).

 11
Author: Robert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-18 20:38:29

No es el remolino en sí lo que es realmente peligroso. El problema es, como usted dice, que a menudo se utiliza para modificar el comportamiento de las clases del marco. Es asumir que sabes algo sobre cómo funcionan esas clases privadas que es "peligroso."Incluso si sus modificaciones funcionan hoy, siempre hay una 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, hace que sea mucho más difícil para Apple cambiar el marco sin romper una gran cantidad de software existente.

 7
Author: Caleb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-17 13:16:06

Usado cuidadosa y sabiamente, puede conducir a código elegante, pero generalmente, solo conduce a código confuso.

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

Por ejemplo, una buena aplicación del método swizzling es isa swizzling, que es cómo ObjC implementa el Valor de clave Observar.

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.

 5
Author: Arafangion,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-17 13:16:06

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

  • Ofusca su código porque puede causar efectos secundarios no documentados, aunque deseados. Cuando uno lee el código él/ella puede ser inconsciente del comportamiento del efecto secundario que se requiere a menos que él / ella recuerde buscar la base del código para ver si ha sido swizzled. No estoy seguro de cómo aliviar este problema porque no siempre es posible documentar cada lugar donde el código depende del lado efecto swizzled comportamiento.
  • Puede hacer que su código sea menos reutilizable porque alguien que encuentra un segmento de código que depende del comportamiento 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.
 5
Author: user3288724,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-09 03:24:30

Siento que el mayor peligro está en la creación de 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 código ilegible, confuso y frustrante. Como cuando alguien sobreutiliza punteros de función en C++.

 4
Author: A.R.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-17 13:10:19

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 interfaz de usuario.

 4
Author: quantumpotato,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-07 19:34:39

Método swizzling puede ser muy útil es en las pruebas unitarias.

Le permite escribir un objeto simulado y usar ese objeto simulado en lugar del objeto real. Su código para permanecer limpio y su prueba unitaria tiene un comportamiento predecible. Digamos que quieres probar algún código que use CLLocationManager. Su prueba unitaria podría cambiar startUpdatingLocation para que alimentara un conjunto predeterminado de ubicaciones a su delegado y su código no tuviera que cambiar.

 3
Author: user3288724,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-09 14:34:36