web-dev-qa-db-fra.com

performSelector peut provoquer une fuite car son sélecteur est inconnu

Je reçois l'avertissement suivant du compilateur ARC:

"performSelector may cause a leak because its selector is unknown".

Voici ce que je fais:

[_controller performSelector:NSSelectorFromString(@"someMethod")];

Pourquoi ai-je cet avertissement? Je comprends que le compilateur ne peut pas vérifier si le sélecteur existe ou non, mais pourquoi cela causerait-il une fuite? Et comment puis-je changer mon code pour que je ne reçoive plus cet avertissement?

1242
Eduardo Scoz

Solution

Le compilateur met en garde à ce sujet pour une raison. Il est très rare que cet avertissement soit simplement ignoré et il est facile de contourner le problème. Voici comment:

if (!_controller) { return; }
SEL selector = NSSelectorFromString(@"someMethod");
IMP imp = [_controller methodForSelector:selector];
void (*func)(id, SEL) = (void *)imp;
func(_controller, selector);

Ou plus sommairement (bien que difficile à lire et sans la garde):

SEL selector = NSSelectorFromString(@"someMethod");
((void (*)(id, SEL))[_controller methodForSelector:selector])(_controller, selector);

Explication

Ce qui se passe ici, c'est que vous demandez au contrôleur le pointeur de fonction C pour la méthode correspondant au contrôleur. Tous les NSObjects répondent à methodForSelector:, mais vous pouvez également utiliser class_getMethodImplementation dans l'exécution d'Objective-C (utile si vous ne disposez que d'une référence de protocole, comme id<SomeProto>). Ces pointeurs de fonction sont appelés IMPs, et sont simples typedefed des pointeurs de fonction (id (*IMP)(id, SEL, ...))1. Cela peut être proche de la signature de méthode réelle de la méthode, mais ne correspond pas toujours exactement.

Une fois que vous avez le IMP, vous devez le convertir en un pointeur de fonction incluant tous les détails nécessaires à ARC (y compris les deux arguments cachés implicites self et _cmd de chaque objectif). Appel de méthode C). Ceci est traité dans la troisième ligne (le (void *) à droite indique simplement au compilateur que vous savez ce que vous faites et qu'il ne doit pas générer d'avertissement car les types de pointeurs ne correspondent pas).

Enfin, vous appelez le pointeur de fonction2.

Exemple complexe

Lorsque le sélecteur prend des arguments ou retourne une valeur, vous devrez changer un peu les choses:

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;

Raisonnement d'avertissement

La raison de cet avertissement est qu’avec ARC, le moteur d’exécution a besoin de savoir quoi faire avec le résultat de la méthode que vous appelez. Le résultat peut être n'importe quoi: void, int, char, NSString *, id, etc. ARC obtient normalement cette information de l'en-tête du type d'objet vous travaillez avec.3

