performSelector może spowodować wyciek, ponieważ jego selektor jest nieznany

1258

Otrzymuję następujące ostrzeżenie przez kompilator ARC:

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

Oto co robię:

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

Dlaczego dostaję to ostrzeżenie? Rozumiem, że kompilator nie może sprawdzić, czy selektor istnieje, czy nie, ale dlaczego miałoby to spowodować wyciek? Jak mogę zmienić kod, aby nie wyświetlało się już to ostrzeżenie?

Eduardo Scoz
źródło
3
Nazwa zmiennej jest dynamiczna, zależy od wielu innych rzeczy. Istnieje ryzyko, że nazywam coś, co nie istnieje, ale to nie jest problem.
Eduardo Scoz,
6
@matt dlaczego dynamiczne wywoływanie metody na obiekcie byłoby złą praktyką? Czy celem NSSelectorFromString () nie jest wspieranie tej praktyki?
Eduardo Scoz,
7
Powinieneś / mógłbyś również przetestować [_controller odpowiadaToSelector: mySelector] przed ustawieniem go za pomocą performSelector:
mattacular
50
@mattacular Chciałbym móc głosować w dół: „To… to zła praktyka”.
ctpenrose,
6
Jeśli wiesz, że ciąg znaków jest dosłowny, po prostu użyj @selector (), aby kompilator mógł powiedzieć, jaka jest nazwa selektora. Jeśli Twój rzeczywisty kod wywołuje NSSelectorFromString () z łańcuchem, który został zbudowany lub udostępniony w czasie wykonywania, musisz użyć NSSelectorFromString ().
Chris Page

Odpowiedzi:

1211

Rozwiązanie

Kompilator ostrzega o tym z jakiegoś powodu. Bardzo rzadko to ostrzeżenie należy po prostu zignorować i łatwo się obejść. Oto jak:

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

Lub bardziej zwięźle (choć trudny do odczytania i bez osłony):

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

Wyjaśnienie

Chodzi o to, że pytasz kontroler o wskaźnik funkcji C dla metody odpowiadającej kontrolerowi. Wszystkie NSObjectreagują methodForSelector:, ale możesz także użyć class_getMethodImplementationw środowisku wykonawczym Objective-C (przydatne, jeśli masz tylko odwołanie do protokołu, takie jak id<SomeProto>). Te wskaźniki funkcji są nazywane IMPs i są prostymi typedefwskaźnikami funkcji ed ( id (*IMP)(id, SEL, ...)) 1 . Może to być zbliżone do rzeczywistej sygnatury metody, ale nie zawsze będzie dokładnie pasować.

Gdy już go IMPmasz, musisz rzucić go na wskaźnik funkcji, który zawiera wszystkie szczegóły potrzebne ARC (w tym dwa ukryte argumenty ukryte selfi _cmdkażde wywołanie metody Objective-C). Jest to obsługiwane w trzecim wierszu ( (void *)po prawej stronie informuje kompilator, że wiesz, co robisz, i nie generuje ostrzeżenia, ponieważ typy wskaźników nie pasują).

Na koniec wywołujesz wskaźnik funkcji 2 .

Złożony przykład

Kiedy selektor przyjmuje argumenty lub zwraca wartość, musisz nieco zmienić rzeczy:

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;

Uzasadnienie ostrzeżenia

Powodem tego ostrzeżenia jest to, że w przypadku ARC środowisko wykonawcze musi wiedzieć, co zrobić z wynikiem wywoływanej metody. Rezultatem może być cokolwiek: void, int, char, NSString *, id, itd. ARC normalnie pobiera te informacje z nagłówka typu obiektu ty pracujesz. 3)

Są naprawdę tylko 4 rzeczy, które ARC rozważa dla wartości zwracanej: 4

  1. Rodzaje ignorować niż przedmiot ( void, intitp)
  2. Zachowaj wartość obiektu, a następnie zwolnij ją, gdy nie będzie już używana (założenie standardowe)
  3. Zwolnij nowe wartości obiektów, gdy nie będą już używane (metody w init/ copyrodzinie lub przypisane do ns_returns_retained)
  4. Nie rób nic i zakładaj, że zwrócona wartość obiektu będzie poprawna w zasięgu lokalnym (do momentu wyczerpania wewnętrznej puli zwolnień, przypisanej przez ns_returns_autoreleased)

