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




objective-c memory-leaks (13)

Je reçois l'avertissement suivant par le 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 ne plus recevoir cet avertissement?


Ne supprime pas les avertissements!

Il n'y a pas moins de 12 solutions alternatives au bricolage avec le compilateur.
Alors que vous êtes intelligent au moment 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 désirez:

Itinéraire sécurisé, 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]];

Itinéraire sécurisé, comportement légèrement différent:

(Voir this réponse)
Utilisez n'importe quel fil à la place 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 mise en silence du compilateur, qui est liée à la rupture. Note that at present time, it did break in Swift .

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

Solution

Le compilateur avertit à ce sujet pour une raison. Il est très rare que cet avertissement soit simplement ignoré et qu'il soit 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 lacunaire (bien que difficile à lire et sans le garde):

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

Explication

Qu'est-ce qui se passe ici, vous demandez au contrôleur pour le pointeur de la fonction C pour la méthode correspondant au contrôleur. Tous les methodForSelector: NSObject répondent à methodForSelector: mais vous pouvez également utiliser class_getMethodImplementation dans le runtime Objective-C (utile si vous avez seulement une référence de protocole, comme id<SomeProto> ). Ces pointeurs de fonction sont appelés IMP s, et sont des pointeurs de fonction simple typedef ed ( id (*IMP)(id, SEL, ...) ) 1 . Cela peut être proche de la signature de la méthode, mais ne correspondra pas toujours exactement.

Une fois que vous avez l' IMP , vous devez le convertir en un pointeur de fonction qui inclut tous les détails dont ARC a besoin (y compris les deux arguments cachés implicites self et _cmd de chaque appel de méthode Objective-C). Ceci est traité dans la troisième ligne (le (void *) sur le côté droit indique simplement au compilateur que vous savez ce que vous faites et ne pas générer d'avertissement puisque les types de pointeurs ne correspondent pas).

Enfin, vous appelez le pointeur de fonction 2 .

Exemple complexe

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

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 pour l'avertissement

La raison de cet avertissement est qu'avec ARC, le runtime doit 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 avec lequel vous travaillez. 3

Il n'y a vraiment que 4 choses qu'ARC considérerait pour la valeur de retour: 4

  1. Ignorer les types non-objet ( 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 dans la famille init / copy ou attribuées avec ns_returns_retained )
  4. Ne rien faire & supposer que la valeur de l'objet retourné sera valide dans la portée locale (jusqu'à ce que le pool de versions le plus interne soit drainé, attribué avec ns_returns_autoreleased )

L'appel à methodForSelector: suppose que la valeur de retour de la méthode appelée est un objet, mais ne le conserve pas / ne le libère pas. Ainsi, vous pourriez finir par créer une fuite si votre objet est censé être libéré comme dans # 3 ci-dessus (c'est-à-dire que la méthode que vous appelez retourne un nouvel objet).

Pour les sélecteurs que vous tentez d'appeler void ou d'autres non-objets, vous pouvez activer les fonctions du compilateur pour ignorer l'avertissement, mais cela peut être dangereux. J'ai vu Clang passer par quelques itérations de la façon dont il gère les valeurs de retour qui ne sont pas affectées aux variables locales. Il n'y a aucune raison pour qu'ARC soit activé et qu'il ne puisse pas conserver et libérer la valeur de l'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 objet non (y compris void ), vous pourriez vous retrouver avec une valeur de pointeur de déchets qui est conservée / libérée et se bloque.

Arguments supplémentaires

Une considération est que c'est le même avertissement 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 les paramètres consommés , et si la méthode consomme le paramètre, vous finirez probablement par envoyer un message à un zombie et tomber en panne. Il existe des moyens de contourner ce problème avec un casting en pont, mais il serait vraiment préférable d'utiliser simplement la méthodologie du pointeur IMP et de la fonction ci-dessus. Étant donné que les paramètres consommés sont 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 en mesure d'enregistrer toutes les informations sur le sélecteur et l'objet lors de la compilation. Il n'a pas besoin de faire des suppositions à propos de 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 où la suppression de cet avertissement serait nécessaire et la conception de code bonne, je viens vide. Quelqu'un s'il vous plaît partager si elles ont eu une expérience où le silence de cet avertissement était nécessaire (et ce qui précède ne gère pas les choses correctement).