ARC ne prendrait en compte que 4 éléments pour la valeur de retour:4

  1. Ignorer les types non-objets (void, int, etc.)
  2. Conserver la valeur de l'objet, puis relâcher quand il n'est plus utilisé (hypothèse standard)
  3. Libère les nouvelles valeurs d'objet lorsqu'elles ne sont plus utilisées (méthodes de la famille init/copy ou attribuées avec ns_returns_retained)
  4. Ne rien faire et supposer que la valeur d'objet renvoyée sera valide dans l'étendue locale (jusqu'à ce que le pool de libération le plus interne soit vidé, attribué avec ns_returns_autoreleased)

L'appel à methodForSelector: suppose que la valeur de retour de la méthode qu'il appelle est un objet, mais ne le conserve pas/ne le libère pas. Vous pouvez donc créer une fuite si votre objet est censé être publié comme indiqué au point 3 ci-dessus (en d’autres termes, la méthode que vous appelez renvoie un nouvel objet).

Pour les sélecteurs que vous essayez d'appeler et qui renvoient void ou d'autres objets non objets, vous pouvez permettre aux fonctionnalités du compilateur d'ignorer l'avertissement, mais cela peut être dangereux. J'ai vu Clang parcourir quelques itérations sur la façon dont il gère les valeurs de retour qui ne sont pas attribuées à des variables locales. Rien n'empêche, avec ARC, de ne pas conserver ni publier la valeur d'objet renvoyée par methodForSelector: même si vous ne souhaitez pas l'utiliser. Du point de vue du compilateur, c'est un objet après tout. Cela signifie que si la méthode que vous appelez, someMethod, renvoie un non-objet (y compris void), vous pouvez vous retrouver avec une valeur de pointeur de mémoire non conservée/libérée et un plantage.

Arguments supplémentaires

Une considération est que c'est le même avertissement qui se produira avec performSelector:withObject: et vous pourriez rencontrer des problèmes similaires en ne déclarant pas comment cette méthode consomme des paramètres. ARC permet de déclarer paramètres consommés , et si la méthode utilise le paramètre, vous enverrez probablement un message à un zombie et vous planterez. Il existe des moyens de contourner ce problème avec le casting ponté, mais il serait préférable d'utiliser simplement la méthodologie de la variable IMP et de la fonction ci-dessus. Les paramètres consommés étant rarement un problème, il est peu probable que cela se produise.

Sélecteurs Statiques

Fait intéressant, le compilateur ne se plaindra pas des sélecteurs déclarés statiquement:

[_controller performSelector:@selector(someMethod)];

La raison en est que le compilateur est capable d’enregistrer toutes les informations sur le sélecteur et l’objet lors de la compilation. Il n'est pas nécessaire de faire des hypothèses sur quoi que ce soit. (J'ai vérifié cela il y a un an en regardant la source, mais je n'ai pas de référence pour le moment.)

Suppression

En essayant de penser à une situation dans laquelle la suppression de cet avertissement serait nécessaire et une bonne conception de code, je ne sais rien. Quelqu'un s'il vous plaît partager s'il a eu une expérience où réduire au silence cet avertissement était nécessaire (et ce qui précède ne gère pas les choses correctement).

Plus

Il est possible de créer une NSMethodInvocation pour gérer cela également, mais cela nécessite beaucoup plus de dactylographie et est également plus lent, il n'y a donc aucune raison de le faire.

Histoire

Lorsque la famille de méthodes performSelector: a été ajoutée à Objective-C, ARC n'existait pas. Lors de la création d'ARC, Apple a décidé qu'un avertissement devait être généré pour ces méthodes afin de guider les développeurs vers d'autres moyens permettant de définir explicitement le traitement de la mémoire lors de l'envoi de messages arbitraires via un sélecteur nommé. En Objective-C, les développeurs peuvent le faire en utilisant des conversions de style C sur des pointeurs de fonction bruts.

Avec l'introduction de Swift, Apple a documenté la famille performSelector: comme "intrinsèquement non sécurisée" et elles ne sont pas disponibles pour Swift.

Au fil du temps, nous avons vu cette progression:

  1. Les premières versions d'Objective-C permettent performSelector: (gestion manuelle de la mémoire)
  2. Objective-C avec ARC met en garde contre l'utilisation de performSelector:
  3. Swift n'a pas accès à performSelector: et documente ces méthodes comme "intrinsèquement peu sûres"

L’idée d’envoyer des messages en fonction d’un sélecteur nommé n’est toutefois pas une fonctionnalité "intrinsèquement dangereuse". Cette idée est utilisée avec succès depuis longtemps dans Objective-C ainsi que dans de nombreux autres langages de programmation.


1 Toutes les méthodes Objective-C ont deux arguments cachés, self et _cmd, qui sont ajoutés implicitement lorsque vous appelez une méthode.

2 L'appel d'une fonction NULL n'est pas sûr en C. La protection utilisée pour vérifier la présence du contrôleur garantit que nous avons un objet. Nous savons donc que nous obtiendrons une IMP de methodForSelector: (bien que cela puisse être _objc_msgForward, entrée dans le système de transfert de messages). En gros, avec la garde en place, nous savons que nous avons une fonction à appeler.

3 En fait, il est possible qu'il obtienne des informations erronées si vous déclarez des objets comme id et que vous n'importez pas tous les en-têtes. Vous pourriez vous retrouver avec des plantages dans du code que le compilateur pense bien. C'est très rare, mais cela peut arriver. Généralement, vous recevez simplement un avertissement indiquant qu'il ne sait pas laquelle des deux signatures de méthode parmi lesquelles choisir.

4 Voir la référence ARC sur valeurs de retour conservées et valeurs de retour non retenues pour plus de détails.

1193
wbyoung

Dans le compilateur LLVM 3.0 dans Xcode 4.2, vous pouvez supprimer l’avertissement comme suit:

#pragma clang diagnostic Push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self.ticketTarget performSelector: self.ticketAction withObject: self];
#pragma clang diagnostic pop

Si vous obtenez l'erreur à plusieurs endroits et que vous souhaitez utiliser le système de macro C pour masquer les pragmas, vous pouvez définir une macro pour faciliter la suppression de l'avertissement:

#define SuppressPerformSelectorLeakWarning(Stuff) \
    do { \
        _Pragma("clang diagnostic Push") \
        _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
        Stuff; \
        _Pragma("clang diagnostic pop") \
    } while (0)

Vous pouvez utiliser la macro comme ceci:

SuppressPerformSelectorLeakWarning(
    [_target performSelector:_action withObject:self]
);

Si vous avez besoin du résultat du message exécuté, vous pouvez le faire:

id result;
SuppressPerformSelectorLeakWarning(
    result = [_target performSelector:_action withObject:self]
);
1178
Scott Thompson

Mon hypothèse à ce sujet est la suivante: étant donné que le sélecteur est inconnu du compilateur, ARC ne peut pas appliquer une gestion de mémoire appropriée.

En fait, il arrive parfois que la gestion de la mémoire soit liée au nom de la méthode par une convention spécifique. Plus précisément, je pense aux méthodes constructeurs de commodité versus make; les anciens rendent par convention un objet auto-libéré; ce dernier est un objet retenu. La convention est basée sur les noms du sélecteur. Par conséquent, si le compilateur ne connaît pas le sélecteur, il ne peut pas appliquer la règle de gestion de la mémoire appropriée.

Si cela est correct, je pense que vous pouvez utiliser votre code en toute sécurité, à condition de vous assurer que tout est ok pour la gestion de la mémoire (par exemple, que vos méthodes ne renvoient pas les objets qu'elles allouent).

208
sergio

Dans votre projet Paramètres de construction, sous Autres drapeaux d'avertissement (WARNING_CFLAGS), ajoutez
-Wno-arc-performSelector-leaks

Maintenant, assurez-vous que le sélecteur que vous appelez ne fait pas que votre objet soit conservé ou copié.

120
0xced

Pour contourner le problème jusqu’à ce que le compilateur permette de remplacer l’avertissement, vous pouvez utiliser le moteur d’exécution.

objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));