Wywołanie to methodForSelector:zakłada, że ​​wartość zwracana przez wywoływaną metodę jest obiektem, ale go nie zachowuje / nie zwalnia. Możesz więc spowodować wyciek, jeśli twój obiekt ma zostać zwolniony jak w punkcie 3 powyżej (to znaczy, że wywoływana metoda zwraca nowy obiekt).

W przypadku selektorów, które próbujesz nazwać tym zwrotem voidlub innymi obiektami niebędącymi obiektami, możesz włączyć funkcje kompilatora, aby zignorować ostrzeżenie, ale może to być niebezpieczne. Widziałem, jak Clang przechodzi kilka iteracji tego, w jaki sposób obsługuje zwracane wartości, które nie są przypisane do zmiennych lokalnych. Nie ma powodu, aby po włączeniu ARC nie można było zachować i zwolnić wartości obiektu, która została zwrócona, methodForSelector:nawet jeśli nie chcesz jej używać. Z perspektywy kompilatora jest to jednak obiekt. Oznacza to, że jeśli metoda, którą wywołujesz, someMethodzwraca obiekt niebędący obiektem (w tym void), możesz skończyć z zachowaniem / zwolnieniem wartości wskaźnika śmieci i awarią.

Dodatkowe argumenty

Jednym z powodów jest to, że pojawi się to samo ostrzeżenie performSelector:withObject:i możesz napotkać podobne problemy, nie deklarując, w jaki sposób ta metoda zużywa parametry. ARC pozwala na deklarowanie zużytych parametrów , a jeśli metoda zużywa parametr, prawdopodobnie ostatecznie wyślesz wiadomość do zombie i nastąpi awaria. Istnieją sposoby obejścia tego problemu przy użyciu mostkowania rzutowania, ale tak naprawdę lepiej byłoby po prostu użyć IMPpowyższej metodologii wskaźnika i funkcji. Ponieważ zużyte parametry rzadko stanowią problem, prawdopodobnie się nie pojawi.

Selektory statyczne

Co ciekawe, kompilator nie będzie narzekał na selektory zadeklarowane statycznie:

[_controller performSelector:@selector(someMethod)];

Powodem tego jest fakt, że kompilator faktycznie może rejestrować wszystkie informacje o selektorze i obiekcie podczas kompilacji. Nie musi niczego zakładać. (Sprawdziłem to rok temu, patrząc na źródło, ale nie mam teraz odniesienia.)

Tłumienie

Próbując wymyślić sytuację, w której konieczne byłoby zniesienie tego ostrzeżenia i dobry projekt kodu, wychodzę na pustkę. Ktoś proszę podzielić się, jeśli miał doświadczenie, w którym wyciszenie tego ostrzeżenia było konieczne (a powyższe nie radzi sobie właściwie).

Więcej

Można to również zbudować, NSMethodInvocationaby sobie z tym poradzić, ale wymaga to znacznie więcej pisania i jest wolniejsze, więc nie ma powodu, aby to robić.

Historia

Kiedy performSelector:rodzina metod została po raz pierwszy dodana do celu C, ARC nie istniało. Tworząc ARC, Apple zdecydował, że należy wygenerować ostrzeżenie dla tych metod jako sposób na poprowadzenie programistów do użycia innych środków do jawnego zdefiniowania sposobu obsługi pamięci podczas wysyłania dowolnych wiadomości za pośrednictwem nazwanego selektora. W Objective-C programiści mogą to zrobić, używając rzutów w stylu C na surowych wskaźnikach funkcji.

Wraz z wprowadzeniem Swift, Apple został udokumentowany na performSelector:rodzinę metod jako „z natury niebezpieczne” i nie są one dostępne do Swift.

