performSelector puede causar una fuga porque su selector es desconocido

Recibo la siguiente advertencia del comstackdor de ARC:

"performSelector may cause a leak because its selector is unknown". 

Esto es lo que estoy haciendo:

 [_controller performSelector:NSSelectorFromString(@"someMethod")]; 

¿Por qué recibo esta advertencia? Entiendo que el comstackdor no puede verificar si el selector existe o no, pero ¿por qué eso causaría una fuga? ¿Y cómo puedo cambiar mi código para no tener más esta advertencia?

Solución

El comstackdor advierte sobre esto por una razón. Es muy raro que esta advertencia simplemente deba ignorarse, y es fácil evitarla. Así es cómo:

 if (!_controller) { return; } SEL selector = NSSelectorFromString(@"someMethod"); IMP imp = [_controller methodForSelector:selector]; void (*func)(id, SEL) = (void *)imp; func(_controller, selector); 

O más tersely (aunque difícil de leer y sin el guardia):

 SEL selector = NSSelectorFromString(@"someMethod"); ((void (*)(id, SEL))[_controller methodForSelector:selector])(_controller, selector); 

Explicación

Lo que está sucediendo aquí es que está pidiendo al controlador el puntero de la función C para el método correspondiente al controlador. Todos los NSObject responden a methodForSelector: pero también puede usar class_getMethodImplementation en Objective-C runtime (útil si solo tiene una referencia de protocolo, como id ). Estos punteros de función se denominan IMP , y son punteros de función de typedef simple ( id (*IMP)(id, SEL, ...) ) 1 . Esto puede estar cerca de la firma del método real del método, pero no siempre coincidirá exactamente.

Una vez que tiene el IMP , debe convertirlo en un puntero de función que incluya todos los detalles que necesita ARC (incluidos los dos argumentos ocultos implícitos self y _cmd de cada llamada al método Objective-C). Esto se maneja en la tercera línea (el (void *) en el lado derecho simplemente le dice al comstackdor que usted sabe lo que está haciendo y no genera una advertencia ya que los tipos de puntero no coinciden).

Finalmente, llama al puntero de función 2 .

Ejemplo complejo

Cuando el selector toma argumentos o devuelve un valor, tendrá que cambiar un poco las cosas:

 SEL selector = NSSelectorFromString(@"processRegion:ofView:"); IMP imp = [_controller methodForSelector:selector]; CGRect (*func)(id, SEL, CGRect, UIView *) = (void *)imp; CGRect result = _controller ? func(_controller, selector, someRect, someView) : CGRectZero; 

Razonamiento de advertencia

El motivo de esta advertencia es que con ARC, el tiempo de ejecución necesita saber qué hacer con el resultado del método que está llamando. El resultado podría ser cualquier cosa: void , int , char , NSString * , id , etc. ARC normalmente obtiene esta información del encabezado del tipo de objeto con el que está trabajando. 3

En realidad, hay solo 4 cosas que ARC consideraría para el valor de retorno: 4

  1. Ignorar tipos que no sean objetos ( void , int , etc.)
  2. Conservar el valor del objeto, luego suelte cuando ya no se use (suposición estándar)
  3. Liberar nuevos valores de objeto cuando ya no se usen (métodos en la familia init / copy o atribuidos con ns_returns_retained )
  4. No haga nada y suponga que el valor del objeto devuelto será válido en el ámbito local (hasta que se ns_returns_autoreleased grupo de versiones más interno, lo que se atribuye con ns_returns_autoreleased )

La llamada a methodForSelector: asume que el valor de retorno del método al que llama es un objeto, pero no lo retiene / libera. Por lo tanto, podría terminar creando una fuga si se supone que su objeto se liberará como en el punto 3 anterior (es decir, el método que está llamando devuelve un nuevo objeto).

Para los selectores que intentas llamar a ese retorno void u otros no objetos, puedes habilitar las funciones del comstackdor para ignorar la advertencia, pero puede ser peligroso. He visto a Clang pasar por algunas iteraciones de cómo maneja los valores devueltos que no están asignados a las variables locales. No hay ninguna razón para que, con ARC habilitado, no pueda retener y liberar el valor del objeto que devuelve methodForSelector: aunque no desee usarlo. Desde la perspectiva del comstackdor, es un objeto después de todo. Eso significa que si el método al que llama, someMethod , devuelve un objeto que no sea objeto (incluido el void ), puede terminar con un valor del puntero de basura retenido / liberado y bloquearse.

