ios objective-c swizzling

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



objective-c (8)

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 ser que no esté al tanto del comportamiento de los efectos secundarios que se requiere, a menos que se acuerde de buscar en la base del código para ver si se ha modificado. No estoy seguro de cómo aliviar este problema porque no siempre es posible documentar cada lugar donde el código depende del comportamiento de los efectos secundarios.
  • Puede hacer que su código sea menos reutilizable porque alguien que encuentra un segmento de código que depende del comportamiento alterado que le gustaría usar en otro lugar no puede simplemente cortarlo y pegarlo en otra base de código sin encontrar y copiar el método extravagante.

He escuchado a la gente decir que el método swizzling es una práctica peligrosa. Incluso el nombre swizzling sugiere que es un poco engañoso.

El método Swizzling es modificar la asignación para que el selector de llamadas A 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 decida si usar swizzling puede tomar una decisión informada si vale la pena por lo que está tratando de hacer?

P.ej

  • Nombrar colisiones : si la clase más adelante extiende su funcionalidad para incluir el nombre del método que ha agregado, causará una gran cantidad de problemas. Reduzca el riesgo nombrando sensiblemente los métodos enojados.

Creo que esta es una pregunta realmente genial, y es una pena que en lugar de abordar la pregunta real, la mayoría de las respuestas hayan evitado el problema y simplemente hayan dicho que no utilicen swizzling.

Usar el método de chisporrotear es como usar cuchillos afilados en la cocina. Algunas personas tienen miedo de los cuchillos afilados porque creen que se van a cortar mal, 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 se puede abusar y provocar bichos horribles.

Fondo

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 muy difíciles de implementar adecuadamente. Sin embargo, muchas personas aún optan por usar singletons. Lo mismo se puede decir sobre swizzling. Debes formar tu propia opinión una vez que entiendas completamente lo bueno y lo malo.

Discusión

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

  • Método swizzling no es atómico
  • Cambia el comportamiento del código no propio.
  • Posibles conflictos de nombres
  • Swizzling cambia los argumentos del método
  • El orden de los swizzles importa
  • 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 una a la vez.

Método swizzling no es atómico

Todavía tengo que ver una implementación de métodos 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 querría usar 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 debe hacer su método swizzling en +(void)load . El método de clase de load se ejecuta en serie al inicio de su aplicación. No tendrás ningún problema con la concurrencia si haces tu swizzling aquí. Sin embargo, si tuviera que iniciar el proceso de inicio +(void)initialize , podría terminar con una condición de carrera en su implementación de swizzling y el tiempo de ejecución podría terminar en un estado extraño.

Cambia el comportamiento del código no propio.

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 la gente señala esto como un gran problema es porque no solo está cambiando las cosas para la única instancia de NSButton que desea cambiar las cosas, sino en lugar de todas NSButton instancias de NSButton en su aplicación. Por esta razón, debes ser cauteloso cuando te arruines, pero no necesitas evitarlo por completo.

Piénselo de esta manera ... si anula un método en una clase y no llama el método de la súper clase, puede causar problemas. En la mayoría de los casos, la súper clase espera que se llame a ese método (a menos que se documente lo contrario). Si aplica este mismo pensamiento a 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. A menudo prefijamos 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 necesitamos cambiar la forma en que pensamos sobre el método swizzling ligeramente. La mayoría 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: fuera definido en otro lugar? Este problema no es exclusivo de swizzling, pero podemos solucionarlo de todos modos. La solución tiene un beneficio adicional de abordar otras trampas 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

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 de cambio para las personas que han estado usando swizzling como se ha definido por un tiempo, pero al final, creo que es mejor. El método 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

Los métodos de cambio de nombre de Swizzling cambian 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 no se debe realizar. Estás 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 de setFrame: por lo que sigue adelante y llama a eso, pero el argumento setFrame: no es setFrame: como debería ser. Ahora es my_setFrame: La implementación original está siendo llamada 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 se mantendrán sin cambios!

El orden de los swizzles importa

El orden en el que los métodos se enredan importa. Suponiendo que setFrame: solo está definido 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 es swizzled? Bueno, la mayoría de los swizzling se asegurarán de que no reemplace la implementación de setFrame: para todas las vistas, por lo que levantará el método de instancia. Esto usará la implementación existente para redefinir setFrame: en la clase NSButton 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 swizzling en NSControl (nuevamente utilizando la implementación NSView ).

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

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:)];

Dado que la vista de swizzling tiene lugar primero, el control de swizzling será capaz de desplegar el método correcto. Del mismo modo, dado que el control de control estaba antes que el botón, el botón abrirá la implementación del setFrame: de setFrame: Esto es un poco confuso, pero este es el orden correcto. ¿Cómo podemos asegurar este orden de cosas?

De nuevo, solo usa la load para revolver las cosas. Si te agitas en la load y solo haces cambios en la clase que se está cargando, estarás a salvo. El método de load garantiza que el método de carga de súper clase se llamará antes que cualquier subclase. ¡Tendremos el orden exacto correcto!

Difícil de entender (parece recursivo)

Mirando un método swizzled tradicionalmente definido, creo que es muy difícil saber qué está pasando. Pero viendo 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 rastro en el que los nombres enredados se mezclan y todo se confunde en tu cabeza. Una vez más, la implementación alternativa aborda esto. Verás funciones claramente nombradas en backtraces. Aun así, puede ser difícil depurarlo porque es difícil recordar el impacto que está teniendo. 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 multihilo.

Conclusión

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

1 Usando el método de swizzling definido anteriormente, puede hacer que las cosas sean seguras si tuviera que usar trampolines. Necesitarías dos trampolines. Al comienzo del método, tendría que asignar el puntero a la función, store , a una función que giró hasta que la dirección a la que apuntaba la store cambiara. Esto evitaría cualquier condición de carrera en la que se haya llamado al método swizzled antes de poder establecer el puntero de la función de store . Entonces necesitaría usar un trampolín en el caso de que la implementación no esté ya definida en la clase y realizar la búsqueda del trampolín y llamar al método de la superclase correctamente. La definición del método para que busque de forma dinámica la súper implementación garantizará que el orden de las llamadas rápidas no sea importante.


No es el remolino 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. Se 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 cause que la 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.


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

  • Redireccionar 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 hace algo de trabajo y luego llama al método A.

El método de 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 haciendo swizzling. 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 incluso no darse cuenta de que el swizzle se ha formado previamente. Si se introducen errores desde el swizzle (quizás debido a cambios en la clase original) será difícil resolverlos.

En resumen, debe mantener la velocidad 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).


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];

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


Siento que el mayor peligro está en 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 ilegible, confuso y frustrante. Algo así como cuando alguien usa en exceso los punteros de función en C ++.


Usado con cuidado y prudencia, puede llevar a un código elegante, pero por lo general, solo conduce a un 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 necesita saber claramente por qué se aplica bien a la situación y por qué las alternativas no funcionan con elegancia para la situación .

Por ejemplo, una buena aplicación del método swizzling es un swizzling, que es como ObjC implementa Key Value Observing.

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


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

Te permite escribir un objeto simulado y tener ese objeto simulado usado en lugar del objeto real. Tu código para permanecer limpio y tu prueba de unidad tiene un comportamiento predecible. Digamos que quiere probar algún código que usa CLLocationManager. Su prueba de unidad podría interrumpir el inicio de la ubicación de actualización para que alimente un conjunto predeterminado de ubicaciones a su delegado y su código no tenga que cambiar.