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


1258

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 provoquerait-il une fuite? Et comment puis-je changer mon code pour ne plus recevoir cet avertissement?


3
Le nom de la variable est dynamique, cela dépend de beaucoup d'autres choses. Il y a le risque que j'appelle quelque chose qui n'existe pas, mais ce n'est pas le problème.
Eduardo Scoz

6
@matt pourquoi appeler dynamiquement une méthode sur un objet serait une mauvaise pratique? NSSelectorFromString () n'a-t-il pas pour seul objectif de prendre en charge cette pratique?
Eduardo Scoz

7
Vous devriez / pourriez également tester [_controller respondsToSelector: mySelector] avant de le configurer via performSelector:
mattacular

50
@mattacular Souhaite que je puisse voter contre: "C'est ... c'est une mauvaise pratique."
ctpenrose

6
Si vous savez que la chaîne est un littéral, utilisez simplement @selector () pour que le compilateur sache quel est le nom du sélecteur. Si votre code réel appelle NSSelectorFromString () avec une chaîne construite ou fournie au moment de l'exécution, vous devez utiliser NSSelectorFromString ().
Chris Page

Réponses:


1212

Solution

Le compilateur l'avertit pour une raison. Il est très rare que cet avertissement soit simplement ignoré et il est facile de contourner ce 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 laconiquement (bien que difficile à lire et sans le 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 la fonction C pour la méthode correspondant au contrôleur. Tous NSObjectrépondent à methodForSelector:, mais vous pouvez également utiliser class_getMethodImplementationdans le runtime Objective-C (utile si vous n'avez qu'une référence de protocole, comme id<SomeProto>). Ces pointeurs de fonction sont appelés IMPs et sont de simples typedefpointeurs de fonction ed ( id (*IMP)(id, SEL, ...)) 1 . Cela peut être proche de la signature de méthode réelle de la méthode, mais ne correspondra pas toujours exactement.