Z biegiem czasu zaobserwowaliśmy następujący postęp:

  1. Wczesne wersje Objective-C pozwalają performSelector:(ręczne zarządzanie pamięcią)
  2. Cel C z ARC ostrzega przed użyciem performSelector:
  3. Swift nie ma dostępu do performSelector:tych metod i dokumentuje je jako „z natury niebezpieczne”

Idea wysyłania wiadomości na podstawie nazwanego selektora nie jest jednak funkcją „z natury niebezpieczną”. Pomysł ten był z powodzeniem stosowany od dawna w Objective-C, a także w wielu innych językach programowania.


1 Wszystkie metody Objective-C mają dwa ukryte argumenty, selfa _cmdktóre są domyślnie dodawane podczas wywołania metody.

2 Wywołanie NULLfunkcji nie jest bezpieczne w C. Osłona używana do sprawdzania obecności kontrolera zapewnia, że ​​mamy obiekt. Dlatego wiem, że otrzymujemy IMPz methodForSelector:(choć może to być _objc_msgForward, wejście do systemu przekazywania wiadomości). Zasadniczo, gdy strażnik jest na miejscu, wiemy, że mamy funkcję do wywołania.

3 W rzeczywistości możliwe jest uzyskanie błędnych informacji, jeśli zadeklarujesz obiekty jako idi nie importujesz wszystkich nagłówków. Może dojść do awarii kodu, które kompilator uważa za prawidłowe. Jest to bardzo rzadkie, ale może się zdarzyć. Zwykle pojawia się ostrzeżenie, że nie wie, z której z dwóch sygnatur metody wybrać.

4 Aby uzyskać więcej informacji, zobacz odniesienie do ARC dotyczące zachowanych wartości zwrotnych i niezatwierdzonych wartości zwrotnych .

wbyoung
źródło
@wbyoung Jeśli twój kod rozwiązuje problem z utrzymaniem, zastanawiam się, dlaczego performSelector:metody nie są zaimplementowane w ten sposób. Mają ścisłą sygnaturę metody (zwracanie id, przyjmowanie jednej lub dwóch ids), więc nie trzeba obsługiwać typów pierwotnych.
Tricertops,
1
@Andy argument jest obsługiwany na podstawie definicji prototypu metody (nie zostanie zachowany / zwolniony). Problem dotyczy głównie rodzaju zwrotu.
wbyoung
2
„Złożony przykład” podaje błąd Cannot initialize a variable of type 'CGRect (*)(__strong id, SEL, CGRect, UIView *__strong)' with an rvalue of type 'void *'przy korzystaniu z najnowszego Xcode. (5.1.1) Mimo to wiele się nauczyłem!
Stan James
2
void (*func)(id, SEL) = (void *)imp;nie kompiluje się, zastąpiłem govoid (*func)(id, SEL) = (void (*)(id, SEL))imp;
Davyd Geyl
1
zmień void (*func)(id, SEL) = (void *)imp;na <…> = (void (*))imp;lub<…> = (void (*) (id, SEL))imp;
Isaak Osipovich Dunayevsky
1182

W kompilatorze LLVM 3.0 w Xcode 4.2 możesz wyłączyć ostrzeżenie w następujący sposób:

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

Jeśli błąd pojawia się w kilku miejscach i chcesz użyć systemu makr C do ukrycia pragm, możesz zdefiniować makro, aby ułatwić wyłączenie ostrzeżenia:

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

Możesz użyć makra w następujący sposób:

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

Jeśli potrzebujesz wyniku wykonanej wiadomości, możesz to zrobić:

id result;
SuppressPerformSelectorLeakWarning(
    result = [_target performSelector:_action withObject:self]
);
Scott Thompson
źródło
Ta metoda może powodować wycieki pamięci, gdy optymalizacja jest ustawiona na wartość inną niż Brak.
Eric
4
@Eric Nie, nie może, chyba że wywołasz zabawne metody, takie jak „initSomething”, „newSomething” lub „cośKopiuj”.
Andrey Tarantsov,
3
@Julian To działa, ale wyłącza ostrzeżenie dla całego pliku - możesz tego nie potrzebować lub nie chcieć. Owijanie go za pomocą popi push-pragmas są znacznie czystsze i bezpieczniejsze.
Emil,
2
Wszystko to powoduje wyciszenie kompilatora. To nie rozwiązuje problemu. Jeśli selektor nie istnieje, jesteś mocno spieprzony.
Andra Todorescu
2
Powinno to być używane tylko wtedy, gdy jest otoczone if ([_target respondsToSelector:_selector]) {podobną lub podobną logiką.
208

Domyślam się, że: ponieważ selektor nie jest znany kompilatorowi, ARC nie może wymusić właściwego zarządzania pamięcią.

W rzeczywistości zdarza się, że zarządzanie pamięcią jest powiązane z nazwą metody według określonej konwencji. W szczególności myślę o konstruktorach wygody a metodach make ; poprzedni zwrócił konwencją automatycznie wydany obiekt; ten ostatni jest zachowanym przedmiotem. Konwencja opiera się na nazwach selektora, więc jeśli kompilator nie zna selektora, nie może egzekwować właściwej reguły zarządzania pamięcią.

Jeśli jest to poprawne, myślę, że możesz bezpiecznie używać swojego kodu, pod warunkiem, że upewnisz się, że wszystko jest w porządku pod względem zarządzania pamięcią (np. Że twoje metody nie zwracają przydzielonych obiektów).

sergio
źródło
5
Dzięki za odpowiedź, przyjrzę się temu, aby zobaczyć, co się dzieje. Masz pomysł, jak ominąć ostrzeżenie i sprawić, by zniknęło? Nie chciałbym mieć ostrzeżenia w moim kodzie na zawsze, co oznacza bezpieczne połączenie.
Eduardo Scoz,
84
Otrzymałem więc potwierdzenie od kogoś z Apple na forach, że tak właśnie jest. Dodają zapomniane zastąpienie, aby umożliwić użytkownikom wyłączenie tego ostrzeżenia w przyszłych wydaniach. Dzięki.
Eduardo Scoz,
5
Ta odpowiedź rodzi kilka pytań, na przykład jeśli ARC próbuje ustalić, kiedy należy wydać coś na podstawie konwencji i nazw metod, to jak to jest „zliczanie referencji”? Opisane przez ciebie zachowanie brzmi tylko nieznacznie lepiej niż całkowicie arbitralne, jeśli ARC zakłada, że ​​kod jest zgodny z pewną konwencją, w przeciwieństwie do faktycznego śledzenia referencji bez względu na to, która konwencja jest przestrzegana.
aroth
8
ARC automatyzuje proces dodawania zachowań i wydań podczas kompilacji. To nie jest odśmiecanie pamięci (dlatego jest tak niesamowicie szybki i niski narzut). To wcale nie jest arbitralne. Domyślne reguły oparte są na ugruntowanych konwencjach ObjC, które są konsekwentnie stosowane od dziesięcioleci. Pozwala to uniknąć konieczności jawnego dodawania __attributedo każdej metody wyjaśniającej zarządzanie pamięcią. Uniemożliwia to również kompresorowi prawidłowe obsługiwanie tego wzorca (wzorzec, który był bardzo powszechny, ale w ostatnich latach został zastąpiony bardziej solidnymi wzorami).
Rob Napier
8
Więc nie możemy już mieć typu ivar SELi przypisywać różnych selektorów w zależności od sytuacji?
Dobra robota
121

W projekcie Ustawienia budowania , w obszarze Inne flagi ostrzegawcze ( WARNING_CFLAGS), dodaj
-Wno-arc-performSelector-leaks

Teraz upewnij się, że selektor, do którego dzwonisz, nie powoduje zatrzymania lub skopiowania obiektu.

0xced
źródło
12
Uwaga: możesz dodać tę samą flagę dla określonych plików, a nie dla całego projektu. Jeśli spojrzysz w Fazy kompilacji-> Kompiluj źródła, możesz ustawić flagi pliku kompilatora (tak jak chcesz zrobić, aby wykluczyć pliki z ARC). W moim projekcie tylko jeden plik powinien używać selektorów w ten sposób, więc po prostu go wykluczyłem i zostawiłem pozostałe.
Michael
111

Aby obejść ten problem, dopóki kompilator nie będzie mógł zastąpić ostrzeżenia, można użyć środowiska wykonawczego

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

zamiast

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

Będziesz musiał

#import <objc/message.h>

jluckyiv
źródło
8
ARC rozpoznaje konwencje kakao, a następnie dodaje zachowania i wydania w oparciu o te konwencje. Ponieważ C nie przestrzega tych konwencji, ARC zmusza do korzystania z ręcznych technik zarządzania pamięcią. Jeśli tworzysz obiekt CF, musisz go CFRelease (). Jeśli dispatch_queue_create (), musisz dispatch_release (). Podsumowując, jeśli chcesz uniknąć ostrzeżeń ARC, możesz ich uniknąć, używając obiektów C i ręcznego zarządzania pamięcią. Możesz także wyłączyć ARC dla poszczególnych plików, używając flagi kompilatora -fno-objc-arc dla tego pliku.
jluckyiv,
8
Nie bez castingu nie możesz. Varargs to nie to samo, co jawnie wpisana lista argumentów. Zasadniczo będzie działać przez przypadek, ale nie uważam, że „przez przypadek” jest poprawne.
bbum
21
Nie rób tego [_controller performSelector:NSSelectorFromString(@"someMethod")];i objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));nie są równoważne! Spójrz na niedopasowania sygnatur metod i dużą słabość w słabym pisaniu w Celu-C wyjaśniają one dogłębnie problem.
0xced
5
@ 0xced W tym przypadku jest w porządku. objc_msgSend nie utworzy niedopasowania sygnatury metody dla żadnego selektora, który działałby poprawnie w performSelector: ani jego wariantów, ponieważ tylko one przyjmują obiekty jako parametry. Tak długo, jak wszystkie twoje parametry są wskaźnikami (włączając obiekty), dubluje się i NSInteger / long, a typ zwracany jest void, wskaźnik lub długi, wtedy objc_msgSend będzie działał poprawnie.
Matt Gallagher
88

