performSelector kann ein Leck verursachen, da sein Selektor unbekannt ist

1258

Ich erhalte die folgende Warnung vom ARC-Compiler:

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

Folgendes mache ich:

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

Warum bekomme ich diese Warnung? Ich verstehe, dass der Compiler nicht überprüfen kann, ob der Selektor vorhanden ist oder nicht, aber warum würde das ein Leck verursachen? Und wie kann ich meinen Code ändern, damit ich diese Warnung nicht mehr erhalte?

Eduardo Scoz
quelle
3
Der Name der Variablen ist dynamisch und hängt von vielen anderen Dingen ab. Es besteht das Risiko, dass ich etwas nenne, das nicht existiert, aber das ist nicht das Problem.
Eduardo Scoz
6
@matt warum sollte es eine schlechte Praxis sein, eine Methode dynamisch für ein Objekt aufzurufen? Ist es nicht der ganze Zweck von NSSelectorFromString (), diese Praxis zu unterstützen?
Eduardo Scoz
7
Sie sollten / könnten auch [_controller responsondsToSelector: mySelector] testen, bevor Sie es über performSelector einstellen:
mattacular
50
@mattacular Ich wünschte, ich könnte abstimmen: "Das ... ist eine schlechte Praxis."
Ctpenrose
6
Wenn Sie wissen, dass die Zeichenfolge ein Literal ist, verwenden Sie einfach @selector (), damit der Compiler den Selektornamen erkennen kann. Wenn Ihr tatsächlicher Code NSSelectorFromString () mit einer Zeichenfolge aufruft, die zur Laufzeit erstellt oder bereitgestellt wird, müssen Sie NSSelectorFromString () verwenden.
Chris Seite

Antworten:

1211

Lösung

Der Compiler warnt aus einem bestimmten Grund davor. Es ist sehr selten, dass diese Warnung einfach ignoriert wird, und es ist einfach, sie zu umgehen. Hier ist wie:

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

Oder knapper (obwohl schwer zu lesen und ohne Wache):

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

Erläuterung

Hier wird der Controller nach dem C-Funktionszeiger für die dem Controller entsprechende Methode gefragt. Alle NSObjectantworten darauf methodForSelector:, aber Sie können es auch class_getMethodImplementationin der Objective-C-Laufzeit verwenden (nützlich, wenn Sie nur eine Protokollreferenz haben, wie z id<SomeProto>. B. ). Diese Funktionszeiger heißen IMPs und sind einfache typedefed Funktionszeiger ( id (*IMP)(id, SEL, ...)) 1 . Dies kann nahe an der tatsächlichen Methodensignatur der Methode liegen, stimmt jedoch nicht immer genau überein.

Sobald Sie das haben IMP, müssen Sie es in einen Funktionszeiger umwandeln, der alle Details enthält, die ARC benötigt (einschließlich der beiden impliziten versteckten Argumente selfund _cmdjedes Objective-C-Methodenaufrufs). Dies wird in der dritten Zeile behandelt (die (void *)rechte Seite teilt dem Compiler einfach mit, dass Sie wissen, was Sie tun, und keine Warnung zu generieren, da die Zeigertypen nicht übereinstimmen).

Schließlich rufen Sie den Funktionszeiger 2 auf .

Komplexes Beispiel

Wenn der Selektor Argumente akzeptiert oder einen Wert zurückgibt, müssen Sie die Dinge ein wenig ändern:

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;

Begründung für die Warnung

Der Grund für diese Warnung ist, dass die Laufzeit bei ARC wissen muss, was mit dem Ergebnis der von Ihnen aufgerufenen Methode zu tun ist. Das Ergebnis könnte sein , alles: void, int, char, NSString *, id, usw. ARC wird normalerweise diese Informationen aus dem Header des Objekttypen mit dem Sie arbeiten. 3

Es gibt wirklich nur 4 Dinge, die ARC für den Rückgabewert berücksichtigen würde: 4

  1. Ignorieren nicht-Objekttypen ( void, int, usw.)
  2. Objektwert beibehalten und freigeben, wenn er nicht mehr verwendet wird (Standardannahme)
  3. Geben Sie neue Objektwerte frei, wenn sie nicht mehr verwendet werden (Methoden in der init/ copy-Familie oder zugeordnet mit ns_returns_retained)
  4. Nichts tun und davon ausgehen, dass der zurückgegebene Objektwert im lokalen Bereich gültig ist (bis der innerste Release-Pool geleert ist, zugeordnet mit ns_returns_autoreleased)