Argumentos adicionales

Una consideración es que esta es la misma advertencia que ocurrirá con performSelector:withObject: y podría encontrarse con problemas similares al no declarar cómo ese método consume parámetros. ARC permite declarar los parámetros consumidos , y si el método consume el parámetro, probablemente eventualmente enviarás un mensaje a un zombi y se bloqueará. Hay formas de evitar esto con el bridge puenteado, pero realmente sería mejor simplemente usar el IMP y la metodología del puntero de función arriba. Dado que los parámetros consumidos rara vez son un problema, no es probable que esto surja.

Selectores estáticos

Curiosamente, el comstackdor no se quejará de los selectores declarados estáticamente:

 [_controller performSelector:@selector(someMethod)]; 

La razón de esto es porque el comstackdor realmente puede registrar toda la información sobre el selector y el objeto durante la comstackción. No necesita hacer suposiciones sobre nada. (Revisé esto hace un año, al mirar la fuente, pero no tengo una referencia en este momento).

Supresión

Al tratar de pensar en una situación en la que sea necesaria la supresión de esta advertencia y un buen diseño del código, me quedaré en blanco. Alguien por favor comparta si han tenido una experiencia donde silenciar esta advertencia era necesaria (y lo anterior no maneja las cosas adecuadamente).

Más

Es posible crear una NSMethodInvocation para manejar esto también, pero hacerlo requiere mucho más tipeo y también es más lento, por lo que hay pocas razones para hacerlo.

Historia

Cuando la familia de métodos performSelector: first se agregó por primera vez a Objective-C, ARC no existía. Al crear ARC, Apple decidió que se debe generar una advertencia para estos métodos como una forma de guiar a los desarrolladores hacia el uso de otros medios para definir explícitamente cómo se debe manejar la memoria cuando se envían mensajes arbitrarios a través de un selector con nombre. En Objective-C, los desarrolladores pueden hacer esto usando moldes de estilo C en punteros de función sin formato.

Con la presentación de Swift, Apple ha documentado la familia de métodos de performSelector: “inherentemente insegura” y no están disponibles para Swift.

Con el tiempo, hemos visto esta progresión:

  1. Las primeras versiones de Objective-C permiten performSelector: (gestión de memoria manual)
  2. Objective-C con ARC advierte sobre el uso de performSelector:
  3. Swift no tiene acceso a performSelector: y documenta estos métodos como “intrínsecamente inseguros”

La idea de enviar mensajes basados ​​en un selector nombrado no es, sin embargo, una característica “intrínsecamente insegura”. Esta idea se ha utilizado con éxito durante mucho tiempo en Objective-C, así como en muchos otros lenguajes de progtwigción.


1 Todos los métodos de Objective-C tienen dos argumentos ocultos, self y _cmd que se agregan implícitamente al llamar a un método.

2 Llamar a una función NULL no es seguro en C. El guardia utilizado para verificar la presencia del controlador garantiza que tengamos un objeto. Por lo tanto, sabemos que obtendremos un IMP de methodForSelector: (aunque puede ser _objc_msgForward , _objc_msgForward al sistema de reenvío de mensajes). Básicamente, con la protección en su lugar, sabemos que tenemos una función para llamar.

3 De hecho, es posible que obtenga la información incorrecta si declara que los objetos son una id y no está importando todos los encabezados. Podría terminar con fallas en el código que el comstackdor cree que está bien. Esto es muy raro, pero podría suceder. Por lo general, recibirá una advertencia de que no sabe cuál de las dos firmas de método puede elegir.

4 Consulte la referencia de ARC en los valores de retorno retenidos y los valores de retorno no retenidos para obtener más detalles.

En el comstackdor LLVM 3.0 en Xcode 4.2, puede suprimir la advertencia de la siguiente manera:

 #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self.ticketTarget performSelector: self.ticketAction withObject: self]; #pragma clang diagnostic pop 