au lieu de

[_controller performSelector:NSSelectorFromString(@"someMethod")];

Tu devras

#import <objc/message.h>
111
jluckyiv

Pour ignorer l'erreur uniquement dans le fichier avec le sélecteur d'exécution, ajoutez un #pragma comme suit:

#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

Cela ignorerait l'avertissement sur cette ligne, mais le laisserait tout au long du reste de votre projet.

88
Barlow Tucker

Étrange mais vrai: si cela est acceptable (c'est-à-dire que le résultat est nul et que cela ne vous gêne pas de laisser le cycle de cycle d'exécution tourner une fois), ajoutez un délai, même s'il est égal à zéro:

[_controller performSelector:NSSelectorFromString(@"someMethod")
    withObject:nil
    afterDelay:0];

Cela supprime l'avertissement, probablement parce que cela rassure le compilateur qu'aucun objet ne peut être retourné et en quelque sorte mal géré.

68
matt

Voici une macro mise à jour basée sur la réponse donnée ci-dessus. Celui-ci devrait vous permettre d’envelopper votre code même avec une instruction return.

#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
syvex

Ce code n'implique pas de drapeaux de compilateur ni d'appels d'exécution directs:

SEL selector = @selector(zeroArgumentMethod);
NSMethodSignature *methodSig = [[self class] instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setSelector:selector];
[invocation setTarget:self];
[invocation invoke];

NSInvocation permet à plusieurs arguments d'être définis de sorte que, contrairement à performSelector, cela fonctionnera avec n'importe quelle méthode.

31
Benedict Cohen

Eh bien, beaucoup de réponses ici, mais comme c'est un peu différent, en combinant quelques réponses, je pensais l'inclure. J'utilise une catégorie NSObject qui vérifie que le sélecteur renvoie nuls et supprime également le compilateur. Attention.

#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://stackoverflow.com/questions/7017281/performselector-may-cause-a-leak-because-its-selector-is-unknown