Der Aufruf von methodForSelector:setzt voraus, dass der Rückgabewert der aufgerufenen Methode ein Objekt ist, behält es jedoch nicht bei. Sie könnten also ein Leck erzeugen, wenn Ihr Objekt wie in # 3 oben freigegeben werden soll (dh die Methode, die Sie aufrufen, gibt ein neues Objekt zurück).

Für Selektoren, die Sie versuchen, diese Rückgabe voidoder andere Nicht-Objekte aufzurufen , können Sie Compiler-Funktionen aktivieren, um die Warnung zu ignorieren. Dies kann jedoch gefährlich sein. Ich habe gesehen, wie Clang einige Iterationen durchlaufen hat, wie Rückgabewerte behandelt werden, die lokalen Variablen nicht zugewiesen sind. Es gibt keinen Grund dafür, dass bei aktiviertem ARC der zurückgegebene Objektwert nicht beibehalten und freigegeben werden kann, methodForSelector:obwohl Sie ihn nicht verwenden möchten. Aus Sicht des Compilers ist es schließlich ein Objekt. Das heißt, wenn die von Ihnen aufgerufene Methode someMethodein Nicht-Objekt (einschließlich void) zurückgibt, kann dies dazu führen, dass ein Garbage-Pointer-Wert beibehalten / freigegeben wird und abstürzt.

Zusätzliche Argumente

Eine Überlegung ist, dass dies dieselbe Warnung ist, bei der performSelector:withObject:Sie auf ähnliche Probleme stoßen können, wenn Sie nicht angeben, wie diese Methode Parameter verwendet. ARC ermöglicht das Deklarieren verbrauchter Parameter . Wenn die Methode den Parameter verbraucht, senden Sie wahrscheinlich eine Nachricht an einen Zombie und stürzen ab. Es gibt Möglichkeiten, dies mit Bridged Casting zu umgehen, aber es ist wirklich besser, einfach die IMPoben beschriebene Methode und den Funktionszeiger zu verwenden. Da verbrauchte Parameter selten ein Problem darstellen, ist es unwahrscheinlich, dass dies auftritt.

Statische Selektoren

Interessanterweise beschwert sich der Compiler nicht über statisch deklarierte Selektoren:

[_controller performSelector:@selector(someMethod)];

Der Grund dafür ist, dass der Compiler während der Kompilierung tatsächlich alle Informationen über den Selektor und das Objekt aufzeichnen kann. Es müssen keine Annahmen über irgendetwas gemacht werden. (Ich habe dies vor einem Jahr überprüft, indem ich mir die Quelle angesehen habe, habe aber momentan keine Referenz.)

Unterdrückung

Bei dem Versuch, an eine Situation zu denken, in der die Unterdrückung dieser Warnung und ein gutes Code-Design erforderlich wären, werde ich leer. Bitte teilen Sie jemandem mit, ob er die Erfahrung gemacht hat, diese Warnung zum Schweigen zu bringen (und die oben genannten Dinge nicht richtig handhaben).

Mehr

Es ist möglich, ein zu erstellen NSMethodInvocation, um dies auch zu handhaben, aber dies erfordert viel mehr Eingabe und ist auch langsamer, so dass es wenig Grund gibt, dies zu tun.

Geschichte

Als die performSelector:Methodenfamilie zum ersten Mal zu Objective-C hinzugefügt wurde, existierte ARC nicht. Während der Erstellung von ARC entschied Apple, dass für diese Methoden eine Warnung generiert werden sollte, um Entwickler dazu zu führen, andere Mittel zu verwenden, um explizit zu definieren, wie Speicher beim Senden beliebiger Nachrichten über einen benannten Selektor behandelt werden soll. In Objective-C können Entwickler dies mithilfe von Casts im C-Stil für Rohfunktionszeiger tun.

Mit der Einführung von Swift hat Apple die Methodenfamilie als "von Natur aus unsicher" dokumentiertperformSelector: und steht Swift nicht zur Verfügung.