Si obtiene el error en varios lugares y desea utilizar el sistema de macro C para ocultar los pragmas, puede definir una macro para que sea más fácil suprimir la advertencia:

 #define SuppressPerformSelectorLeakWarning(Stuff) \ do { \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \ Stuff; \ _Pragma("clang diagnostic pop") \ } while (0) 

Puedes usar la macro así:

 SuppressPerformSelectorLeakWarning( [_target performSelector:_action withObject:self] ); 

Si necesita el resultado del mensaje realizado, puede hacer esto:

 id result; SuppressPerformSelectorLeakWarning( result = [_target performSelector:_action withObject:self] ); 

Mi suposición sobre esto es esta: dado que el selector es desconocido para el comstackdor, ARC no puede imponer una gestión de memoria adecuada.

De hecho, hay momentos en que la gestión de la memoria está vinculada al nombre del método por una convención específica. Específicamente, estoy pensando en constructores de conveniencia versus métodos make ; el antiguo retorno por convención de un objeto liberado automáticamente; este último es un objeto retenido. La convención se basa en los nombres del selector, por lo que si el comstackdor no conoce el selector, no puede aplicar la regla de administración de memoria adecuada.

Si esto es correcto, creo que puede usar su código de forma segura, siempre que se asegure de que todo está bien en cuanto a la administración de la memoria (por ejemplo, que sus métodos no devuelvan los objetos que asignan).

En la configuración de comstackción de su proyecto, en Otros indicadores de advertencia ( WARNING_CFLAGS ), agregue
-Wno-arc-performSelector-leaks

Ahora solo asegúrese de que el selector que está llamando no haga que su objeto sea retenido o copiado.

Como solución alternativa hasta que el comstackdor permita anular la advertencia, puede usar el tiempo de ejecución

 objc_msgSend(_controller, NSSelectorFromString(@"someMethod")); 

en lugar de

 [_controller performSelector:NSSelectorFromString(@"someMethod")]; 

Tendrás que

 #import  

Para ignorar el error solo en el archivo con el selector de ejecución, agregue un #pragma de la siguiente manera:

 #pragma clang diagnostic ignored "-Warc-performSelector-leaks" 

Esto ignoraría la advertencia en esta línea, pero aún así lo permite a lo largo del rest de su proyecto.

Extraño pero cierto: si es aceptable (es decir, el resultado es nulo y no te importa dejar que el ciclo runloop una vez), agrega un retraso, incluso si esto es cero:

 [_controller performSelector:NSSelectorFromString(@"someMethod") withObject:nil afterDelay:0]; 

Esto elimina la advertencia, presumiblemente porque le asegura al comstackdor que no se puede devolver ningún objeto y de alguna manera se maneja mal.

Aquí hay una macro actualizada basada en la respuesta dada arriba. Éste debería permitirle envolver su código incluso con una statement de devolución.

 #define SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING(code) \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \ code; \ _Pragma("clang diagnostic pop") \ SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING( return [_target performSelector:_action withObject:self] ); 