Plus

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

Histoire

Lorsque la famille de méthodes performSelector: été ajoutée pour la première fois à Objective-C, l'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 l'utilisation d'autres moyens pour définir explicitement comment la mémoire devrait être gérée lors de l'envoi de messages arbitraires via un sélecteur nommé. En Objective-C, les développeurs peuvent faire cela en utilisant des jets de style C sur les pointeurs de fonctions brutes.

Avec l'introduction de Swift, Apple a documenté le performSelector: famille de méthodes est "intrinsèquement dangereuse" 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 dangereuses"

L'idée d'envoyer des messages basés sur un sélecteur nommé n'est cependant pas une fonctionnalité "intrinsèquement dangereuse". Cette idée a été utilisée avec succès pendant longtemps dans Objective-C ainsi que de nombreux autres langages de programmation.

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

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

En fait, il est possible qu'il obtienne la mauvaise information si vous déclarez vos objets comme id et que vous n'importiez pas tous les en-têtes. Vous pourriez vous retrouver avec des plantages dans le code que le compilateur pense très bien. C'est très rare, mais cela pourrait arriver. Habituellement, vous recevrez simplement un avertissement indiquant qu'il ne sait pas laquelle des deux signatures de méthode choisir.

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


@ 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 lancez Memory Leak Tool, vous pouvez voir l'image suivante: ... et il n'y a pas de fuites de mémoire dans les autres cas:


Ce code n'implique pas de drapeaux de compilateur ou 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 de NSInvocation plusieurs arguments, contrairement à performSelector cela fonctionnera sur n'importe quelle méthode.


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 effectué, vous pouvez le faire:

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

Dans les paramètres de construction de votre projet, sous Autres WARNING_CFLAGS avertissement ( WARNING_CFLAGS ), ajoutez
-Wno-arc-performSelector-leaks

Maintenant, assurez-vous que le sélecteur que vous appelez n'entraîne pas la conservation ou la copie de votre objet.


La réponse de Matt Galloway sur ce sujet 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 renvoie un objet avec un compte de retenue de 1 mais le second renvoie un objet qui est auto-libéré?

Il semble qu'il est généralement sûr de supprimer l'avertissement si vous ignorez la valeur de retour. Je ne suis pas sûr de ce que la meilleure pratique est si vous avez vraiment besoin d'obtenir un objet retenu de performSelector - autre que "ne fais pas ça".


Ma conjecture à ce sujet est la suivante: puisque le sélecteur est inconnu du compilateur, ARC ne peut pas appliquer la gestion de la mémoire appropriée.

En fait, il y a des moments où la gestion de la mémoire est liée au nom de la méthode par une convention spécifique. Plus précisément, je pense aux constructeurs de commodité plutôt qu'aux méthodes de fabrication ; les premiers renvoient par convention un objet autoréalisé; ce dernier est un objet retenu. La convention est basée sur les noms du sélecteur, donc 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 va bien pour la gestion de la mémoire (par exemple, que vos méthodes ne retournent pas les objets qu'ils allouent).


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 l'autorisera tout au long de votre projet.


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

Récemment, j'ai observé de plus en plus de restructurations à partir du paradigme de la target / selector , en faveur de choses comme les protocoles, les blocs, etc. Cependant, il y a un remplaçant pour performSelector que j'ai utilisé plusieurs fois maintenant :

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

Ceux-ci semblent être un remplacement propre, ARC-safe, et presque identique pour performSelector sans avoir à trop parler de objc_msgSend() .

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


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 déclaration de retour.

#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]
);

If you don't need to pass any arguments an easy workaround is to use valueForKeyPath . This is even possible on a Class object.

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

You could also use a protocol here. So, create a protocol like so:

@protocol MyProtocol
-(void)doSomethingWithObject:(id)object;
@end

Dans votre classe qui doit appeler votre sélecteur, vous avez alors un @property.

@interface MyObject
    @property (strong) id<MyProtocol> source;
@end

Lorsque vous devez appeler @selector(doSomethingWithObject:)une instance de MyObject, procédez comme suit:

[self.source doSomethingWithObject:object];




automatic-ref-counting