Im Laufe der Zeit haben wir diesen Fortschritt gesehen:

  1. Frühere Versionen von Objective-C ermöglichen performSelector:(manuelle Speicherverwaltung)
  2. Objective-C mit ARC warnt vor der Verwendung von performSelector:
  3. Swift hat keinen Zugriff auf performSelector:diese Methoden und dokumentiert sie als "von Natur aus unsicher".

Die Idee, Nachrichten basierend auf einem benannten Selektor zu senden, ist jedoch keine "inhärent unsichere" Funktion. Diese Idee wird seit langem erfolgreich in Objective-C sowie in vielen anderen Programmiersprachen eingesetzt.


1 Alle Objective-C - Methoden haben zwei versteckte Argumente, selfund _cmddas sind implizit hinzugefügt , wenn Sie eine Methode aufrufen.

2 Das Aufrufen einer NULLFunktion ist in C nicht sicher. Der Schutz, mit dem das Vorhandensein des Controllers überprüft wird, stellt sicher, dass wir ein Objekt haben. Wir wissen daher, dass wir eine IMPvon erhalten werden methodForSelector:(obwohl dies der Fall sein kann _objc_msgForward, Eintrag in das Nachrichtenweiterleitungssystem). Grundsätzlich wissen wir, dass wir mit der Wache eine Funktion haben, die wir aufrufen können.

3 Tatsächlich kann es zu falschen Informationen kommen, wenn Sie Objekte als deklarieren idund nicht alle Header importieren. Es kann zu Abstürzen im Code kommen, die der Compiler für in Ordnung hält. Dies ist sehr selten, könnte aber passieren. Normalerweise erhalten Sie nur eine Warnung, dass nicht bekannt ist, aus welcher der beiden Methodensignaturen Sie auswählen können.

4 Weitere Informationen finden Sie in der ARC-Referenz zu beibehaltenen Rückgabewerten und nicht beibehaltenen Rückgabewerten .

wbyoung
quelle
@wbyoung Wenn Ihr Code das Problem der Beibehaltung löst, frage ich mich, warum performSelector:Methoden nicht auf diese Weise implementiert werden. Sie haben eine strikte Methodensignatur (Rückgabe id, ein oder zwei idSekunden), sodass keine primitiven Typen behandelt werden müssen.
Tricertops
1
@Andy das Argument wird basierend auf der Definition des Prototyps der Methode behandelt (es wird nicht beibehalten / freigegeben). Das Problem basiert hauptsächlich auf dem Rückgabetyp.
wbyoung
2
Das "Komplexe Beispiel" gibt einen Fehler Cannot initialize a variable of type 'CGRect (*)(__strong id, SEL, CGRect, UIView *__strong)' with an rvalue of type 'void *'bei der Verwendung des neuesten Xcodes aus. (5.1.1) Trotzdem habe ich viel gelernt!
Stan James
2
void (*func)(id, SEL) = (void *)imp;nicht kompiliert, ich habe es ersetzt durchvoid (*func)(id, SEL) = (void (*)(id, SEL))imp;
Davyd Geyl
1
Wechsel void (*func)(id, SEL) = (void *)imp;zu <…> = (void (*))imp;oder<…> = (void (*) (id, SEL))imp;
Isaak Osipovich Dunayevsky
1182

Im LLVM 3.0-Compiler in Xcode 4.2 können Sie die Warnung wie folgt unterdrücken:

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

Wenn Sie den Fehler an mehreren Stellen erhalten und das C-Makrosystem zum Ausblenden der Pragmas verwenden möchten, können Sie ein Makro definieren, um das Unterdrücken der Warnung zu vereinfachen:

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

Sie können das Makro folgendermaßen verwenden:

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

Wenn Sie das Ergebnis der ausgeführten Nachricht benötigen, können Sie Folgendes tun:

id result;
SuppressPerformSelectorLeakWarning(
    result = [_target performSelector:_action withObject:self]
);
Scott Thompson
quelle
Diese Methode kann zu Speicherverlusten führen, wenn die Optimierung auf einen anderen Wert als "Keine" eingestellt ist.
Eric
4
@Eric Nein, es kann nicht, es sei denn, Sie rufen lustige Methoden wie "initSomething" oder "newSomething" oder "SomethingCopy" auf.
Andrey Tarantsov
3
@Julian Das funktioniert, aber das deaktiviert die Warnung für die gesamte Datei - das brauchen oder wollen Sie vielleicht nicht. Das Umwickeln mit den popund push-Pragmas ist viel sauberer und sicherer.
Emil
2
Das alles bringt den Compiler zum Schweigen. Dies löst das Problem nicht. Wenn der Selektor nicht existiert, sind Sie ziemlich durchgeknallt.
Andra Todorescu
2
Dies sollte nur verwendet werden, wenn eine if ([_target respondsToSelector:_selector]) {oder eine ähnliche Logik verwendet wird.
208

Meine Vermutung dazu lautet: Da der Selektor dem Compiler unbekannt ist, kann ARC keine ordnungsgemäße Speicherverwaltung erzwingen.

In der Tat gibt es Zeiten, in denen die Speicherverwaltung durch eine bestimmte Konvention an den Namen der Methode gebunden ist. Insbesondere denke ich an Convenience-Konstruktoren im Vergleich zu Make- Methoden. die frühere Rückgabe per Konvention ein automatisch freigegebenes Objekt; Letzteres ist ein zurückbehaltenes Objekt. Die Konvention basiert auf den Namen des Selektors. Wenn der Compiler den Selektor nicht kennt, kann er die ordnungsgemäße Speicherverwaltungsregel nicht erzwingen.

Wenn dies korrekt ist, können Sie Ihren Code sicher verwenden, vorausgesetzt, Sie stellen sicher, dass die Speicherverwaltung in Ordnung ist (z. B. dass Ihre Methoden keine von ihnen zugewiesenen Objekte zurückgeben).

Sergio
quelle
5
Vielen Dank für die Antwort, ich werde mehr darüber nachdenken, um zu sehen, was los ist. Irgendeine Idee, wie ich die Warnung umgehen und verschwinden lassen kann? Ich würde es hassen, die Warnung für immer in meinem Code zu haben, um einen sicheren Anruf zu erhalten.
Eduardo Scoz
84
Also habe ich von jemandem bei Apple in den Foren die Bestätigung erhalten, dass dies tatsächlich der Fall ist. Sie werden eine vergessene Überschreibung hinzufügen, damit Benutzer diese Warnung in zukünftigen Versionen deaktivieren können. Vielen Dank.
Eduardo Scoz
5
Diese Antwort wirft einige Fragen auf, z. B. wenn ARC versucht, anhand von Konventions- und Methodennamen zu bestimmen, wann etwas veröffentlicht werden soll, wie ist es dann "Referenzzählung"? Das von Ihnen beschriebene Verhalten klingt nur unwesentlich besser als völlig willkürlich, wenn ARC davon ausgeht, dass der Code einer bestimmten Konvention folgt, anstatt die Referenzen tatsächlich zu verfolgen, unabhängig davon, welche Konvention befolgt wird.
aroth
8
ARC automatisiert das Hinzufügen von Aufbewahrungen und Releases beim Kompilieren. Es ist keine Speicherbereinigung (weshalb es so unglaublich schnell und mit geringem Overhead ist). Es ist überhaupt nicht willkürlich. Die Standardregeln basieren auf etablierten ObjC-Konventionen, die seit Jahrzehnten konsequent angewendet werden. Dadurch wird vermieden, dass __attributejeder Methode explizit eine hinzugefügt werden muss, um die Speicherverwaltung zu erläutern. Es macht es dem Komplizen jedoch auch unmöglich, mit diesem Muster richtig umzugehen (ein Muster, das früher sehr verbreitet war, in den letzten Jahren jedoch durch robustere Muster ersetzt wurde).
Rob Napier
8
Wir können also keinen Ivar vom Typ mehr haben SELund je nach Situation unterschiedliche Selektoren zuweisen?
Gut gemacht
121

Fügen Sie in Ihrem Projekt Build Settings unter Other Warning Flags ( WARNING_CFLAGS) hinzu
-Wno-arc-performSelector-leaks

Stellen Sie jetzt sicher, dass der von Ihnen aufgerufene Selektor nicht dazu führt, dass Ihr Objekt beibehalten oder kopiert wird.

0xced
quelle
12
Beachten Sie, dass Sie dasselbe Flag für bestimmte Dateien und nicht für das gesamte Projekt hinzufügen können. Wenn Sie unter Build Phases-> Compile Sources nachsehen, können Sie Compiler-Flags pro Datei festlegen (genau wie Sie dies tun möchten, um Dateien von ARC auszuschließen). In meinem Projekt sollte nur eine Datei Selektoren auf diese Weise verwenden, also habe ich sie einfach ausgeschlossen und die anderen verlassen.
Michael
111

Als Problemumgehung, bis der Compiler das Überschreiben der Warnung zulässt, können Sie die Laufzeit verwenden

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

anstatt

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

Du musst

#import <objc/message.h>

jluckyiv
quelle
8
ARC erkennt Kakao-Konventionen und fügt dann auf der Grundlage dieser Konventionen Aufbewahrungen und Freigaben hinzu. Da C diesen Konventionen nicht entspricht, werden Sie von ARC gezwungen, manuelle Speicherverwaltungstechniken zu verwenden. Wenn Sie ein CF-Objekt erstellen, müssen Sie es CFRelease (). Wenn Sie dispatch_queue_create () verwenden, müssen Sie dispatch_release (). Fazit: Wenn Sie die ARC-Warnungen vermeiden möchten, können Sie sie mithilfe von C-Objekten und manueller Speicherverwaltung vermeiden. Sie können ARC auch für jede Datei deaktivieren, indem Sie das Compiler-Flag -fno-objc-arc für diese Datei verwenden.
jluckyiv
8
Nicht ohne Casting kannst du nicht. Varargs ist nicht dasselbe wie eine explizit typisierte Argumentliste. Es wird im Allgemeinen durch Zufall funktionieren, aber ich halte "durch Zufall" nicht für richtig.
bbum
21
Tun Sie das nicht [_controller performSelector:NSSelectorFromString(@"someMethod")];und objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));sind nicht gleichwertig! Werfen Sie einen Blick auf Method Signature Mismatches und eine große Schwäche in der schwachen Typisierung von Objective-C, die das Problem ausführlich erklärt.
0xced
5
@ 0xced In diesem Fall ist es in Ordnung. objc_msgSend erstellt keine Nichtübereinstimmung der Methodensignatur für einen Selektor, der in performSelector: oder seinen Varianten ordnungsgemäß funktioniert hätte, da nur Objekte als Parameter verwendet werden. Solange alle Ihre Parameter Zeiger (inkl. Objekte), Doubles und NSInteger / long sind und Ihr Rückgabetyp void, pointer oder long ist, funktioniert objc_msgSend korrekt.
Matt Gallagher
88