Une fois que vous avez le 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 selfet _cmdde chaque appel de méthode Objective-C). Ceci est géré dans la troisième ligne (le (void *)côté droit indique simplement au compilateur que vous savez ce que vous faites et ne pas générer d'avertissement car 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 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;

Raison de 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 pourrait être quelque chose: void, int, char, NSString *, id, etc. ARC reçoit normalement ces informations de l' en- tête du type d'objet que vous travaillez avec. 3

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

  1. Ignorer types non-objet ( void, int, etc.)
  2. Conserver la valeur de l'objet, puis la relâcher lorsqu'elle n'est plus utilisée (hypothèse standard)
  3. Libérer de nouvelles valeurs d'objet lorsqu'elles ne sont plus utilisées (méthodes dans la famille init/ copyou attribuées avec ns_returns_retained)
  4. Ne rien faire et supposer que la valeur d'objet retournée sera valide dans la portée locale (jusqu'à ce que le pool de versions 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. Ainsi, vous pourriez finir par créer une fuite si votre objet est censé être libéré comme au n ° 3 ci-dessus (c'est-à-dire que la méthode que vous appelez renvoie un nouvel objet).

Pour les sélecteurs que vous essayez d'appeler ce retour voidou d'autres non-objets, vous pouvez activer les fonctionnalités du compilateur pour 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 affectées aux variables locales. Il n'y a aucune raison qu'avec ARC activé, il ne puisse pas conserver et libérer la valeur d'objet retournée methodForSelector:même si vous ne voulez 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, someMethodretourne un objet non (y compris void), vous pourriez vous retrouver avec une valeur de pointeur de mémoire conservée / libérée et planter.

Arguments supplémentaires

Une considération est que c'est le même avertissement qui se produira performSelector:withObject:et vous pourriez rencontrer des problèmes similaires en ne déclarant pas comment cette méthode consomme les 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 à planter. Il existe des moyens de contourner ce problème avec le casting ponté, mais il serait vraiment préférable d'utiliser simplement la IMPméthodologie de pointeur de fonction ci-dessus. Étant donné que les paramètres consommés sont rarement un problème, cela ne devrait pas se produire.

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 réellement capable d'enregistrer toutes les informations sur le sélecteur et l'objet pendant la compilation. Il n'a pas besoin de faire d'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 où la suppression de cet avertissement serait nécessaire et une bonne conception de code, je suis vide. Quelqu'un s'il vous plaît partager s'ils 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 possible de créer un NSMethodInvocationpour gérer cela également, mais cela nécessite beaucoup plus de frappe et est également plus lent, il n'y a donc aucune raison de le faire.

Histoire

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

Avec l'introduction de Swift, Apple a documenté la performSelector:famille de méthodes comme "intrinsèquement dangereuses" 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:ces méthodes et ne les documente pas comme "intrinsèquement dangereuses"

L'idée d'envoyer des messages sur la base d'un sélecteur nommé n'est cependant 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, selfet _cmdqui sont implicitement ajoutés lorsque vous appelez une méthode.

2 L' appel d'une NULLfonction n'est pas sûr en C. Le gardien utilisé pour vérifier la présence du contrôleur s'assure que nous avons un objet. Nous savons donc que nous aurons un IMPde methodForSelector:(bien qu'il puisse être _objc_msgForward, l' entrée dans le système de transfert des messages). Fondamentalement, 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 idet que vous n'importez pas tous les en-têtes. Vous pourriez vous retrouver avec des plantages dans le code que le compilateur pense être correct. 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 conservées et les valeurs de retour non retenues pour plus de détails.


@wbyoung Si votre code résout le problème de rétention, je me demande pourquoi les performSelector:méthodes ne sont pas implémentées de cette façon. Ils ont une signature de méthode stricte (renvoyant id, prenant un ou deux ids), donc aucun type primitif n'a besoin d'être manipulé.
Tricertops

1
@Andy l'argument est géré en fonction de la définition du prototype de la méthode (il ne sera pas conservé / publié). La préoccupation est principalement basée sur le type de retour.
wbyoung

2
L '"exemple complexe" donne une erreur Cannot initialize a variable of type 'CGRect (*)(__strong id, SEL, CGRect, UIView *__strong)' with an rvalue of type 'void *'lors de l'utilisation du dernier Xcode. (5.1.1) Pourtant, j'ai beaucoup appris!
Stan James

2
void (*func)(id, SEL) = (void *)imp;ne compile pas, je l'ai remplacé parvoid (*func)(id, SEL) = (void (*)(id, SEL))imp;
Davyd Geyl

1
changer void (*func)(id, SEL) = (void *)imp;pour <…> = (void (*))imp;ou<…> = (void (*) (id, SEL))imp;
Isaak Osipovich Dunayevsky

1183

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

Cette méthode peut provoquer des fuites de mémoire lorsque l'optimisation est définie sur autre chose que Aucun.
Eric

4
@Eric Non, ce n'est pas possible, à moins que vous n'invoquiez des méthodes amusantes comme "initSomething" ou "newSomething" ou "somethingCopy".
Andrey Tarantsov

3
@Julian Cela fonctionne, mais cela désactive l'avertissement pour l'intégralité du fichier - vous n'en aurez peut-être pas besoin ou n'en voudrez pas. Envelopper avec les programmes popet pushest beaucoup plus propre et plus sûr.
Emil

2
Tout cela fait taire le compilateur. Cela ne résout pas le problème. Si le sélecteur n'existe pas, vous êtes à peu près foutu.
Andra Todorescu

2
Cela ne doit être utilisé que s'il est encapsulé par une if ([_target respondsToSelector:_selector]) {logique ou une logique similaire.

208

Ma conjecture à ce sujet est la suivante: puisque le sélecteur est inconnu du compilateur, ARC ne peut pas appliquer une 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é et aux méthodes de fabrication ; l'ancien restitue par convention un objet libéré automatiquement; ce dernier 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 mémoire appropriée.

Si c'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'elles allouent).


5
Merci pour la réponse, je vais approfondir la question pour voir ce qui se passe. Une idée sur la façon de contourner l'avertissement et de le faire disparaître? Je détesterais avoir l'avertissement dans mon code pour toujours pour ce qui est un appel sûr.
Eduardo Scoz

84
J'ai donc reçu la confirmation de quelqu'un chez Apple dans leurs forums que c'est effectivement le cas. Ils ajouteront un remplacement oublié pour permettre aux utilisateurs de désactiver cet avertissement dans les futures versions. Merci.
Eduardo Scoz

5
Cette réponse soulève quelques questions, comme si ARC essaye de déterminer quand publier quelque chose en fonction des conventions et des noms de méthode, alors comment est-ce le "comptage de références"? Le comportement que vous décrivez ne semble que légèrement meilleur que complètement arbitraire, si ARC suppose que le code suit une certaine convention au lieu de suivre réellement les références, quelle que soit la convention suivie.
2012

8
ARC automatise le processus d'ajout de rétentions et de versions lors de la compilation. Ce n'est pas un ramasse-miettes (c'est pourquoi il est si incroyablement rapide et si peu coûteux). Ce n'est pas du tout arbitraire. Les règles par défaut sont basées sur des conventions ObjC bien établies qui ont été appliquées de manière cohérente pendant des décennies. Cela évite d'avoir à ajouter explicitement un __attributeà chaque méthode expliquant sa gestion de la mémoire. Mais cela empêche également le complice de gérer correctement ce modèle (un modèle qui était très courant auparavant, mais qui a été remplacé par des modèles plus robustes ces dernières années).
Rob Napier

8
Donc on ne peut plus avoir un ivar de type SELet assigner différents sélecteurs selon la situation? Bravo, langage dynamique ...
Nicolas Miari

121

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

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


12
Notez que vous pouvez ajouter le même indicateur pour des fichiers spécifiques plutôt que pour l'ensemble du projet. Si vous regardez sous Build Phases-> Compile Sources, vous pouvez définir par fichier les indicateurs de compilateur (comme vous voulez le faire pour exclure des fichiers d'ARC). Dans mon projet, un seul fichier devrait utiliser des sélecteurs de cette façon, je l'ai donc simplement exclu et laissé les autres.
Michael

111

Comme solution de contournement jusqu'à ce que le compilateur permette de remplacer l'avertissement, vous pouvez utiliser le runtime

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

au lieu de

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

Tu devras

#import <objc/message.h>


8
ARC reconnaît les conventions Cocoa, puis ajoute des conservations et des versions basées sur ces conventions. Étant donné que C ne respecte pas ces conventions, ARC vous oblige à utiliser des techniques de gestion manuelle de la mémoire. Si vous créez un objet CF, vous devez le CFRelease (). Si vous dispatch_queue_create (), vous devez dispatch_release (). En bout de ligne, si vous voulez éviter les avertissements ARC, vous pouvez les éviter en utilisant des objets C et une gestion manuelle de la mémoire. En outre, vous pouvez désactiver ARC par fichier en utilisant l'indicateur de compilateur -fno-objc-arc sur ce fichier.
jluckyiv

8
Pas sans casting, vous ne pouvez pas. Varargs n'est pas la même chose qu'une liste d'arguments explicitement typée. Cela fonctionnera généralement par coïncidence, mais je ne considère pas que "par coïncidence" soit correct.
bbum

21
Ne faites pas ça, [_controller performSelector:NSSelectorFromString(@"someMethod")];et ce objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));n'est pas équivalent! Jetez un œil aux asymétries de signature de méthode et à une grande faiblesse du typage faible d'Objective-C, ils expliquent le problème en profondeur.
0xced

