35

Durante años he estado siguiendo un gran patrón llamado Target-Action que dice así:¿El patrón de diseño Target-Action se convirtió en una mala práctica bajo ARC?

Un objeto llama a un selector especificado en un objeto de destino específico cuando llega el momento de llamar. Esto es muy útil en muchos casos diferentes en los que necesita una devolución de llamada simple a un método arbitrario.

He aquí un ejemplo:

- (void)itemLoaded { 
    [specifiedReceiver performSelector:specifiedSelector]; 
} 

Bajo ARC ahora resulta que hacer algo como esto, de repente se convirtió en peligroso.

Xcode lanza una advertencia que dice así:

PerformSelector puede causar una fuga debido a su selector se desconoce

Por supuesto, el selector es desconocida ya que como parte del diseño Meta-Acción patrón puede especificar el selector que desee para recibir una llamada cuando sucede algo interesante.

Lo que más me molesta acerca de esta advertencia es que dice que puede haber una posible pérdida de memoria. Desde mi punto de vista, ARC no dobla las reglas de administración de memoria, sino que simplemente automatiza la inserción de mensajes de retención/liberación/liberación automática en las ubicaciones correctas.

Otra cosa a tener en cuenta aquí: -performSelector: tiene un valor de retorno id. ARC analiza las firmas de métodos para descubrir mediante la aplicación de convenciones de nomenclatura si el método devuelve un +1 retener el objeto de recuento o no. En este caso, ARC no sabe si el selector es una fábrica -newFooBar o simplemente llama a un método de trabajo no sospechoso (que casi siempre es el caso con Target-Action). En realidad, ARC debería haber reconocido que no esperaba un valor de retorno, y por lo tanto, olvidarme de cualquier potencial +1 de retención del valor de retorno contado. Mirándolo desde ese punto de vista, puedo ver de dónde viene el ARC, pero aún hay demasiada incertidumbre sobre lo que realmente significa en la práctica.

¿Eso significa ahora que bajo ARC algo puede ir mal, lo que nunca ocurriría sin ARC? No veo cómo esto podría producir una pérdida de memoria. ¿Puede alguien dar ejemplos de situaciones en las que es peligroso hacerlo y cómo se crea exactamente una fuga en ese caso?

Realmente busqué en Google, pero no encontré ningún sitio explicando por qué.

Respuesta

24

El problema es que performSelector ARC no sabe lo que el selector que se realiza, lo hace.Considere lo siguiente:

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

Ahora, ¿cómo se puede saber ARC que las primeras declaraciones de un objeto con una cuenta de retención de 1, pero el segundo devuelve un objeto que se autoreleased? (Estoy definiendo un método llamado giveMeAnotherNonRetainedObject aquí que devuelve algo que se ha lanzado automáticamente). Si no agrega ninguna versión, entoncesse filtraría aquí.

Obviamente en mi ejemplo, los selectores que se realizarán son realmente conocidos, pero imagine que fueron elegidos en tiempo de ejecución. ARC realmente no pudo hacer su trabajo al ingresar el número correcto de retain o release s porque simplemente no sabe qué hará el selector. Tiene razón en que ARC no está doblando ninguna regla y solo está agregando las llamadas correctas de administración de memoria para usted, pero eso es precisamente lo que no puede hacer aquí.

Tiene razón en que el hecho de que esté ignorando el valor de retorno significa que va a estar bien, pero en general ARC está siendo exigente y advirtiendo. Pero supongo que es por eso que es una advertencia y no un error.

Editar:

Si estás realmente seguro de su código está bien, usted podría ocultar la advertencia de este modo:

#pragma clang diagnostic push 
#pragma clang diagnostic ignored "-Warc-performSelector-leaks" 
[specifiedReceiver performSelector:specifiedSelector]; 
#pragma clang diagnostic pop 
+1

Tiene toda la razón. Pero en mi caso ignoro completamente el valor de retorno, lo que en una consecuencia lógica significa que no puede haber un problema en esta ubicación. Incluso he intentado convertir el void, el bridging, __unsafe_unretained, etc. - la única forma de deshacerse de él es pasar a bloques (lo que causa una plétora de otros problemas) o deshabilitar la advertencia en la configuración de fases de compilación. –

+0

Ver mi edición para desactivar la advertencia por solo un pedacito de código. – mattjgalloway

+0