Fügen Sie ein #pragma wie folgt hinzu, um den Fehler nur in der Datei mit dem Perform-Selektor zu ignorieren:

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

Dies würde die Warnung in dieser Zeile ignorieren, sie jedoch für den Rest Ihres Projekts zulassen.

Barlow Tucker
quelle
6
Ich verstehe, dass Sie die Warnung auch sofort nach der fraglichen Methode mit wieder einschalten können #pragma clang diagnostic warning "-Warc-performSelector-leaks". Ich weiß, wenn ich eine Warnung ausschalte, schalte ich sie gerne zum frühestmöglichen Zeitpunkt wieder ein, damit ich nicht versehentlich eine weitere unerwartete Warnung vorbeiziehen lasse. Es ist unwahrscheinlich, dass dies ein Problem ist, aber es ist nur meine Praxis, wenn ich eine Warnung ausschalte.
Rob
2
Sie können auch Ihren vorherigen Compilerkonfigurationsstatus wiederherstellen, indem Sie verwenden, #pragma clang diagnostic warning pushbevor Sie Änderungen vornehmen, und #pragma clang diagnostic warning popden vorherigen Status wiederherstellen. Nützlich, wenn Sie Lasten ausschalten und nicht viele Pragma-Zeilen in Ihrem Code wieder aktivieren möchten.
DeanWombourne
Es wird nur die folgende Zeile ignoriert?
Hfossli
70