- (void) checkSelector:(SEL)aSelector {
    // See http://stackoverflow.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
20
Chris Prince

Pour la postérité, j'ai décidé de jeter mon chapeau dans le ring :)

Récemment, j'ai vu de plus en plus de restructurations s'éloigner du paradigme target/selector, en faveur d'éléments tels que les protocoles, les blocs, etc. Cependant, il existe un remplacement instantané pour performSelector que j'ai utilisé plusieurs fois maintenant:

[NSApp sendAction: NSSelectorFromString(@"someMethod") to: _controller from: nil];

Celles-ci semblent constituer un remplacement propre, sans danger pour les arcs et presque identique pour performSelector sans avoir à trop en parler avec objc_msgSend().

Bien que, je n'ai aucune idée s'il existe un analogue disponible sur iOS.

16
Patrick Perini

La réponse de Matt Galloway sur ce fil explique le pourquoi:

Considérer ce qui suit:

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

Maintenant, comment ARC peut-il savoir que le premier retourne un objet avec un nombre de retenues égal à 1 alors que le second renvoie un objet autoreleased?

Il semble qu'il soit généralement prudent de supprimer l'avertissement si vous ignorez la valeur de retour. Je ne sais pas quelle est la meilleure pratique si vous avez vraiment besoin d'obtenir un objet conservé de performSelector, à l'exception de "ne le faites pas".

15
c roald

@ c-road fournit le bon lien avec la description du problème ici . Ci-dessous, vous pouvez voir mon exemple, lorsque performSelector provoque une fuite de mémoire.

@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;
}

La seule méthode qui provoque une fuite de mémoire dans mon exemple est CopyDummyWithLeak. La raison en est que ARC ne sait pas, que copySelector renvoie l'objet conservé.

Si vous utilisez l'outil Memory Leak Tool, vous pouvez voir l'image suivante: enter image description here ... et il n'y a aucune fuite de mémoire dans les autres cas: enter image description here

14
Pavel Osipov

Pour rendre la macro de Scott Thompson plus générique:

// 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")

Ensuite, utilisez-le comme ceci:

MYSilenceWarning(-Warc-performSelector-leaks,
[_target performSelector:_action withObject:self];
                )
6
Ben Flynn

Ne supprimez pas les avertissements!

Il n'y a pas moins de 12 solutions alternatives au bricolage avec le compilateur.
Bien que vous soyez intelligent lors de la première mise en œuvre, peu d'ingénieurs sur Terre peuvent suivre vos traces, et ce code finira par se briser.

Routes sécurisées:

Toutes ces solutions fonctionneront, avec un certain degré de variation par rapport à votre intention initiale. Supposons que param puisse être nil si vous le souhaitez:

Route sécurisée, même comportement conceptuel:

// 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]];

Route sûre, comportement légèrement différent:

(Voir this réponse)
Utilisez n’importe quel fil au lieu 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]];

Routes dangereuses

Nécessite une sorte de désactivation du compilateur, qui est vouée à la rupture. Notez que pour le moment, il est entré Swift .

// AT YOUR OWN RISK
[_controller performSelector:selector];
[_controller performSelector:selector withObject:anArgument];
[_controller performSelector:selector withObject:anArgument withObject:nil];
6
SwiftArchitect

Comme vous utilisez ARC, vous devez utiliser iOS 4.0 ou une version ultérieure. Cela signifie que vous pouvez utiliser des blocs. Si au lieu de vous rappeler le sélecteur à exécuter, vous preniez un bloc, ARC serait en mesure de mieux suivre ce qui se passe réellement et vous ne courriez pas le risque d'introduire accidentellement une fuite de mémoire.

4
honus

Au lieu d'utiliser l'approche par blocs, ce qui m'a posé quelques problèmes:

    IMP imp = [_controller methodForSelector:selector];
    void (*func)(id, SEL) = (void *)imp;

Je vais utiliser NSInvocation, comme ceci:

    -(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];
    }
2
supersabbath

Si vous n'avez pas besoin de passer d'argument, une solution simple consiste à utiliser valueForKeyPath. Ceci est même possible sur un objet Class.

NSString *colorName = @"brightPinkColor";
id uicolor = [UIColor class];
if ([uicolor respondsToSelector:NSSelectorFromString(colorName)]){
    UIColor *brightPink = [uicolor valueForKeyPath:colorName];
    ...
}
1
arsenius