Aby zignorować błąd tylko w pliku z selektorem perform, dodaj #pragma w następujący sposób:

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

To zignoruje ostrzeżenie w tym wierszu, ale nadal pozwoli na to w pozostałej części projektu.

Barlow Tucker
źródło
6
Rozumiem, że można również włączyć ostrzeżenie natychmiast po danej metodzie za pomocą #pragma clang diagnostic warning "-Warc-performSelector-leaks". Wiem, że jeśli wyłączę ostrzeżenie, lubię je włączyć w najbliższym możliwym momencie, więc nie mogę przypadkowo pozwolić, by przeszło kolejne nieoczekiwane ostrzeżenie. Jest mało prawdopodobne, że jest to problem, ale to tylko moja praktyka, gdy wyłączam ostrzeżenie.
Rob
2
Możesz także przywrócić poprzedni stan konfiguracji kompilatora, używając go #pragma clang diagnostic warning pushprzed wprowadzeniem jakichkolwiek zmian i #pragma clang diagnostic warning popprzywrócić poprzedni stan. Przydatne, jeśli wyłączasz obciążenia i nie chcesz mieć wielu linii ponownego pragma w kodzie.
dziekanWombourne,
Zignoruje tylko następujący wiersz?
hfossli,
70

Dziwne, ale prawdziwe: jeśli dopuszczalne (tzn. Wynik jest nieważny i nie masz nic przeciwko jednokrotnemu cyklowi wybiegu), dodaj opóźnienie, nawet jeśli jest to zero:

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

Usuwa to ostrzeżenie, prawdopodobnie dlatego, że zapewnia kompilatorowi, że żaden obiekt nie może zostać zwrócony i jakoś źle zarządzony.

