ios - PerformSelector puede causar una fuga porque su selector es desconocido
objective-c memory-leaks (19)
No suprimir las advertencias!
No hay menos de 12 soluciones alternativas para jugar con el compilador.
Mientras estás siendo inteligente en el momento de la primera implementación, pocos ingenieros en la Tierra pueden seguir tus pasos, y este código eventualmente se romperá.
Rutas seguras:
Todas estas soluciones funcionarán, con cierto grado de variación con respecto a 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 this 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]];
Rutas peligrosas
Requiere algún tipo de silenciamiento del compilador, que está destinado a romperse. Tenga en cuenta que en la actualidad, que se rompen en Swift .
// AT YOUR OWN RISK
[_controller performSelector:selector];
[_controller performSelector:selector withObject:anArgument];
[_controller performSelector:selector withObject:anArgument withObject:nil];
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é causaría una fuga? ¿Y cómo puedo cambiar mi código para que no reciba más esta advertencia?
Solución
El compilador está advirtiendo sobre esto por una razón. Es muy raro que esta advertencia simplemente deba ignorarse, 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 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 los NSObject
s responden a methodForSelector:
pero también 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 simples punteros de función tipificados ( 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 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 sabe lo que está haciendo y no genera una advertencia ya que los tipos de puntero no coinciden).
Finalmente, se llama a la función puntero 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
El motivo de esta advertencia es que con ARC, el tiempo de ejecución debe 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, solo hay 4 cosas que ARC consideraría para el valor de retorno: 4
- Ignorar los tipos que no son objetos (
void
,int
, etc) - Retener el valor del objeto, luego liberarlo cuando ya no se use (suposición estándar)
- Liberar nuevos valores de objeto cuando ya no se utilicen (métodos en la familia de
init
/copy
o atribuidos conns_returns_retained
) - No haga nada y suponga que el valor de objeto devuelto será válido en el ámbito local (hasta que se
ns_returns_autoreleased
agrupación de versiones más interna, se atribuye conns_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 debe ser liberado como en el # 3 anterior (es decir, el método al que llama devuelve un nuevo objeto).
Para los selectores a los que intenta llamar que devuelven el 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 las variables locales. No hay razón para que con ARC habilitado no pueda retener y liberar el valor del objeto que 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 que está llamando, someMethod
, está devolviendo un objeto no (incluido el void
), podría terminar con un valor de puntero de basura que se retiene / libera y se bloquea.
Argumentos adicionales
Una consideración es que esto es lo mismo que ocurrirá con 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, probablemente enviará un mensaje a un zombi y se bloqueará. Hay formas de solucionar este problema con la conversión en puente, pero en realidad sería mejor simplemente usar la IMP
y la metodología de puntero de función de arriba. Dado que los parámetros consumidos rara vez son un problema, es probable que esto no 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 en realidad puede registrar toda la información sobre el selector y el objeto durante la compilación. No es necesario hacer ninguna suposición sobre nada. (Lo verifiqué 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 sería necesaria la supresión de esta advertencia y un buen diseño de código, me quedo en blanco. Alguien, por favor, comparte 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 NSMethodInvocation
para manejar esto, pero hacerlo requiere mucho más escritura y también es más lento, por lo que hay pocas razones para hacerlo.
Historia
Cuando el performSelector:
familia de métodos se agregó por primera vez a Objective-C, ARC no existía. Al crear ARC, Apple decidió que debería generarse 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 en bruto.
Con la introducción de Swift, Apple ha documentado la familia de métodos performSelector:
como "inherentemente 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 de memoria manual) - Objective-C con ARC advierte sobre el uso de
performSelector:
- Swift no tiene acceso a
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 agregan implícitamente cuando se llama a un método.
2 Llamar a una función NULL
no es seguro en C. La guarda usada para verificar la presencia del controlador asegura que tengamos un objeto. Por lo tanto, sabemos que obtendremos un IMP
de methodForSelector:
(aunque puede ser _objc_msgForward
, 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 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 considera que está bien. Esto es muy raro, pero podría suceder. Por lo general, solo recibirá una advertencia de que no sabe cuál de las dos firmas de métodos puede elegir.
4 Consulte la referencia ARC en valores de retorno retenidos y valores de retorno no retenidos para más detalles.
@ c-road proporciona el enlace correcto con la descripción del problema aquí . A continuación puede ver mi ejemplo, cuando performSelector provoca una pérdida de memoria.
@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 provoca una pérdida de memoria en mi ejemplo es CopyDummyWithLeak. La razón es que ARC no sabe, que copySelector devuelve el objeto retenido.
Si ejecuta la herramienta de pérdida de memoria, puede ver la siguiente imagen: ... y no hay pérdidas de memoria en ningún otro caso:
Aquí hay una macro actualizada basada en la respuesta dada anteriormente. Éste debería permitirle ajustar 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]
);
Bueno, hay muchas respuestas aquí, pero como esto es un poco diferente, combinando algunas respuestas, pensé que las pondría. Estoy usando una categoría NSObject que verifica que el selector regrese al vacío y que también suprima el compilador 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://.com/questions/7017281/performselector-may-cause-a-leak-because-its-selector-is-unknown
- (void) checkSelector:(SEL)aSelector {
// See http://.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 ignored "-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 ignored "-Warc-performSelector-leaks"
[self performSelector: aSelector];
#pragma clang diagnostic pop
}
@end
Como solución temporal 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>
Debido a que está utilizando ARC, debe usar iOS 4.0 o posterior. Esto significa que podrías usar bloques. Si en lugar de recordar el selector para ejecutarse, tomara un bloque, ARC podría rastrear mejor lo que realmente está sucediendo y no tendría que correr el riesgo de introducir accidentalmente una pérdida de memoria.
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 facilitar la supresión de 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]
);
En la configuración de compilación de su proyecto, bajo 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.
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, por lo que, a diferencia de performSelector
, funcionará con cualquier método.
Extraño pero cierto: si es aceptable (es decir, el resultado es nulo y no le importa dejar que el ciclo de ejecución de una vez), agregue un retraso, incluso si esto es cero:
[_controller performSelector:NSSelectorFromString(@"someMethod")
withObject:nil
afterDelay:0];
Esto elimina la advertencia, presumiblemente porque reafirma al compilador de que no se puede devolver ningún objeto y, de alguna manera, no administrarse correctamente.
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 saber ARC que el primero devuelve un objeto con una cuenta de retención de 1, pero el segundo devuelve un objeto que se lanza automáticamente?
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 obtener un objeto retenido de performSelector, aparte de "no hacer eso".
Mi conjetura acerca de esto es la siguiente: dado que el selector es desconocido para el compilador, ARC no puede imponer una administración de memoria adecuada.
De hecho, hay ocasiones en que la administración de la memoria está vinculada al nombre del método por una convención específica. Específicamente, estoy pensando en los constructores de conveniencia frente a los métodos de fabricación ; los primeros devuelven por convención un objeto lanzado 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 imponer la regla de administració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 lo que respecta a la administración de la memoria (por ejemplo, que sus métodos no devuelven objetos que asignan).
Para hacer que la macro de Scott Thompson sea 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 ignored MY_STR(FLAG))) /
MACRO /
_Pragma("clang diagnostic pop")
Entonces úsalo así:
MYSilenceWarning(-Warc-performSelector-leaks,
[_target performSelector:_action withObject:self];
)
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 permitiría en el resto de su proyecto.
Para la posteridad, he decidido tirar mi sombrero en el anillo :)
Recientemente he estado viendo más y más reestructuraciones fuera del paradigma de target
/ selector
, a favor de cosas como protocolos, bloques, etc. Sin embargo, hay un reemplazo performSelector
para performSelector
que he usado varias veces ahora :
[NSApp sendAction: NSSelectorFromString(@"someMethod") to: _controller from: nil];
Estos parecen ser un reemplazo limpio, seguro para ARC y casi idéntico para performSelector
sin tener que objc_msgSend()
mucho con objc_msgSend()
.
Sin embargo, no tengo idea si hay un análogo disponible en iOS.
En lugar de utilizar el enfoque de bloque, que me dio algunos problemas:
IMP imp = [_controller methodForSelector:selector];
void (*func)(id, SEL) = (void *)imp;
Usaré NSInvocation, así:
-(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];
}
Si no necesita pasar ningún argumento, es una solución fácil de usar valueForKeyPath
. Esto es incluso posible en un Class
objeto.
NSString *colorName = @"brightPinkColor";
id uicolor = [UIColor class];
if ([uicolor respondsToSelector:NSSelectorFromString(colorName)]){
UIColor *brightPink = [uicolor valueForKeyPath:colorName];
...
}
También podrías usar un protocolo aquí. Entonces, crea un protocolo así:
@protocol MyProtocol
-(void)doSomethingWithObject:(id)object;
@end
En su clase que necesita llamar a su selector, entonces tiene una propiedad @.
@interface MyObject
@property (strong) id<MyProtocol> source;
@end
Cuando necesite llamar @selector(doSomethingWithObject:)
a una instancia de MyObject, haga esto:
[self.source doSomethingWithObject:object];