Este código no implica indicadores del comstackdor o llamadas directas en tiempo de ejecución:

 SEL selector = @selector(zeroArgumentMethod); NSMethodSignature *methodSig = [[self class] instanceMethodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; [invocation setSelector:selector]; [invocation setTarget:self]; [invocation invoke]; 

NSInvocation permite establecer múltiples argumentos, por lo que a diferencia de performSelector esto funcionará en cualquier método.

Bueno, muchas respuestas aquí, pero ya que esto es un poco diferente, combinando algunas respuestas que pensé que pondría. Estoy usando una categoría de NSObject que verifica para asegurarse de que el selector devuelve vacío, y también suprime el comstackdor advertencia.

 #import  #import  #import "Debug.h" // not given; just an assert @interface NSObject (Extras) // Enforce the rule that the selector used must return void. - (void) performVoidReturnSelector:(SEL)aSelector withObject:(id)object; - (void) performVoidReturnSelector:(SEL)aSelector; @end @implementation NSObject (Extras) // Apparently the reason the regular performSelect gives a compile time warning is that the system doesn't know the return type. I'm going to (a) make sure that the return type is void, and (b) disable this warning // See http://stackoverflow.com/questions/7017281/performselector-may-cause-a-leak-because-its-selector-is-unknown - (void) checkSelector:(SEL)aSelector { // See http://stackoverflow.com/questions/14602854/objective-c-is-there-a-way-to-check-a-selector-return-value Method m = class_getInstanceMethod([self class], aSelector); char type[128]; method_getReturnType(m, type, sizeof(type)); NSString *message = [[NSString alloc] initWithFormat:@"NSObject+Extras.performVoidReturnSelector: %@.%@ selector (type: %s)", [self class], NSStringFromSelector(aSelector), type]; NSLog(@"%@", message); if (type[0] != 'v') { message = [[NSString alloc] initWithFormat:@"%@ was not void", message]; [Debug assertTrue:FALSE withMessage:message]; } } - (void) performVoidReturnSelector:(SEL)aSelector withObject:(id)object { [self checkSelector:aSelector]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" // Since the selector (aSelector) is returning void, it doesn't make sense to try to obtain the return result of performSelector. In fact, if we do, it crashes the app. [self performSelector: aSelector withObject: object]; #pragma clang diagnostic pop } - (void) performVoidReturnSelector:(SEL)aSelector { [self checkSelector:aSelector]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self performSelector: aSelector]; #pragma clang diagnostic pop } @end 

Por la posteridad, he decidido arrojar mi sombrero al ring 🙂

Recientemente he estado viendo más y más reestructuración lejos del paradigma de target / selector , a favor de cosas como protocolos, bloques, etc. Sin embargo, hay un reemplazo performSelector para performSelector que he usado algunas veces ahora :

 [NSApp sendAction: NSSelectorFromString(@"someMethod") to: _controller from: nil]; 

Estos parecen ser un reemplazo limpio, ARC seguro y casi idéntico para performSelector sin tener que preocuparse mucho por objc_msgSend() .

Sin embargo, no tengo idea si hay un análogo disponible en iOS.

La respuesta de Matt Galloway en este hilo explica por qué:

Considera lo siguiente:

 id anotherObject1 = [someObject performSelector:@selector(copy)]; id anotherObject2 = [someObject performSelector:@selector(giveMeAnotherNonRetainedObject)]; 

Ahora, ¿cómo puede saber ARC que el primero devuelve un objeto con un conteo de retención de 1, pero el segundo devuelve un objeto que se libera automáticamente?

Parece que generalmente es seguro suprimir la advertencia si está ignorando el valor de retorno. No estoy seguro de cuál es la mejor práctica si realmente necesita obtener un objeto retenido de performSelector, aparte de “no hacer eso”.

@ c-road proporciona el enlace correcto con la descripción del problema aquí . A continuación puede ver mi ejemplo, cuando performSelector causa una pérdida de memoria.

 @interface Dummy : NSObject  @end @implementation Dummy - (id)copyWithZone:(NSZone *)zone { return [[Dummy alloc] init]; } - (id)clone { return [[Dummy alloc] init]; } @end void CopyDummy(Dummy *dummy) { __unused Dummy *dummyClone = [dummy copy]; } void CloneDummy(Dummy *dummy) { __unused Dummy *dummyClone = [dummy clone]; } void CopyDummyWithLeak(Dummy *dummy, SEL copySelector) { __unused Dummy *dummyClone = [dummy performSelector:copySelector]; } void CloneDummyWithoutLeak(Dummy *dummy, SEL cloneSelector) { __unused Dummy *dummyClone = [dummy performSelector:cloneSelector]; } int main(int argc, const char * argv[]) { @autoreleasepool { Dummy *dummy = [[Dummy alloc] init]; for (;;) { @autoreleasepool { //CopyDummy(dummy); //CloneDummy(dummy); //CloneDummyWithoutLeak(dummy, @selector(clone)); CopyDummyWithLeak(dummy, @selector(copy)); [NSThread sleepForTimeInterval:1]; }} } return 0; } 

El único método que causa pérdida de memoria en mi ejemplo es CopyDummyWithLeak. La razón es que ARC no sabe, que copySelector devuelve el objeto retenido.

Si ejecuta Memory Leak Tool, puede ver la siguiente imagen: enter image description here … y no hay pérdidas de memoria en ningún otro caso: enter image description here

Para hacer que la macro de Scott Thompson sea más genérica:

 // String expander #define MY_STRX(X) #X #define MY_STR(X) MY_STRX(X) #define MYSilenceWarning(FLAG, MACRO) \ _Pragma("clang diagnostic push") \ _Pragma(MY_STR(clang diagnostic ignored MY_STR(FLAG))) \ MACRO \ _Pragma("clang diagnostic pop") 

Entonces úsalo así:

 MYSilenceWarning(-Warc-performSelector-leaks, [_target performSelector:_action withObject:self]; ) 

Como está utilizando ARC, debe usar iOS 4.0 o posterior. Esto significa que podrías usar bloques. Si en lugar de recordar el selector para realizar, en su lugar tomó un bloque, ARC sería capaz de realizar un mejor seguimiento de lo que realmente está sucediendo y no tendría que correr el riesgo de introducir accidentalmente una pérdida de memoria.

¡No suprimas las advertencias!

No hay menos de 12 soluciones alternativas para jugar con el comstackdor.
Mientras está siendo listo en el momento de la primera implementación, pocos ingenieros en la Tierra pueden seguir sus pasos, y este código eventualmente se romperá.

Rutas Seguras:

Todas estas soluciones funcionarán, con algún grado de variación de su intención original. Suponga que param puede ser nil si lo desea:

Ruta segura, mismo comportamiento conceptual:

 // GREAT [_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:YES]; [_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:YES modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]]; [_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:YES]; [_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:YES modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]]; 

Ruta segura, comportamiento ligeramente diferente:

(Ver esta respuesta)
Use cualquier hilo en lugar de [NSThread mainThread] .

 // GOOD [_controller performSelector:selector withObject:anArgument afterDelay:0]; [_controller performSelector:selector withObject:anArgument afterDelay:0 inModes:@[(__bridge NSString *)kCFRunLoopDefaultMode]]; [_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:NO]; [_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:NO]; [_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:NO modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]]; [_controller performSelectorInBackground:selector withObject:anArgument]; [_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:NO]; [_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:NO modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]]; 

Rutas peligrosas

Requiere algún tipo de silenciamiento del comstackdor, que está destinado a romperse. Tenga en cuenta que en este momento, se rompió en Swift .

 // AT YOUR OWN RISK [_controller performSelector:selector]; [_controller performSelector:selector withObject:anArgument]; [_controller performSelector:selector withObject:anArgument withObject:nil]; 

Instead of using the block approach, which gave me some problems:

  IMP imp = [_controller methodForSelector:selector]; void (*func)(id, SEL) = (void *)imp; 

I will use NSInvocation, like this:

  -(void) sendSelectorToDelegate:(SEL) selector withSender:(UIButton *)button if ([delegate respondsToSelector:selector]) { NSMethodSignature * methodSignature = [[delegate class] instanceMethodSignatureForSelector:selector]; NSInvocation * delegateInvocation = [NSInvocation invocationWithMethodSignature:methodSignature]; [delegateInvocation setSelector:selector]; [delegateInvocation setTarget:delegate]; // remember the first two parameter are cmd and self [delegateInvocation setArgument:&button atIndex:2]; [delegateInvocation invoke]; } 

If you don’t need to pass any arguments an easy workaround is to use valueForKeyPath . This is even possible on a Class object.

 NSString *colorName = @"brightPinkColor"; id uicolor = [UIColor class]; if ([uicolor respondsToSelector:NSSelectorFromString(colorName)]){ UIColor *brightPink = [uicolor valueForKeyPath:colorName]; ... } 

You could also use a protocol here. So, create a protocol like so:

 @protocol MyProtocol -(void)doSomethingWithObject:(id)object; @end 

In your class that needs to call your selector, you then have a @property.

 @interface MyObject @property (strong) id source; @end 

When you need to call @selector(doSomethingWithObject:) in an instance of MyObject, do this:

 [self.source doSomethingWithObject:object];