5
@ 0xced Dans ce cas, ça va. objc_msgSend ne créera pas de non-concordance de signature de méthode pour un sélecteur qui aurait fonctionné correctement dans performSelector: ou ses variantes, car ils ne prennent que des objets comme paramètres. Tant que tous vos paramètres sont des pointeurs (y compris les objets), doubles et NSInteger / long, et que votre type de retour est void, pointer ou long, objc_msgSend fonctionnera correctement.
Matt Gallagher

88

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

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

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


6
Je suppose que vous pouvez également réactiver l'avertissement immédiatement après la méthode en question avec #pragma clang diagnostic warning "-Warc-performSelector-leaks". Je sais que si je désactive un avertissement, j'aime le réactiver le plus tôt possible, donc je ne laisse pas accidentellement passer un autre avertissement imprévu. Il est peu probable que ce soit un problème, mais c'est juste ma pratique chaque fois que je désactive un avertissement.
Rob

2
Vous pouvez également restaurer votre état de configuration de compilateur précédent en utilisant #pragma clang diagnostic warning pushavant d'effectuer des modifications et #pragma clang diagnostic warning poppour restaurer l'état précédent. Utile si vous désactivez des charges et que vous ne voulez pas avoir beaucoup de lignes de pragma réactivées dans votre code.
deanWombourne