Seltsam, aber wahr: Wenn akzeptabel (dh das Ergebnis ist ungültig und es macht Ihnen nichts aus, den Runloop-Zyklus einmal zuzulassen), fügen Sie eine Verzögerung hinzu, auch wenn diese Null ist:

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

Dadurch wird die Warnung entfernt, vermutlich weil dadurch der Compiler beruhigt wird, dass kein Objekt zurückgegeben und irgendwie schlecht verwaltet werden kann.

matt
quelle
2
Wissen Sie, ob dies tatsächlich die damit verbundenen Speicherverwaltungsprobleme behebt oder ob es dieselben Probleme gibt, aber Xcode nicht intelligent genug ist, um Sie mit diesem Code zu warnen?
Aaron Brager
Dies ist semantisch nicht dasselbe! Mit performSelector: withObject: AfterDelay: wird der Selektor im nächsten Lauf des Runloops ausgeführt. Daher wird diese Methode sofort zurückgegeben.
Florian
10
@Florian Natürlich ist es nicht dasselbe! Lesen Sie meine Antwort: Ich sage, wenn akzeptabel, weil das Ergebnis ungültig ist und die Runloop-Zyklen. Das ist der erste Satz meiner Antwort.
Matt
34

Hier ist ein aktualisiertes Makro basierend auf der oben angegebenen Antwort. Dieser sollte es Ihnen ermöglichen, Ihren Code auch mit einer return-Anweisung zu verpacken.

#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]
);
syvex
quelle
6
returnmuss nicht im Makro sein; return SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING([_target performSelector:_action withObject:self]);funktioniert auch und sieht vernünftiger aus.
Uasi
31

Dieser Code beinhaltet keine Compiler-Flags oder direkten Laufzeitaufrufe:

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

NSInvocationErmöglicht das Festlegen mehrerer Argumente, sodass performSelectordies im Gegensatz dazu bei jeder Methode funktioniert.

Benedict Cohen
quelle
3
Wissen Sie, ob dies tatsächlich die damit verbundenen Speicherverwaltungsprobleme behebt oder ob es dieselben Probleme gibt, aber Xcode nicht intelligent genug ist, um Sie mit diesem Code zu warnen?
Aaron Brager
1
Man könnte sagen, es löst die Speicherverwaltungsprobleme. Dies liegt jedoch daran, dass Sie damit grundsätzlich das Verhalten festlegen können. Sie können beispielsweise festlegen, ob der Aufruf die Argumente beibehalten soll oder nicht. Nach meinem derzeitigen Kenntnisstand wird versucht, die möglicherweise auftretenden Probleme mit der Signaturinkongruenz zu beheben, indem Sie darauf vertrauen, dass Sie wissen, was Sie tun, und keine falschen Daten bereitstellen. Ich bin nicht sicher, ob alle Überprüfungen zur Laufzeit durchgeführt werden können. Wie in einem anderen Kommentar erwähnt, erklärt mikeash.com/pyblog/… gut, was Fehlanpassungen bewirken können.
Mihai Timar
20

Nun, viele Antworten hier, aber da dies etwas anders ist, habe ich ein paar Antworten kombiniert, von denen ich dachte, ich würde sie einfügen. Ich verwende eine NSObject-Kategorie, die überprüft, ob der Selektor ungültig ist, und auch den Compiler unterdrückt Warnung.

#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
Chris Prince
quelle
Sollte 'v' durch _C_VOID ersetzt werden? _C_VOID wird in <objc / runtime.h> deklariert.
Rik Renich
16

Um der Nachwelt willen habe ich beschlossen, meinen Hut in den Ring zu werfen :)

In letzter Zeit habe ich immer mehr Umstrukturierungen außerhalb des target/ selector-Paradigmas zugunsten von Dingen wie Protokollen, Blöcken usw. gesehen. Es gibt jedoch einen Drop-In-Ersatz dafür performSelector, den ich jetzt einige Male verwendet habe:

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

Dies scheint ein sauberer, ARC-sicherer und nahezu identischer Ersatz zu sein, performSelectorohne zu viel damit zu tun zu haben objc_msgSend().

Ich habe jedoch keine Ahnung, ob unter iOS ein Analog verfügbar ist.

