performSelector puede causar una fuga porque su selector es desconocido

Recibo la siguiente advertencia del comstackdor 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 que no reciba esta advertencia?

Solución

El comstackdor está advirtiendo sobre esto por alguna razón. Es muy raro que esta advertencia simplemente se ignore, y es fácil de solucionar. 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 tersamente (aunque es 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 controller el puntero de function C para el método correspondiente al controller. Todos los NSObject responden a methodForSelector: pero también puede usar class_getMethodImplementation en el time de ejecución Objective-C (útil si solo tiene una reference de protocolo, como id<SomeProto> ). Estos indicadores de function se llaman IMP s, y son pointers simples de function tipeados ( id (*IMP)(id, SEL, ...) ) 1 . Esto puede ser cercano a la firma del método real del método, pero no siempre coincidirá exactamente.

Una vez que tenga el IMP , debe convertirlo en un puntero de function que incluya todos los detalles que ARC necesita (incluidos los dos arguments 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 generar una advertencia ya que los types de puntero no coinciden).

Finalmente, llama al puntero de function 2 .

Ejemplo complejo

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

 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 para la advertencia

El motivo de esta advertencia es que con ARC, el time de ejecución necesita saber qué hacer con el resultado del método al 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 object con el que está trabajando. 3

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

  1. Ignore los types de object no ( void , int , etc.)
  2. Conserve el valor del object, luego suéltelo cuando ya no se use (suposition estándar)
  3. Libere nuevos valores de objects cuando ya no se utilicen (methods en la familia init / copy o atribuidos con ns_returns_retained )
  4. No hacer nada y asumir que el valor del object devuelto será válido en el ámbito local (hasta que se drene el set de la mayoría de los lanzamientos internos, atribuido con ns_returns_autoreleased )

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

Para los selectores que está intentando llamar a ese void retorno u otros que no son objects, podría habilitar las características del comstackdor para ignorar la advertencia, pero puede ser peligroso. He visto a Clang pasar por algunas iteraciones sobre cómo maneja los valores de retorno que no están asignados a las variables locales. No hay ninguna razón para que con ARC haya sido habilitado que no pueda retener y liberar el valor del object que se devuelve de methodForSelector: aunque no desee usarlo. Desde la perspectiva del comstackdor, después de todo es un object. Eso significa que si el método al que está llamando, someMethod devuelve un no object (incluido el void ), podría terminar con un valor de puntero de basura que se retenga / libere y se bloquee.

Argumentos adicionales

Una consideración es que esta es la misma advertencia que se producirá con performSelector:withObject: y podría encontrarse con problemas similares sin declarar cómo ese método consume parameters. ARC permite declarar los parameters consumidos , y si el método consume el parámetro, eventualmente enviará un post a un zombie y se bloqueará. Hay forms de solucionar este problema con el casting puenteado, pero en realidad sería mejor simplemente usar la metodología IMP y puntero de function anterior. Dado que los parameters consumidos rara vez son un problema, esto no es probable que aparezca.

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 en realidad puede grabar toda la información sobre el selector y el object durante la compilation. No necesita hacer suposiciones sobre nada. (Revisé esto hace un año, mirando la fuente, pero no tengo una reference en este momento).

Supresión

Al tratar de pensar en una situación donde la supresión de esta advertencia sería necesaria y un buen layout de código, me voy a poner en blanco. Alguien por favor comparta si han tenido una experiencia en la que es necesario silenciar esta advertencia (y lo anterior no maneja las cosas correctamente).

Más

Es posible build una NSMethodInvocation para manejar esto también, pero hacerlo requiere mucho más tipado y también es más lento, por lo que hay pocos motivos para hacerlo.

Historia

Cuando se realizó la primera function de selección de performSelector: family of methods en Objective-C, ARC no existía. Al crear ARC, Apple decidió que se debería generar una advertencia para estos methods como una forma de guiar a los desarrolladores hacia el uso de otros medios para definir explícitamente cómo se debe manejar la memory al enviar posts arbitrarios a través de un selector con nombre. En Objective-C, los desarrolladores pueden hacer esto utilizando moldes de estilo C en pointers de function sin formatting.

Con la introducción de Swift, Apple ha documentado the performSelector: familia de methods como "intrínsecamente insegura" y no están disponibles para Swift.

Con el time, hemos visto esta progresión:

  1. Las primeras versiones de Objective-C permiten performSelector: (gestión manual de la memory)
  2. Objective-C con ARC advierte del uso de performSelector:
  3. Swift no tiene acceso a performSelector: y documenta estos methods como "inherentemente inseguros"

Sin embargo, la idea de enviar posts basados ​​en un selector con nombre no es una característica "inherentemente insegura". Esta idea se ha utilizado con éxito durante mucho time en Objective-C, así como en muchos otros lenguajes de progtwigción.


1 Todos los methods de Objective-C tienen dos arguments ocultos, self y _cmd que se agregan implícitamente cuando llamas a un método.

2 Llamar a una function NULL no es seguro en C. El guardia utilizado para verificar la presencia del controller garantiza que tengamos un object. Por lo tanto, sabemos que obtendremos un IMP de methodForSelector: (aunque puede ser _objc_msgForward , input en el sistema de reenvío de posts). Básicamente, con el guardia en su lugar, sabemos que tenemos una function para llamar.

3 En realidad, es posible que obtenga la información incorrecta si declara que los objects son id y no importa todos los encabezados. Podría terminar con lockings en el código que el comstackdor piensa 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 methods debe elegir.

4 Consulte la reference ARC en los valores de retorno retenidos y los valores retornados sin retener para get 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 ignonetworking "-Warc-performSelector-leaks" [self.ticketTarget performSelector: self.ticketAction withObject: self]; #pragma clang diagnostic pop 

Si obtiene el error en varios lugares y desea usar el sistema de macros C para ocultar los pragmas, puede definir una macro para facilitar la supresión de la advertencia:

 #define SuppressPerformSelectorLeakWarning(Stuff) \ do { \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignonetworking \"-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 post realizado, puede hacer esto:

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

Mi conjetura sobre esto es esto: dado que el selector no es conocido por el comstackdor, ARC no puede hacer cumplir la administración de memory adecuada.

De hecho, hay ocasiones en que la gestión de la memory está vinculada al nombre del método por una convención específica. Específicamente, estoy pensando en constructores de conveniencia versus methods de fabricación ; el antiguo retorno por convención de un object autoelegido; el último es un object retenido. La convención se basa en los nombres del selector, de modo que si el comstackdor no conoce el selector, no puede aplicar la regla de gestión de memory adecuada.

Si esto es correcto, creo que puedes usar tu código con security, siempre que te asegures de que todo está bien en cuanto a la gestión de la memory (por ejemplo, que tus methods no devuelvan los objects que asignan).

En la Configuración de generación del proyecto, en Otras banderas de advertencia ( WARNING_CFLAGS ), agregue
-Wno-arc-performSelector-leaks

Ahora solo asegúrese de que el selector al que llama no cause que su object sea retenido o copydo.

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

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

en lugar de

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

Tendrás que

 #import <objc/message.h> 

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

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

Esto ignoraría la advertencia en esta línea, pero aún así lo permitirá durante el rest de su proyecto.

Extraño pero cierto: si es aceptable (es decir, el resultado es nulo y no le molesta dejar correr el ciclo una sola vez), agregue un retraso, incluso si es cero:

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

Esto elimina la advertencia, presumiblemente porque tranquiliza al comstackdor que no se puede devolver ningún object y que, de alguna manera, se ha administrado mal.

Aquí hay una macro actualizada basada en la respuesta dada arriba. Este 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 ignonetworking \"-Warc-performSelector-leaks\"") \ code; \ _Pragma("clang diagnostic pop") \ SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING( return [_target performSelector:_action withObject:self] ); 