Il ignorera seulement la ligne suivante?
hfossli

70

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

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

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


2
Savez-vous si cela résout réellement les problèmes de gestion de la mémoire associés, ou a-t-il les mêmes problèmes mais Xcode n'est pas assez intelligent pour vous avertir avec ce code?
Aaron Brager du

Ce n'est pas sémantiquement la même chose! L'utilisation de performSelector: withObject: AfterDelay: exécutera le sélecteur lors de la prochaine exécution de la boucle d'exécution. Par conséquent, cette méthode renvoie immédiatement.
Florian

10
@Florian Bien sûr, ce n'est pas pareil! Lisez ma réponse: je dis si cela est acceptable, car le résultat est nul et les cycles runloop. C'est la première phrase de ma réponse.
mat

34

Voici une macro mise à jour basée sur la réponse donnée ci-dessus. Celui-ci devrait vous permettre d'encapsuler 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]
);

6
returnn'a pas besoin d'être à l'intérieur de la macro; return SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING([_target performSelector:_action withObject:self]);fonctionne également et semble plus sain.
uasi

31

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

NSInvocationpermet de définir plusieurs arguments, contrairement à performSelectorce qui fonctionnera sur n'importe quelle méthode.


3
Savez-vous si cela résout réellement les problèmes de gestion de la mémoire associés, ou a-t-il les mêmes problèmes mais Xcode n'est pas assez intelligent pour vous avertir avec ce code?
Aaron Brager

1
On pourrait dire que cela résout les problèmes de gestion de la mémoire; mais c'est parce qu'il vous permet essentiellement de spécifier le comportement. Par exemple, vous pouvez choisir de laisser l'invocation conserver ou non les arguments. À ma connaissance actuelle, il tente de résoudre les problèmes de non-concordance de signature qui peuvent apparaître en faisant confiance à ce que vous savez et ne lui fournissez pas de données incorrectes. Je ne sais pas si toutes les vérifications peuvent être effectuées au moment de l'exécution. Comme mentionné dans un autre commentaire, mikeash.com/pyblog/… explique bien ce que les décalages peuvent faire.
Mihai Timar

20

Eh bien, beaucoup de réponses ici, mais comme c'est un peu différent, en combinant quelques réponses, je pensais que je les mettrais. J'utilise une catégorie NSObject qui vérifie que le sélecteur retourne vide, 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

Faut-il remplacer «v» par _C_VOID? _C_VOID est déclaré dans <objc / runtime.h>.
Rik Renich

16

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

Récemment , j'ai été voir de plus en plus loin de la restructuration du target/ selectorparadigme, en faveur des choses telles que des protocoles, des blocs, etc. Cependant, il y a une solution de remplacement pour performSelectorque je l' ai utilisé quelques fois:

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

Ceux-ci semblent être un remplacement propre, sans danger pour l'ARC et presque identique performSelectorsans avoir trop à faire avecobjc_msgSend() .

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


6
Merci pour inclure ce .. Il est disponible dans iOS: [[UIApplication sharedApplication] sendAction: to: from: forEvent:]. Je l'ai étudié une fois, mais c'est un peu gênant d'utiliser une classe liée à l'interface utilisateur au milieu de votre domaine ou service juste pour faire un appel dynamique .. Merci d'avoir inclus cela cependant!
Eduardo Scoz