Patrick Perini
quelle
6
Vielen Dank, dass Sie dies aufgenommen haben. Es ist in iOS verfügbar : [[UIApplication sharedApplication] sendAction: to: from: forEvent:]. Ich habe es mir einmal angesehen, aber es ist irgendwie unangenehm, eine UI-bezogene Klasse in der Mitte Ihrer Domain oder Ihres Dienstes zu verwenden, um nur einen dynamischen Anruf zu tätigen. Vielen Dank, dass Sie dies aufgenommen haben!
Eduardo Scoz
2
Ew! Es hat mehr Overhead (da es prüfen muss, ob die Methode verfügbar ist, und die Antwortkette hochgefahren, wenn dies nicht der Fall ist) und ein anderes Fehlerverhalten (die Antwortkette hochlaufen und NEIN zurückgeben, wenn es nichts finden kann) die auf die Methode reagiert, anstatt einfach abzustürzen). Es funktioniert auch nicht, wenn Sie das idfrom-performSelector:...
tc wollen.
2
@tc. Es "läuft nicht die Antwortkette hinauf", es to:sei denn, es ist Null, was es nicht ist. Es geht einfach direkt zum Zielobjekt ohne vorherige Überprüfung. Es gibt also keinen "Overhead". Es ist keine großartige Lösung, aber der Grund, den Sie angeben, ist nicht der Grund. :)
Matt
15

Die Antwort von Matt Galloway auf diesen Thread erklärt das Warum:

Folgendes berücksichtigen:

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

Wie kann ARC nun wissen, dass der erste ein Objekt mit einer Beibehaltungszahl von 1 zurückgibt, der zweite jedoch ein Objekt zurückgibt, das automatisch freigegeben wurde?

Es scheint im Allgemeinen sicher zu sein, die Warnung zu unterdrücken, wenn Sie den Rückgabewert ignorieren. Ich bin mir nicht sicher, was die beste Vorgehensweise ist, wenn Sie wirklich ein beibehaltenes Objekt von performSelector erhalten müssen - außer "Tun Sie das nicht".

c roald
quelle
14

@ c-Straße bietet die richtige Verbindung mit Problembeschreibung hier . Unten sehen Sie mein Beispiel, wenn performSelector einen Speicherverlust verursacht.

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

Die einzige Methode, die in meinem Beispiel einen Speicherverlust verursacht, ist CopyDummyWithLeak. Der Grund ist, dass ARC nicht weiß, dass copySelector ein beibehaltenes Objekt zurückgibt.

Wenn Sie das Memory Leak Tool ausführen, sehen Sie das folgende Bild: Geben Sie hier die Bildbeschreibung ein ... und in keinem anderen Fall treten Speicherlecks auf: Geben Sie hier die Bildbeschreibung ein

Pavel Osipov
quelle
6

Um das Makro von Scott Thompson allgemeiner zu gestalten:

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

Dann benutze es so:

MYSilenceWarning(-Warc-performSelector-leaks,
[_target performSelector:_action withObject:self];
                )
Ben Flynn
quelle
FWIW, ich habe das Makro nicht hinzugefügt. Jemand hat das zu meiner Antwort hinzugefügt. Persönlich würde ich das Makro nicht verwenden. Das Pragma dient dazu, einen speziellen Fall im Code zu umgehen, und die Pragmas sind sehr explizit und direkt in Bezug auf das, was vor sich geht. Ich ziehe es vor, sie an Ort und Stelle zu halten, anstatt sie zu verstecken oder hinter einem Makro zu abstrahieren, aber das bin nur ich. YMMV.
Scott Thompson
@ ScottThompson Das ist fair. Für mich ist es einfach, in meiner Codebasis nach diesem Makro zu suchen, und ich füge im Allgemeinen auch eine nicht stummgeschaltete Warnung hinzu, um das zugrunde liegende Problem zu beheben.
Ben Flynn
6

Warnungen nicht unterdrücken!

Es gibt nicht weniger als 12 alternative Lösungen zum Basteln mit dem Compiler.
Während Sie zum Zeitpunkt der ersten Implementierung klug sind, können nur wenige Ingenieure auf der Erde in Ihre Fußstapfen treten, und dieser Code wird irgendwann brechen.

Sichere Routen:

Alle diese Lösungen funktionieren mit einer gewissen Abweichung von Ihrer ursprünglichen Absicht. Angenommen, das paramkann sein, nilwenn Sie dies wünschen:

Sicherer Weg, gleiches konzeptionelles Verhalten:

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

Sicherer Weg, etwas anderes Verhalten:

(Siehe diese Antwort.)
Verwenden Sie anstelle von einen beliebigen Thread [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]];

Gefährliche Wege

Erfordert eine Art Compiler-Stummschaltung, die zwangsläufig unterbrochen wird. Beachten Sie, dass zum gegenwärtigen Zeitpunkt, es tat Pause in Swift .

// AT YOUR OWN RISK
[_controller performSelector:selector];
[_controller performSelector:selector withObject:anArgument];
[_controller performSelector:selector withObject:anArgument withObject:nil];
SwiftArchitect
quelle
3
Der Wortlaut ist sehr falsch. Die sicheren Wege sind nicht sicherer als gefährlich. Es ist wohl gefährlicher, weil es die Warnung implizit verbirgt.
Bryan Chen
Ich werde den Wortlaut so korrigieren, dass er nicht beleidigend ist, aber ich stehe zu meinem Wort. Ich finde die Stummschaltungswarnung nur dann akzeptabel, wenn ich den Code nicht besitze. Kein Ingenieur kann sicher stummgeschalteten Code verwalten, ohne alle Konsequenzen zu verstehen, was bedeuten würde, dieses Argument zu lesen, und diese Praxis ist einfach riskant. vor allem, wenn Sie die 12, einfachen englischen, robusten Alternativen in Betracht ziehen.
SwiftArchitect
1
Nein, du hast meinen Standpunkt nicht verstanden. Die Verwendung performSelectorOnMainThreadist kein guter Weg, um die Warnung zum Schweigen zu bringen, und hat Nebenwirkungen. (Es löst das Speicherleck nicht.) Das Extra #clang diagnostic ignored unterdrückt die Warnung explizit auf sehr klare Weise.
Bryan Chen
Es stimmt, dass das Ausführen eines Selektors für eine Nicht- - (void)Methode das eigentliche Problem ist.
SwiftArchitect
und wie ruft man einen Selektor mit mehreren Argumenten auf und ist gleichzeitig sicher? @ SwiftArchitect
Catalin
4

Da Sie ARC verwenden, müssen Sie iOS 4.0 oder höher verwenden. Dies bedeutet, dass Sie Blöcke verwenden können. Wenn Sie sich nicht an den auszuführenden Selektor erinnern, sondern stattdessen einen Block nehmen würden, könnte ARC besser verfolgen, was tatsächlich vor sich geht, und Sie müssten nicht das Risiko eingehen, versehentlich einen Speicherverlust einzuführen.

Honus
quelle
Tatsächlich machen es Blöcke sehr einfach, versehentlich einen Haltezyklus zu erstellen, den ARC nicht löst. Ich wünsche mir immer noch, dass es eine Compiler-Warnung gab, wenn Sie implizit selfüber einen ivar verwendet haben (z . B. ivaranstelle von self->ivar).
tc.
Du meinst wie -Wimplicit-Retain-Self?
OrangeDog
2

Anstatt den Blockansatz zu verwenden, der mir einige Probleme bereitete:

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

Ich werde NSInvocation wie folgt verwenden:

    -(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];
    }
Supersabbat
quelle
1

Wenn Sie keine Argumente übergeben müssen, können Sie eine einfache Problemumgehung verwenden valueForKeyPath. Dies ist sogar an einem ClassObjekt möglich.

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

Sie können hier auch ein Protokoll verwenden. Erstellen Sie also ein Protokoll wie folgt:

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

In Ihrer Klasse, die Ihren Selektor aufrufen muss, haben Sie dann eine @ -Eigenschaft.

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

Wenn Sie @selector(doSomethingWithObject:)eine Instanz von MyObject aufrufen müssen, gehen Sie folgendermaßen vor:

[self.source doSomethingWithObject:object];
Damon
quelle
2
Hey Wu, danke, aber der Sinn der Verwendung von NSSelectorFromString liegt darin, dass Sie nicht wissen, welchen Selektor Sie zur Laufzeit aufrufen möchten.
Eduardo Scoz