Este código no incluye banderas de comstackdor ni llamadas en time de ejecución directo:

 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 arguments para que, a diferencia de performSelector esto funcione en cualquier método.

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

 #import <Foundation/Foundation.h> #import <objc/runtime.h> #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 ignonetworking "-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 ignonetworking "-Warc-performSelector-leaks" [self performSelector: aSelector]; #pragma clang diagnostic pop } @end 

Por el bien de la posteridad, he decidido tirar mi sombrero al ring 🙂

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

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

Estos parecen ser un reemploop limpio, seguro para ARC y casi idéntico para performSelector sin tener que tener mucho que ver con objc_msgSend() .

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

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

Considera lo siguiente:

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

Ahora, ¿cómo puede ARC saber que el primero devuelve un object con un recuento de retención de 1 pero el segundo devuelve un object que se autorealiza?

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 get un object retenido de performSelector, que no sea "no haga 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 memory.

 @interface Dummy : NSObject <NSCopying> @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 memory en mi ejemplo es CopyDummyWithLeak. La razón es que ARC no sabe, ese copySelector devuelve el object retenido.

Si ejecuta la herramienta Memory Feak, puede ver la siguiente image: introduzca la descripción de la imagen aquí … y no hay pérdidas de memory en ningún otro caso: introduzca la descripción de la imagen aquí

Para hacer la macro de Scott Thompson 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 ignonetworking MY_STR(FLAG))) \ MACRO \ _Pragma("clang diagnostic pop") 

Entonces úselo así:

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

Debido a que está utilizando ARC, debe usar iOS 4.0 o posterior. Esto significa que puedes usar bloques. Si en lugar de recordar que el selector para realizarlo tomó un bloque, ARC podría seguir mejor lo que realmente está sucediendo y no tendría que correr el riesgo de introducir accidentalmente una pérdida de memory.

¡No suprimas las advertencias!

No hay less de 12 soluciones alternativas para jugar con el comstackdor.
Si bien eres inteligente en el momento de la primera implementación, pocos ingenieros en la Tierra pueden seguir tus pasos, y este código finalmente se romperá.

Rutas seguras:

Todas estas soluciones funcionarán, con algún grado de variación de su intención original. Supongamos que param puede ser nil si así 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]]; 

Dangerous Routes

Requires some kind of compiler silencing, which is bound to break. Note that at present time, it did break in 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<MyProtocol> source; @end 

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

 [self.source doSomethingWithObject:object];