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, der Compiler kann nicht überprüfen, ob der Selektor existiert oder nicht, aber warum würde das ein Leck verursachen? Und wie kann ich meinen Code so ändern, dass ich diese Warnung nicht mehr erhalte?
Der Compiler warnt aus einem bestimmten Grund davor. Es kommt sehr selten vor, 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);
Hier fragen Sie den Controller nach dem C-Funktionszeiger für die Methode, die dem Controller entspricht. Alle NSObject
antworten auf methodForSelector:
, Sie können jedoch auch class_getMethodImplementation
in der Objective-C-Laufzeit verwenden (nützlich, wenn Sie nur über eine Protokollreferenz verfügen, z. B. id<SomeProto>
). Diese Funktionszeiger heißen IMP
s und sind einfache typedef
ed-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 den IMP
haben, müssen Sie ihn in einen Funktionszeiger umwandeln, der alle Details enthält, die ARC benötigt (einschließlich der beiden impliziten versteckten Argumente self
und _cmd
jedes Objective- C Methodenaufruf). Dies wird in der dritten Zeile behandelt (der (void *)
auf der rechten Seite teilt dem Compiler einfach mit, dass Sie wissen, was Sie tun, und dass keine Warnung generiert werden soll, da die Zeigertypen nicht übereinstimmen).
Zuletzt rufen Sie den Funktionszeiger auf2.
Wenn der Selektor Argumente akzeptiert oder einen Wert zurückgibt, müssen Sie einige Dinge ä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;
Der Grund für diese Warnung ist, dass die Laufzeit bei ARC wissen muss, was mit dem Ergebnis der aufgerufenen Methode geschehen soll. Das Ergebnis kann alles Mögliche sein: void
, int
, char
, NSString *
, id
usw. Normalerweise bezieht ARC diese Informationen aus dem Header des Objekttyps du arbeitest mit.3
Es gibt wirklich nur 4 Dinge, die ARC für den Rückgabewert berücksichtigen würde:4
void
, int
usw.)init
/copy
-Familie oder mit ns_returns_retained
belegt)ns_returns_autoreleased
zugewiesen wurde).Der Aufruf von methodForSelector:
geht davon aus, dass der Rückgabewert der aufgerufenen Methode ein Objekt ist, es jedoch nicht beibehält/freigibt. Sie könnten also ein Leck erzeugen, wenn Ihr Objekt wie in # 3 oben freigegeben werden soll (das heißt, die Methode, die Sie aufrufen, gibt ein neues Objekt zurück).
Für Selektoren, die Sie versuchen, diese Rückgabe void
oder 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 durchlief, wie er Rückgabewerte handhabt, die nicht lokalen Variablen zugewiesen sind. Bei aktiviertem ARC kann der von methodForSelector:
zurückgegebene Objektwert nicht beibehalten und freigegeben werden, obwohl Sie ihn nicht verwenden möchten. Aus der Sicht des Compilers ist es schließlich ein Objekt. Das bedeutet, dass, wenn die aufgerufene Methode someMethod
ein Nicht-Objekt (einschließlich void
) zurückgibt, ein Müllzeigerwert beibehalten/freigegeben werden und abstürzen kann.
Eine Überlegung ist, dass dies die gleiche Warnung ist, die bei performSelector:withObject:
auftritt, und Sie könnten auf ähnliche Probleme stoßen, wenn Sie nicht deklarieren, wie diese Methode Parameter verbraucht. ARC erlaubt das Deklarieren von konsumierte Parameter , und wenn die Methode den Parameter konsumiert, werden Sie wahrscheinlich irgendwann eine Nachricht an einen Zombie senden und abstürzen. Es gibt Möglichkeiten, dies mit Bridged Casting zu umgehen, aber es wäre wirklich besser, einfach die oben beschriebene Methode für IMP
und Funktionszeiger zu verwenden. Da verbrauchte Parameter selten ein Problem darstellen, ist es unwahrscheinlich, dass dies auftritt.
Interessanterweise beschwert sich der Compiler nicht über statisch deklarierte Selektoren:
[_controller performSelector:@selector(someMethod)];
Der Grund dafür ist, dass der Compiler tatsächlich alle Informationen über den Selektor und das Objekt während der Kompilierung 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.)
Bei dem Versuch, an eine Situation zu denken, in der die Unterdrückung dieser Warnung notwendig und ein gutes Code-Design erforderlich wäre, komme ich leer aus. Bitte teilen Sie jemandem mit, wenn er eine Erfahrung gemacht hat, bei der diese Warnung stummgeschaltet werden musste (und die oben genannten Dinge nicht richtig handhaben).
Es ist möglich, ein NSMethodInvocation
zu erstellen, um dies ebenfalls zu handhaben, aber dies erfordert viel mehr Eingaben und ist auch langsamer. Es gibt also wenig Grund, dies zu tun.
Als die Methodenfamilie performSelector:
zum ersten Mal zu Objective-C hinzugefügt wurde, war ARC nicht vorhanden. Beim Erstellen von ARC entschied Apple, dass für diese Methoden eine Warnung generiert werden sollte, um die Entwickler dazu anzuhalten, andere Methoden zu verwenden, um explizit zu definieren, wie der Speicher beim Senden beliebiger Nachrichten über einen benannten Selektor behandelt werden soll. In Objective-C können Entwickler dazu C-Darstellungen für rohe Funktionszeiger verwenden.
Mit der Einführung von Swift hat Apple dokumentiert die Methodenfamilie performSelector:
als "inhärent unsicher" eingestuft und steht Swift nicht zur Verfügung.
Im Laufe der Zeit haben wir diesen Fortschritt gesehen:
performSelector:
(manuelle Speicherverwaltung)performSelector:
performSelector:
und dokumentiert diese Methoden als "inhärent 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 und vielen anderen Programmiersprachen eingesetzt.
1 Alle Objective-C-Methoden haben zwei versteckte Argumente, self
und _cmd
, die implizit hinzugefügt werden, wenn Sie eine Methode aufrufen.
2 Das Aufrufen einer NULL
-Funktion ist in C nicht sicher. Der Wächter, mit dem das Vorhandensein des Controllers überprüft wird, stellt sicher, dass wir ein Objekt haben. Wir wissen daher, dass wir von methodForSelector:
einen IMP
bekommen (obwohl es _objc_msgForward
sein kann, Eintrag in das Nachrichtenweiterleitungssystem). Grundsätzlich wissen wir, dass wir mit der Wache eine Funktion haben, die wir anrufen können.
3 Tatsächlich ist es möglich, dass die falschen Informationen angezeigt werden, wenn Sie Objekte als id
deklarieren und 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 welchen beiden Methodensignaturen Sie auswählen sollen.
4 Weitere Informationen finden Sie in der ARC-Referenz zu beibehaltene Rückgabewerte und nicht gespeicherte Rückgabewerte .
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 der Fehler an mehreren Stellen auftritt und Sie das C-Makrosystem zum Ausblenden der Pragmas verwenden möchten, können Sie ein Makro definieren, um die Unterdrückung 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 durchgeführten Meldung benötigen, können Sie dies tun:
id result;
SuppressPerformSelectorLeakWarning(
result = [_target performSelector:_action withObject:self]
);
Meine Vermutung 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 versus make Methoden; die erstere gibt ein automatisch freigegebenes Objekt vereinbarungsgemäß zurück; letzteres ein zurückbehaltener Gegenstand. Die Konvention basiert auf den Namen des Selektors. Wenn der Compiler den Selektor nicht kennt, kann er die richtige Speicherverwaltungsregel nicht durchsetzen.
Wenn dies korrekt ist, können Sie Ihren Code sicher verwenden, sofern Sie sicherstellen, dass die Speicherverwaltung in Ordnung ist (z. B., dass Ihre Methoden keine von ihnen zugewiesenen Objekte zurückgeben).
Fügen Sie in Ihrem Projekt Build Settings unter Other Warning Flags (WARNING_CFLAGS
) hinzu-Wno-arc-performSelector-leaks
Stellen Sie jetzt nur sicher, dass der von Ihnen aufgerufene Selektor nicht dazu führt, dass Ihr Objekt beibehalten oder kopiert wird.
Um dieses Problem zu umgehen, können Sie die Laufzeitumgebung verwenden, bis der Compiler das Überschreiben der Warnung zulässt
objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));
anstatt von
[_controller performSelector:NSSelectorFromString(@"someMethod")];
Du musst
#import <objc/message.h>
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.
Merkwürdig, aber wahr: Wenn akzeptabel (d. H. 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 der Compiler dadurch beruhigt wird, dass kein Objekt zurückgegeben und auf irgendeine Weise falsch verwaltet werden kann.
Hier ist ein aktualisiertes Makro basierend auf der oben gegebenen Antwort. Dieser sollte es Ihnen ermöglichen, Ihren Code auch mit einer return-Anweisung zu umbrechen.
#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]
);
Dieser Code beinhaltet keine Compiler-Flags oder direkte Laufzeitaufrufe:
SEL selector = @selector(zeroArgumentMethod);
NSMethodSignature *methodSig = [[self class] instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setSelector:selector];
[invocation setTarget:self];
[invocation invoke];
NSInvocation
ermöglicht das Setzen mehrerer Argumente, so dass dies im Gegensatz zu performSelector
bei jeder Methode funktioniert.
Nun, viele Antworten hier, aber da dies ein wenig anders ist, kombiniere ich ein paar Antworten, von denen ich dachte, dass ich sie einfügen würde. Ich verwende eine NSObject-Kategorie, die prüft, ob der Selektor ungültig ist und 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
Der Nachwelt zuliebe habe ich beschlossen, meinen Hut in den Ring zu werfen :)
Vor kurzem habe ich immer mehr Umstrukturierungen außerhalb des target
/selector
-Paradigmas zugunsten von Dingen wie Protokollen, Blöcken usw. erlebt. Es gibt jedoch einen Ersatz für performSelector
das ich jetzt schon ein paar mal benutzt habe:
[NSApp sendAction: NSSelectorFromString(@"someMethod") to: _controller from: nil];
Dies scheint ein sauberer, ARC-sicherer und nahezu identischer Ersatz für performSelector
zu sein, ohne sich mit objc_msgSend()
zu beschäftigen.
Ich habe allerdings keine Ahnung, ob es ein Analogon für iOS gibt.
Die Antwort von Matt Galloway auf dieser Thread erklärt das Warum:
Folgendes berücksichtigen:
id anotherObject1 = [someObject performSelector:@selector(copy)]; id anotherObject2 = [someObject performSelector:@selector(giveMeAnotherNonRetainedObject)];
Woher kann ARC nun wissen, dass der erste ein Objekt mit einer Beibehaltungsanzahl von 1 zurückgibt, der zweite jedoch ein Objekt, das automatisch freigegeben wurde?
Es scheint, dass es im Allgemeinen sicher ist, die Warnung zu unterdrücken, wenn Sie den Rückgabewert ignorieren. Ich bin 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-road liefert den richtigen Link 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 dafür ist, dass ARC nicht weiß, dass copySelector ein beibehaltenes Objekt zurückgibt.
Wenn Sie das Memory Leak Tool ausführen, wird das folgende Bild angezeigt: ... und in keinem anderen Fall gibt es Speicherlecks:
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];
)
Es gibt nicht weniger als 12 alternative Lösungen zum Basteln mit dem Compiler.
Während Sie zur Zeit der ersten Implementierung clever sind, können nur wenige Ingenieure auf der Erde Ihren Schritten folgen, und dieser Code wird irgendwann brechen.
Sichere Routen:
Alle diese Lösungen funktionieren mit einem gewissen Grad an Abweichung von Ihrer ursprünglichen Absicht. Angenommen, param
kann nil
sein, wenn Sie dies wünschen:
Sichere Route, 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]];
Sichere Route, etwas anderes Verhalten:
(Siehe this response)
Verwenden Sie einen beliebigen Thread anstelle von [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 Routen
Erfordert eine Art Compiler-Stummschaltung, die zwangsläufig unterbrochen wird. Beachten Sie, dass es derzeit did break in Swift.
// AT YOUR OWN RISK
[_controller performSelector:selector];
[_controller performSelector:selector withObject:anArgument];
[_controller performSelector:selector withObject:anArgument withObject:nil];
Da Sie ARC verwenden, müssen Sie iOS 4.0 oder höher verwenden. Das heißt, Sie könnten Blöcke verwenden. Wenn ARC sich nicht mehr an den Selektor erinnert, den Sie ausgeführt haben, sondern einen Block nimmt, kann es besser verfolgen, was tatsächlich vor sich geht, und Sie müssen nicht das Risiko eingehen, versehentlich einen Speicherverlust zu verursachen.
Anstatt den Block-Ansatz zu verwenden, gab es einige Probleme:
IMP imp = [_controller methodForSelector:selector];
void (*func)(id, SEL) = (void *)imp;
Ich werde NSInvocation folgendermaßen 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];
}
Wenn Sie keine Argumente übergeben müssen, können Sie dies einfach umgehen, indem Sie valueForKeyPath
verwenden. Dies ist sogar für ein Class
Objekt möglich.
NSString *colorName = @"brightPinkColor";
id uicolor = [UIColor class];
if ([uicolor respondsToSelector:NSSelectorFromString(colorName)]){
UIColor *brightPink = [uicolor valueForKeyPath:colorName];
...
}