Dos cerebros, el mismo pensamiento. ¡Estupendo! :-) (mecanografió mi respuesta al mismo tiempo. contiene más información sobre la desactivación de la advertencia) –

3

ARC lanza la advertencia porque no puede garantizar que el selector no está creando un objeto que desconoce. Se podría teóricamente recibir algo de ese método que ARCO no puede manejar:

id objectA = [someObject performSelector:@selector(createObjectA)]; 

Tal vez algún día se puede, pero en este momento no se puede. (Tenga en cuenta que si conoce el objeto (no es una identificación) no arroja esta advertencia).

Si está intentando simplemente ejecutar un método sin recibir un objeto de vuelta, le recomiendo usar objc_msgSend. Pero Tienes que incluyen en su clase:

#include <objc/message.h> 
objc_msgSend(someObject, action); 
+2

Mejor aún, utilice un bloque escrito como un manejador de finalización en lugar de especificar un selector arbitraria. –

+0

Me han dicho que llamar a objc_msgSend tiene muchos inconvenientes y puede ser peligroso en algunas situaciones. –

+2

Sí, he movido todas mis clases personalizadas del modelo 'delegado' a usar bloques. Pero realmente tienes que tener cuidado con la administración de memoria y los bloques. Con el modelo de delegado, el control puede interrumpir el ciclo de retención utilizando una referencia débil al delegado. Pero los bloques se retienen automáticamente al hacer referencia a iVars. Entonces, cualquiera que sea la clase que esté asignando el bloque, debe asegurarse de que no se esté refiriendo fuertemente si también mantiene una referencia al control (que almacena el bloque). Creo que esta es la razón por la que Apple no usará bloques de finalización en lugar de delegados. –

9

La advertencia debe leer como esto:

PerformSelector puede causar una fuga porque su selector es desconocido. ARC no sabe si el ID devuelto tiene un +1 retener recuento o no, y por lo tanto no puede gestionar correctamente la memoria del objeto devuelto.

Lamentablemente, es solo la primera oración.

Ahora la solución:

Si recibe un valor de retorno de un método -performSelector, no se puede hacer nada acerca de la advertencia en el código, excepto ignorarlo.

NSArray *linkedNodes = [startNode performSelector:nodesArrayAccessor]; 

Su mejor apuesta es la siguiente:

#pragma clang diagnostic push 
#pragma clang diagnostic ignored "-Warc-performSelector-leaks" 
NSArray *linkedNodes = [startNode performSelector:nodesArrayAccessor]; 
#pragma clang diagnostic pop 

Lo mismo sucede con el caso de mi pregunta inicial, en la que ignoran por completo el valor de retorno. ARC debería ser lo suficientemente inteligente como para ver que no me importa la ID devuelta, y por lo tanto, el selector anónimo está casi garantizado que no será una fábrica, un constructor de conveniencia o lo que sea. Lamentablemente, ARC no lo es, por lo que se aplica la misma regla. Ignora la advertencia.

También se puede hacer para todo el proyecto configurando el indicador del compilador -Wno-arc-performSelector-leaks en "Otros indicadores de advertencia" en la configuración de compilación del proyecto.

Como alternativa, puede eliminar la advertencia por archivo cuando agrega ese indicador en Su destino> "Fases de compilación"> "Compilar fuentes" en el lado derecho al lado del archivo deseado.

Las tres soluciones son muy desagradables en mi humilde opinión así que espero que alguien encuentre una mejor.

4

Como se describió anteriormente, recibe esa advertencia porque el compilador no sabe dónde (o si) colocar el retener/liberar el performSelector: valor de retorno.

Pero tenga en cuenta que si usa [someObject performSelector:@selector(selectorName)] no generará advertencias (al menos en Xcode 4.5 con llvm 4.1) porque el selector exacto es fácil de determinar (lo establece explícitamente) y es por eso que el compilador puede poner el retener/liberar en el lugar correcto.

Es por eso que recibirá una advertencia solo si pasa el selector usando el puntero SEL porque en ese caso el compilador no puede determinar en todo caso qué hacer. Por lo tanto, utilizando el siguiente

SEL s = nil; 
if(condition1) SEL = @selector(sel1) 
else SEL = @selector(sel2) 

[self performSelector:s]; 

generará una advertencia. Pero refactorización que sea:

if(condition1) [self performSelector:@selector(sel1)] 
else [self performSelector:@selector(sel2)] 

no generará ninguna advertencia

Cuestiones relacionadas