2
Ew! Il aura plus de surcharge (car il doit vérifier si la méthode est disponible et remonter la chaîne du répondeur si elle ne l'est pas) et avoir un comportement d'erreur différent (remonter la chaîne du répondeur et retourner NON s'il ne trouve rien qui répond à la méthode, au lieu de simplement planter). Cela ne fonctionne pas non plus quand vous voulez le idfrom-performSelector:...
tc.

2
@tc. Il ne "remonte la chaîne des répondeurs" que s'il to:est nul, ce qui n'est pas le cas. Il va directement à l'objet ciblé sans vérification préalable. Il n'y a donc pas "plus de frais généraux". Ce n'est pas une excellente solution, mais la raison que vous donnez n'est pas la raison. :)
mat

15

La réponse de Matt Galloway sur ce sujet explique 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 de 1 mais que le second retourne un objet qui est libéré automatiquement?

Il semble qu'il soit généralement sûr 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 - autre que "ne faites pas ça".


14

@ 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 un objet conservé.

Si vous exécutez l'outil Memory Leak Tool, vous pouvez voir l'image suivante: entrez la description de l'image ici ... et il n'y a aucune fuite de mémoire dans les autres cas: entrez la description de l'image ici


6

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

FWIW, je n'ai pas ajouté la macro. Quelqu'un a ajouté cela à ma réponse. Personnellement, je n'utiliserais pas la macro. Le pragma est là pour contourner un cas spécial dans le code et les pragmas sont très explicites et directs sur ce qui se passe. Je préfère les garder en place plutôt que de les cacher ou de les résumer derrière une macro, mais c'est juste moi. YMMV.
Scott Thompson

@ScottThompson C'est juste. Pour moi, il est facile de rechercher cette macro dans ma base de code et j'ajoute généralement un avertissement non silencieux pour traiter le problème sous-jacent.
Ben Flynn du

6

Ne supprimez 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 casser.

Itinéraires sûrs:

Toutes ces solutions fonctionneront, avec un certain degré de variation par rapport à votre intention initiale. Supposons que cela parampuisse être nilsi vous le souhaitez:

Route sûre, 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ûr, comportement légèrement différent:

(Voir cette 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 mise en sourdine du compilateur, qui est susceptible de se casser. Notez qu'à l'heure actuelle, il s'est cassé dans Swift .

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

3
La formulation est très fausse. Les itinéraires sûrs ne sont pas plus sûrs que dangereux du tout. Il est sans doute plus dangereux car il cache implicitement l'avertissement.
Bryan Chen

Je vais corriger la formulation pour qu'elle ne soit pas insultante, mais je m'en tiens à ma parole. La seule fois où je trouve l'avertissement de silence acceptable, c'est si je ne possède pas le code. Aucun ingénieur ne peut maintenir en toute sécurité le code silencieux sans en comprendre toutes les conséquences, ce qui signifierait lire cet argument, et cette pratique est tout à fait risquée; surtout si vous considérez les 12 alternatives, en anglais simple et robuste.
SwiftArchitect

1
Non, vous n'avez pas compris mon point. L'utilisation performSelectorOnMainThreadn'est pas un bon moyen de faire taire l'avertissement et cela a des effets secondaires. (cela ne résout pas la fuite de mémoire) Le supplément #clang diagnostic ignored supprime explicitement l'avertissement de manière très claire.
Bryan Chen

Il est vrai que l'exécution d'un sélecteur sur une - (void)méthode non est le vrai problème.
SwiftArchitect

et comment appeler un sélecteur avec plusieurs arguments à travers cela et être en sécurité en même temps? @SwiftArchitect
Catalin

4

Étant donné que 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 souvenir du sélecteur pour vous exécuter, vous preniez un bloc, ARC serait en mesure de mieux suivre ce qui se passe réellement et vous n'auriez pas à courir le risque d'introduire accidentellement une fuite de mémoire.


En fait, les blocs facilitent la création accidentelle d'un cycle de rétention que l'ARC ne résout pas. Je souhaite toujours qu'il y ait un avertissement du compilateur lorsque vous l'utilisez implicitement selfvia un ivar (par exemple ivarau lieu de self->ivar).
tc.

Tu veux dire comme -Wimplicit-retenir-soi?
OrangeDog

2

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;

J'utiliserai 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];
    }

1

Si vous n'avez pas besoin de passer d'arguments, une solution de contournement simple est à utiliser valueForKeyPath. C'est même possible sur un Classobjet.

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

-2

Vous pouvez également utiliser un protocole ici. Alors, créez un protocole comme ceci:

@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:)dans une instance de MyObject, procédez comme suit:

[self.source doSomethingWithObject:object];

2
Hé Wu, merci, mais l'intérêt d'utiliser NSSelectorFromString est lorsque vous ne savez pas quel sélecteur vous souhaitez appeler pendant l'exécution.
Eduardo Scoz
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.