matowy
źródło
2
Czy wiesz, czy to faktycznie rozwiązuje powiązane problemy z zarządzaniem pamięcią, czy ma takie same problemy, ale Xcode nie jest wystarczająco inteligentny, aby ostrzec Cię o tym kodzie?
Aaron Brager
To semantycznie nie to samo! Użycie performSelector: withObject: AfterDelay: wykona selektor w następnym uruchomieniu pętli uruchomieniowej. Dlatego ta metoda natychmiast wraca.
Florian
10
@Florian Oczywiście to nie to samo! Przeczytaj moją odpowiedź: Mówię, jeśli jest do zaakceptowania, ponieważ wynik jest nieważny, a cykl wybiegu zmienia się. To pierwsze zdanie mojej odpowiedzi.
mat.
34

Oto zaktualizowane makro na podstawie odpowiedzi podanej powyżej. Ten powinien pozwolić ci owinąć kod nawet z instrukcją return.

#define SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING(code)                        \
    _Pragma("clang diagnostic push")                                        \
    _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"")     \
    code;                                                                   \
    _Pragma("clang diagnostic pop")                                         \


SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING(
    return [_target performSelector:_action withObject:self]
);
syvex
źródło
6
returnnie musi znajdować się w makrze; return SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING([_target performSelector:_action withObject:self]);działa również i wygląda zdrowiej.
uasi
31

Ten kod nie obejmuje flag kompilatora ani bezpośrednich wywołań środowiska wykonawczego:

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

NSInvocationumożliwia ustawienie wielu argumentów, więc w przeciwieństwie do performSelectortego będzie działać na dowolnej metodzie.

Benedykt Cohen
źródło
3
Czy wiesz, czy to faktycznie rozwiązuje powiązane problemy z zarządzaniem pamięcią, czy ma takie same problemy, ale Xcode nie jest wystarczająco inteligentny, aby ostrzec Cię o tym kodzie?
Aaron Brager
1
Można powiedzieć, że rozwiązuje to problemy z zarządzaniem pamięcią; ale dzieje się tak, ponieważ w zasadzie pozwala określić zachowanie. Na przykład możesz zezwolić, aby wywołanie zachowało argumenty, czy nie. Według mojej obecnej wiedzy, próbuje rozwiązać problemy z niedopasowaniem podpisu, które mogą się pojawić, ufając, że wiesz, co robisz i nie dostarczasz niepoprawnych danych. Nie jestem pewien, czy wszystkie kontrole można wykonać w czasie wykonywania. Jak wspomniano w innym komentarzu, mikeash.com/pyblog/... ładnie wyjaśnia, co może zrobić rozbieżność.
Mihai Timar,
20

Cóż, wiele odpowiedzi tutaj, ale ponieważ jest to trochę inne, łącząc kilka odpowiedzi, pomyślałem, że je wstawię. Korzystam z kategorii NSObject, która sprawdza, czy selektor zwraca wartość void, a także pomija kompilator ostrzeżenie.

#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
źródło
Czy „v” należy zastąpić przez _C_VOID? _C_VOID jest zadeklarowany w <objc / runtime.h>.
Rik Renich,
16

Dla potomności postanowiłem rzucić kapelusz na ring :)

Ostatnio byłem widząc coraz bardziej restrukturyzacji od target/ selectorparadygmatu, za rzeczy, takich jak protokoły, bloków itp Jednak istnieje jeden zamiennik dla performSelectorże użyłem kilka razy:

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

Wydają się być czystym, bezpiecznym dla ARC i prawie identycznym zamiennikiem, performSelectorbez konieczności zbytniego angażowania się objc_msgSend().

Chociaż nie mam pojęcia, czy w iOS jest dostępny analog.

