Solución
El compilador está advirtiendo sobre esto por una razón. Es muy raro que esta advertencia simplemente se ignore, y es fácil de evitar. 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 brevemente (aunque difícil de leer y sin el protector):
SEL selector = NSSelectorFromString(@"someMethod");
((void (*)(id, SEL))[_controller methodForSelector:selector])(_controller, selector);
Explicación
Lo que está sucediendo aquí es que le está pidiendo al controlador el puntero de función C para el método correspondiente al controlador. Todos NSObject
responden methodForSelector:
, pero también se puede 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 de función se denominan IMP
s, y son typedef
punteros de función ed simples ( 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 tenga el 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
de cada llamada al método Objective-C). Esto se maneja en la tercera línea (el (void *)
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, 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 para la advertencia
La razón de esta advertencia es que con ARC, el tiempo 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 recibe esta información del encabezado del tipo de objeto que está trabajando. 3
En realidad, solo hay 4 cosas que ARC consideraría para el valor de retorno: 4
- Tipos Ignorar no-objeto (
void
, int
, etc)
- Retenga el valor del objeto, luego suéltelo cuando ya no se use (suposición estándar)
- Libere nuevos valores de objetos cuando ya no se usen (métodos en
init
/ copy
family o atribuidos con ns_returns_retained
)
- No hacer nada y asumir que el valor del objeto devuelto será válido en el ámbito local (hasta que se agote el conjunto de versiones más interno, atribuido con
ns_returns_autoreleased
)
La llamada a methodForSelector:
supone que el valor de retorno del método al que está llamando 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 n. ° 3 anterior (es decir, el método al que llama devuelve un nuevo objeto).
Para los selectores que intenta 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 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 que se devuelve methodForSelector:
aunque no quiera usarlo. Desde la perspectiva del compilador, es un objeto después de todo. Eso significa que si el método al que está llamando someMethod
devuelve un objeto no incluido (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 ocurrirá performSelector:withObject:
y podría tener 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, es probable que finalmente envíe un mensaje a un zombie y se bloquee. Hay formas de evitar esto con la conversión en puente, pero realmente sería mejor simplemente usar la IMP
metodología de puntero y función anterior. Dado que los parámetros consumidos rara vez son un problema, es probable que esto no ocurra.
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 grabar 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 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 del código, me quedo en blanco. Alguien comparta si han tenido una experiencia en la que fue necesario silenciar esta advertencia (y lo anterior no maneja las cosas correctamente).
Más
Es posible construir 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 performSelector:
familia de métodos se agregó por primera vez a Objective-C, ARC no existía. Al crear 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 mediante el uso de 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:
(gestión de memoria manual)
- Objective-C con ARC advierte sobre el uso de
performSelector:
- Swift no tiene acceso
performSelector:
y documenta estos métodos como "inherentemente inseguros"
La idea de enviar mensajes basados en un selector con nombre no es, sin embargo, una característica "inherentemente 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 añaden de forma implícita cuando se llama a un método.
2 Llamar a una NULL
función no es seguro en C. El protector utilizado para verificar la presencia del controlador asegura que tengamos un objeto. Por lo tanto, sabemos que conseguiremos una IMP
de methodForSelector:
(aunque puede ser _objc_msgForward
, la entrada en el sistema de reenvío de mensajes). Básicamente, con la guardia en su lugar, sabemos que tenemos una función para llamar.
3 En realidad, es posible que obtenga la información incorrecta si declara sus objetos como id
y no está importando todos los encabezados. Podría terminar con bloqueos 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étodos elegir.
4 Véase la referencia ARC en valores de retorno retenidos y los valores de retorno no retenidos para más detalles.