Pregunta performSelector puede causar una fuga porque su selector es desconocido


Recibo la siguiente advertencia del compilador 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 compilador 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?


1189
2017-08-10 20:23


origen


Respuestas:


Solución

El compilador 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. Todas NSObjectresponder a methodForSelector:, pero también puedes usar class_getMethodImplementation en el tiempo de ejecución de Objective-C (útil si solo tiene una referencia de protocolo, como id<SomeProto>) Estos punteros a las funciones se llaman IMPs, y son simples typedefpunteros de función ed (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 tienes 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 compilador que usted sabe lo que está haciendo y no generar una advertencia ya que los tipos de puntero no coinciden).

Finalmente, llama al puntero de la función2.

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 init/ copy familia o atribuido 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 vacíe la mayoría del grupo de versiones internas, lo que se atribuye a 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, estás intentando llamar a ese retorno void u otros objetos que no sean objetos, puede habilitar las funciones del compilador 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 devuelto por methodForSelector:aunque no quieras usarlo. Desde la perspectiva del compilador, es un objeto después de todo. Eso significa que si el método está llamando, someMethod, está devolviendo un no objeto (incluido void), podría terminar con un valor de puntero de basura retenido / liberado y bloquearse.

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 al no declarar cómo ese método consume parámetros. ARC permite declarar parámetros consumidos, y si el método consume el parámetro, probablemente eventualmente enviarás un mensaje a un zombie 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 compilador no se quejará de los selectores declarados estáticamente:

[_controller performSelector:@selector(someMethod)];

La razón de esto es porque el compilador realmente puede registrar toda la información sobre el selector y el objeto durante la compilació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 construir un 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 el performSelector: familia de métodos primero se agregó 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 el performSelector: familia de métodos como "inherentemente inseguro" 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 "inherentemente 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 programación.


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

2Llamando a NULL la función no es segura en C. La protección utilizada 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, entrada en el sistema de reenvío de mensajes). Básicamente, con la protección en su lugar, sabemos que tenemos una función para llamar.

3 En realidad, es posible que obtenga la información incorrecta si declara que usted se opone id y no estás importando todos los encabezados. Podría terminar con fallas en el código que el compilador 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 Ver la referencia ARC en valores de retorno retenidos y valores de retorno no retenidos para más detalles.


1142
2017-11-18 21:44



En el compilador 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]
);

1170
2017-10-28 19:30



Mi suposición sobre esto es esta: dado que el selector es desconocido para el compilador, 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 hacer métodos; 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 compilador 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).


206
2017-08-10 20:43



En tu proyecto Configuración de compilación, debajo Otras banderas de advertencia (WARNING_CFLAGS), agregar
-Wno-arc-performSelector-leaks

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


119
2017-10-31 13:57



Como solución alternativa hasta que el compilador 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 <objc/message.h>


110
2017-08-16 04:56



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 resto de su proyecto.


87
2018-01-18 21:31



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 compilador que no se puede devolver ningún objeto y de alguna manera se maneja mal.


67
2017-11-11 19:19



Aquí hay una macro actualizada basada en la respuesta dada arriba. Éste debería permitirle envolver su código incluso con una declaración 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]
);

34
2018-05-07 14:58



Este código no implica indicadores del compilador 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 de manera diferente performSelector esto funcionará en cualquier método.


31
2018-02-01 15:46