performSelector puede causar una fuga porque se desconoce su selector
Recibo la siguiente advertencia del compilador 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 recibir más esta advertencia?
Solución
El compilador advierte sobre esto por una 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 concisamente (aunque difícil de leer y sin la guardia):
SEL selector = NSSelectorFromString(@"someMethod");
((void (*)(id, SEL))[_controller methodForSelector:selector])(_controller, selector);
Explicación
Lo que sucede aquí es que le está pidiendo al controlador el puntero de función C para el método correspondiente al controlador. Todos NSObject
los correos electrónicos responden a methodForSelector:
, pero también puedes usarlos class_getMethodImplementation
en el tiempo de ejecución de Objective-C (útil si solo tienes una referencia de protocolo, como id<SomeProto>
). Estos punteros de función se denominan IMP
s y son typedef
punteros de función simples ( id (*IMP)(id, SEL, ...)
) 1 . Esto puede estar cerca de la firma del método real, pero no siempre coincidirá exactamente.
Una vez que tenga el archivo IMP
, debe convertirlo en un puntero de función que incluya todos los detalles que ARC necesita (incluidos los dos argumentos ocultos implícitos self
y _cmd
cada llamada al método Objective-C). Esto se maneja en la tercera línea (la (void *)
del lado derecho simplemente le dice al compilador que usted sabe lo que está haciendo y que no genere una advertencia ya que los tipos de puntero no coinciden).
Finalmente, llamas al puntero de función 2 .
Ejemplo complejo
Cuando el selector toma argumentos o devuelve un valor, tendrás 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 de la 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ás 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, solo hay 4 cosas que ARC consideraría para el valor de retorno: 4
- Ignorar tipos que no sean de objeto (
void
,,int
etc.) - Conservar el valor del objeto y luego liberarlo cuando ya no se utilice (supuesto estándar)
- Liberar nuevos valores de objetos cuando ya no se utilicen (métodos en la familia
init
/copy
o atribuidos conns_returns_retained
) - No hacer nada y asumir que el valor del objeto devuelto será válido en el ámbito local (hasta que se drene el grupo de lanzamiento más interno, atribuido 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 ni lo libera. Por lo tanto, podría terminar creando una fuga si se supone que su objeto debe liberarse como en el punto 3 anterior (es decir, el método al que está llamando devuelve un nuevo objeto).
Para los selectores a los que intenta llamar con retorno void
u otros no objetos, puede habilitar las funciones del compilador para ignorar la advertencia, pero puede ser peligroso. He visto a Clang realizar algunas iteraciones sobre cómo maneja los valores de retorno que no están asignados a variables locales. No hay ninguna razón por la que con ARC habilitado no pueda retener y liberar el valor del objeto devuelto methodForSelector:
aunque no desee usarlo. Desde la perspectiva del compilador, después de todo, es un objeto. Eso significa que si el método al que está llamando, someMethod
devuelve un no objeto (incluido void
), podría terminar con un valor de puntero basura retenido/liberado y fallar.
Argumentos adicionales
Una consideración es que se producirá la misma advertencia 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 fallarás. Hay formas de solucionar este problema con la conversión en puente, pero en realidad sería mejor simplemente usar la IMP
metodología de puntero de función and anterior. 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 que el compilador realmente puede registrar toda la información sobre el selector y el objeto durante la compilación. No es necesario hacer suposiciones sobre nada. (Revisé esto hace un año mirando la fuente, pero no tengo una referencia en este momento).
Supresión
Al tratar de pensar en una situación en la que sería necesaria la supresión de esta advertencia y un buen diseño de código, me quedo en blanco. Alguien, por favor, comparta si ha tenido una experiencia en la que fue necesario silenciar esta advertencia (y lo anterior no maneja las cosas correctamente).
Más
También es posible crear un programa NSMethodInvocation
para manejar esto, pero hacerlo requiere escribir mucho más y también es más lento, por lo que hay pocas razones para hacerlo.
Historia
Cuando la performSelector:
familia de métodos se agregó por primera vez a Objective-C, ARC no existía. Mientras creaba ARC, Apple decidió que se debería 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 al enviar mensajes arbitrarios a través de un selector con nombre. En Objective-C, los desarrolladores pueden hacer esto utilizando conversiones de estilo C en punteros de función sin formato.
Con la introducción de Swift, Apple ha documentado la performSelector:
familia de métodos como "intrínsecamente inseguros" y no están disponibles para Swift.
Con el tiempo, hemos visto esta progresión:
- Las primeras versiones de Objective-C permiten
performSelector:
(administración manual de memoria) - Objective-C con ARC advierte sobre el uso de
performSelector:
- Swift no tiene acceso
performSelector:
ni documenta estos métodos como "intrínsecamente inseguros"
Sin embargo, la idea de enviar mensajes basados en un selector con nombre no es una característica "intrínsecamente insegura". Esta idea se ha utilizado con éxito durante mucho tiempo en Objective-C y en muchos otros lenguajes de programación.
1 Todos los métodos de Objective-C tienen dos argumentos ocultos self
y _cmd
se agregan implícitamente cuando se llama a un método.
2 Llamar a una NULL
función no es seguro en C. El guardia utilizado para comprobar la presencia del controlador garantiza que tenemos un objeto. Por lo tanto, sabemos que obtendremos una IMP
entrada de methodForSelector:
(aunque puede ser _objc_msgForward
una entrada al sistema de reenvío de mensajes). Básicamente, con la guardia en su lugar, sabemos que tenemos una función a la que llamar.
3 En realidad, es posible que obtenga información incorrecta si declara sus objetos como id
y no importa 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 elegir.
4 Consulte la referencia de ARC sobre valores de retorno retenidos y valores de retorno no retenidos para obtener más detalles.
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 recibe el error en varios lugares y desea utilizar el sistema de macros 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 de esta manera:
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 conjetura al respecto es la siguiente: dado que el compilador desconoce el selector, ARC no puede imponer una gestión adecuada de la memoria.
De hecho, hay ocasiones en las que la gestión de la memoria está ligada al nombre del método mediante una convención específica. Específicamente, estoy pensando en constructores de conveniencia versus métodos de creación ; el primero devuelve por convención un objeto liberado automáticamente; este último 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 gestión de memoria adecuada.
Si esto es correcto, creo que puede usar su código de manera segura, siempre que se asegure de que todo esté bien en cuanto a la administración de memoria (por ejemplo, que sus métodos no devuelvan los objetos que asignan).
En la configuración de compilación de su proyecto , en Otros indicadores de advertencia ( WARNING_CFLAGS
), agregue
-Wno-arc-performSelector-leaks
Ahora solo asegúrese de que el selector al que está llamando no haga que su objeto sea retenido o copiado.