Patrick Perini
źródło
6
Dzięki za tym to .. Jest on dostępny w iOS: [[UIApplication sharedApplication] sendAction: to: from: forEvent:]. Przyglądałem się temu raz, ale trochę dziwnie jest używać klasy związanej z interfejsem użytkownika w środku domeny lub usługi tylko po to, aby wykonać dynamiczne połączenie. Dziękuję za to!
Eduardo Scoz
2
Ew! Będzie miał więcej narzutu (ponieważ musi sprawdzić, czy metoda jest dostępna i przejść do łańcucha odpowiedzi, jeśli nie jest) i będzie zachowywał się inaczej (podchodzi do łańcucha odpowiedzi i zwraca NIE, jeśli nie może nic znaleźć który reaguje na metodę, zamiast po prostu zawiesić się). Nie działa również, gdy chcesz idfrom-performSelector:...
tc.
2
@tc. Nie „idzie w górę łańcucha odpowiedzi”, chyba że to:ma zero, co nie jest. Po prostu idzie prosto do obiektu docelowego bez wcześniejszego sprawdzania. Więc nie ma „więcej kosztów ogólnych”. To nie jest świetne rozwiązanie, ale podany powód nie jest powodem. :)
mat.
15

Odpowiedź Matta Gallowaya w tym wątku wyjaśnia, dlaczego:

Rozważ następujące:

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

Skąd więc ARC może wiedzieć, że pierwszy zwraca obiekt z liczbą zatrzymań równą 1, a drugi zwraca obiekt automatycznie wydany?

Wydaje się, że można bezpiecznie wyłączyć ostrzeżenie, jeśli ignorujesz zwracaną wartość. Nie jestem pewien, jaka jest najlepsza praktyka, jeśli naprawdę potrzebujesz uzyskać zachowany obiekt z performSelector - inny niż „nie rób tego”.

c roald
źródło
14

@ c-road zapewnia właściwą więź z opisem problemu tutaj . Poniżej możesz zobaczyć mój przykład, kiedy performSelector powoduje wyciek pamięci.

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

Jedyną metodą, która powoduje wyciek pamięci w moim przykładzie, jest CopyDummyWithLeak. Powodem jest to, że ARC nie wie, że copySelector zwraca zachowany obiekt.

Jeśli uruchomisz narzędzie Memory Leak Tool, możesz zobaczyć następujący obrazek: wprowadź opis zdjęcia tutaj ... i w żadnym innym przypadku nie ma wycieków pamięci: wprowadź opis zdjęcia tutaj

Pavel Osipov
źródło
6

Aby uczynić makro Scotta Thompsona bardziej ogólnym:

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

Następnie użyj go w następujący sposób:

MYSilenceWarning(-Warc-performSelector-leaks,
[_target performSelector:_action withObject:self];
                )
Ben Flynn
źródło
FWIW, nie dodałem makra. Ktoś dodał to do mojej odpowiedzi. Osobiście nie używałbym makra. Pragnienie ma na celu obejście specjalnego przypadku w kodzie, a pragmy są bardzo jasne i bezpośrednie na temat tego, co się dzieje. Wolę trzymać je na miejscu niż ukryć lub wyodrębnić za makro, ale to tylko ja. YMMV.
Scott Thompson,
@ScottThompson To jest sprawiedliwe. Dla mnie łatwe jest wyszukiwanie tego makra w całej mojej bazie kodu i generalnie dodaję również nie wyciszone ostrzeżenie, aby poradzić sobie z podstawowym problemem.
Ben Flynn,
6

Nie usuwaj ostrzeżeń!

Istnieje nie mniej niż 12 alternatywnych rozwiązań majstrowania przy kompilatorze.
Podczas gdy jesteś sprytny w momencie pierwszej implementacji, niewielu inżynierów na Ziemi może podążać twoimi śladami, a ten kod w końcu się zepsuje.

Bezpieczne trasy:

Wszystkie te rozwiązania będą działać z pewną różnicą w stosunku do pierwotnego celu. Załóż, że parammoże tak być, niljeśli chcesz:

Bezpieczna trasa, to samo zachowanie koncepcyjne:

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

Bezpieczna trasa, nieco inne zachowanie:

(Zobacz odpowiedź)
Użyj dowolnego wątku zamiast [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]];

Niebezpieczne trasy

Wymaga pewnego rodzaju wyciszenia kompilatora, który musi się zepsuć. Zauważ, że w chwili obecnej, to zrobił przerwę w Swift .

// AT YOUR OWN RISK
[_controller performSelector:selector];
[_controller performSelector:selector withObject:anArgument];
[_controller performSelector:selector withObject:anArgument withObject:nil];
SwiftArchitect
źródło
3
Sformułowanie jest bardzo błędne. Bezpieczne trasy wcale nie są bezpieczniejsze niż niebezpieczne. Jest to prawdopodobnie bardziej niebezpieczne, ponieważ ukrywa ostrzeżenie w sposób dorozumiany.
Bryan Chen
Poprawię sformułowanie, aby nie było obraźliwe, ale dotrzymuję słowa. Ostrzeżenie o wyciszeniu jest dopuszczalne tylko wtedy, gdy nie jestem właścicielem kodu. Żaden inżynier nie może bezpiecznie utrzymywać wyciszonego kodu bez zrozumienia wszystkich konsekwencji, co oznaczałoby przeczytanie tego argumentu, a ta praktyka jest po prostu ryzykowna; zwłaszcza jeśli weźmiesz pod uwagę 12, zwykły angielski, solidne alternatywy.
SwiftArchitect,
1
Nie. Nie rozumiesz. Stosując performSelectorOnMainThreadto nie jest dobrym sposobem, aby wyciszyć ostrzeżenia i nie ma skutków ubocznych. (nie rozwiązuje wycieku pamięci) Dodatkowy #clang diagnostic ignored wyraźnie tłumi ostrzeżenie w bardzo wyraźny sposób.
Bryan Chen
Prawdą jest, że wykonanie selektora w nie- - (void)metodzie jest prawdziwym problemem.
SwiftArchitect
i jak wywołać selektor z wieloma argumentami i jednocześnie być bezpiecznym? @SwiftArchitect
Catalin
4

Ponieważ używasz ARC, musisz używać iOS 4.0 lub nowszego. Oznacza to, że możesz użyć bloków. Jeśli zamiast zapamiętać selektor do wykonania, zamiast tego wziąłeś blok, ARC byłby w stanie lepiej śledzić, co się właściwie dzieje i nie musiałbyś ryzykować przypadkowego wprowadzenia wycieku pamięci.

honus
źródło
W rzeczywistości bloki ułatwiają przypadkowe utworzenie cyklu zatrzymania, którego ARC nie rozwiązuje. Nadal chciałbym, aby pojawiło się ostrzeżenie kompilatora, gdy domyślnie używasz go selfprzez ivar (np. ivarZamiast self->ivar).
tc.
Masz na myśli jak -Wimplicit-retain-self?
OrangeDog,
2

Zamiast stosować podejście blokowe, co dało mi pewne problemy:

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

Użyję NSInvocation, tak jak to:

    -(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];
    }
supersabat
źródło
1

Jeśli nie musisz przekazywać żadnych argumentów, skorzystaj z łatwego obejścia valueForKeyPath. Jest to nawet możliwe na Classobiekcie.

NSString *colorName = @"brightPinkColor";
id uicolor = [UIColor class];
if ([uicolor respondsToSelector:NSSelectorFromString(colorName)]){
    UIColor *brightPink = [uicolor valueForKeyPath:colorName];
    ...
}
arsenius
źródło
-2

Możesz również użyć protokołu tutaj. Utwórz więc taki protokół:

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

W klasie, która musi zadzwonić do selektora, masz @ właściwość.

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

Jeśli chcesz wywołać @selector(doSomethingWithObject:)instancję MyObject, wykonaj następujące czynności:

[self.source doSomethingWithObject:object];
Damon
źródło
2
Cześć Wu, dziękuję, ale celem użycia NSSelectorFromString jest to, że nie wiesz, który selektor chcesz wywołać w czasie wykonywania.
Eduardo Scoz,