질문자 :Eduardo Scoz
ARC 컴파일러에서 다음과 같은 경고가 표시됩니다.
"performSelector may cause a leak because its selector is unknown".
내가 하는 일은 다음과 같습니다.
[_controller performSelector:NSSelectorFromString(@"someMethod")];
이 경고가 표시되는 이유는 무엇입니까? 컴파일러가 선택기가 있는지 여부를 확인할 수 없다는 것을 이해하지만 누출이 발생하는 이유는 무엇입니까? 이 경고가 더 이상 표시되지 않도록 코드를 변경하려면 어떻게 해야 합니까?
해결책
컴파일러는 이유 때문에 이에 대해 경고합니다. 이 경고를 단순히 무시해야 하는 경우는 매우 드물며 쉽게 해결할 수 있습니다. 방법은 다음과 같습니다.
if (!_controller) { return; } SEL selector = NSSelectorFromString(@"someMethod"); IMP imp = [_controller methodForSelector:selector]; void (*func)(id, SEL) = (void *)imp; func(_controller, selector);
또는 더 간결하게 (가드 없이는 읽기 힘들지만):
SEL selector = NSSelectorFromString(@"someMethod"); ((void (*)(id, SEL))[_controller methodForSelector:selector])(_controller, selector);
설명
여기서 진행되는 것은 컨트롤러에 해당하는 메서드에 대한 C 함수 포인터를 컨트롤러에 요청하는 것입니다. 모든 NSObject
class_getMethodImplementation
methodForSelector:
응답하지만 Objective-C 런타임에서 class_getMethodImplementation을 사용할 수도 있습니다 id<SomeProto>
와 같은 프로토콜 참조만 있는 경우에 유용). 이러한 함수 포인터를 IMP
라고 하며 단순 typedef
함수 포인터( id (*IMP)(id, SEL, ...)
) 1 입니다. 이것은 메서드의 실제 메서드 서명에 가까울 수 있지만 항상 정확히 일치하지는 않습니다.
IMP
가 있으면 ARC가 필요로 하는 모든 세부 정보를 포함하는 함수 포인터로 변환해야 합니다(모든 Objective-C 메서드 호출 self
및 _cmd
이것은 세 번째 줄에서 처리됩니다( (void *)
는 단순히 컴파일러에게 당신이 무엇을 하고 있는지 알고 있으며 포인터 유형이 일치하지 않기 때문에 경고를 생성하지 않는다는 것을 알려줍니다).
마지막으로 함수 포인터 2 를 호출합니다.
복잡한 예
선택자가 인수를 취하거나 값을 반환할 때 약간 변경해야 합니다.
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;
경고 이유
이 경고가 발생하는 이유는 ARC를 사용하는 경우 런타임에서 호출하는 메서드의 결과로 무엇을 해야 하는지 알아야 하기 때문입니다. void
, int
, char
, NSString *
, id
등 무엇이든 될 수 있습니다. ARC는 일반적으로 작업 중인 객체 유형의 헤더에서 이 정보를 가져옵니다. 삼
ARC가 반환 값에 대해 고려할 사항은 실제로 4가지뿐입니다. 4
- 객체가 아닌 유형 무시(
void
, int
등) - 객체 값을 유지하고 더 이상 사용되지 않을 때 해제(표준 가정)
- 더 이상 사용되지 않을 때 새 객체 값을 해제합니다(
init
/ copy
패밀리 ns_returns_retained
속성 부여). - 아무것도 하지 않고 반환된 객체 값이 로컬 범위에서 유효하다고 가정합니다(
ns_returns_autoreleased
로 인해 내부 대부분의 릴리스 풀이 소진될 때까지).
methodForSelector:
호출은 호출하는 메서드의 반환 값이 객체라고 가정하지만 이를 유지/해제하지는 않습니다. 따라서 위의 #3과 같이 개체를 해제해야 하는 경우 누수가 발생할 수 있습니다(즉, 호출하는 메서드가 새 개체를 반환함).
void
또는 기타 비객체를 호출하려는 선택기의 경우 컴파일러 기능이 경고를 무시하도록 설정할 수 있지만 위험할 수 있습니다. Clang이 지역 변수에 할당되지 않은 반환 값을 처리하는 방법을 몇 번 반복하는 것을 보았습니다. methodForSelector:
에서 반환된 객체 값을 유지하고 해제할 수 없는 이유가 없습니다. 사용하고 싶지 않더라도. 컴파일러의 관점에서 볼 때 그것은 결국 객체입니다. 즉, 호출하는 메서드 someMethod
가 비 객체( void
포함)를 반환하는 경우 가비지 포인터 값이 유지/해제되고 충돌이 발생할 수 있습니다.
추가 인수
한 가지 고려 사항은 이것이 performSelector:withObject:
와 동일한 경고가 발생하고 해당 메소드가 매개변수를 사용하는 방법을 선언하지 않으면 유사한 문제에 부딪힐 수 있다는 것입니다. ARC를 사용하면 소비된 매개변수 를 선언할 수 있으며 메서드가 매개변수를 소비하면 결국 좀비에게 메시지를 보내고 충돌하게 될 것입니다. 브리지 캐스팅으로 이 문제를 해결할 수 있는 방법이 있지만 실제로는 IMP
및 함수 포인터 방법을 사용하는 것이 더 좋습니다. 소비된 매개변수가 문제가 되는 경우는 드물기 때문에 이 문제가 발생하지 않을 것입니다.
정적 선택기
흥미롭게도 컴파일러는 정적으로 선언된 선택기에 대해 불평하지 않습니다.
[_controller performSelector:@selector(someMethod)];
그 이유는 컴파일러가 실제로 컴파일하는 동안 선택자와 개체에 대한 모든 정보를 기록할 수 있기 때문입니다. 어떤 것에 대해서도 가정할 필요가 없습니다. (1년전에 출처를 보고 확인했는데 지금은 참고자료가 없네요.)
억압
이 경고의 억제가 필요하고 좋은 코드 디자인이 필요한 상황을 생각하려고 할 때 나는 공백이 되어 가고 있습니다. 누군가 이 경고를 차단해야 하는 경험이 있는 경우 공유해 주십시오(그리고 위의 내용이 제대로 처리되지 않음).
더
NSMethodInvocation
을 구축하는 것도 가능하지만 그렇게 하려면 훨씬 더 많은 타이핑이 필요하고 속도도 느려지므로 그렇게 할 이유가 거의 없습니다.
역사
performSelector:
메소드 패밀리가 Objective-C에 처음 추가되었을 때 ARC는 존재하지 않았습니다. ARC를 만드는 동안 Apple은 개발자가 명명된 선택기를 통해 임의의 메시지를 보낼 때 메모리를 처리하는 방법을 명시적으로 정의하는 다른 수단을 사용하도록 안내하는 방법으로 이러한 메서드에 대한 경고를 생성해야 한다고 결정했습니다. Objective-C에서 개발자는 원시 함수 포인터에 C 스타일 캐스트를 사용하여 이를 수행할 수 있습니다.
Swift의 도입과 함께 Apple 은 performSelector:
메소드 제품군을 "본질적으로 안전하지 않은" 것으로 문서화했으며 Swift에서 사용할 수 없습니다.
시간이 지남에 따라 우리는 다음과 같은 진행 상황을 보았습니다.
- Objective-C의 초기 버전은
performSelector:
허용합니다: (수동 메모리 관리) - ARC가 있는 Objective-C는
performSelector:
-
performSelector:
액세스할 수 없으며 이러한 메서드를 "본질적으로 안전하지 않음"으로 문서화합니다.
그러나 명명된 선택기를 기반으로 메시지를 보내는 아이디어는 "본질적으로 안전하지 않은" 기능이 아닙니다. 이 아이디어는 Objective-C와 다른 많은 프로그래밍 언어에서 오랫동안 성공적으로 사용되었습니다.
1 모든 Objective-C 메서드에는 메서드를 호출할 때 암시적으로 추가되는 self
및 _cmd
2 NULL
함수를 호출하는 것은 C에서 안전하지 않습니다. 컨트롤러의 존재를 확인하는 데 사용되는 가드는 객체가 있는지 확인합니다. _objc_msgForward
methodForSelector:
에서 IMP
를 얻을 것이라는 것을 알고 있습니다. 기본적으로 가드가 있으면 호출할 함수가 있다는 것을 알 수 있습니다.
3 id
로 선언하고 모든 헤더를 가져오지 않는 경우 잘못된 정보를 얻을 수 있습니다. 컴파일러가 괜찮다고 생각하는 코드에서 충돌이 발생할 수 있습니다. 이것은 매우 드물지만 발생할 수 있습니다. 일반적으로 두 가지 메서드 시그니처 중에서 선택할 수 없다는 경고가 표시됩니다.
4 자세한 내용은 유지된 반환 값 및 유지되지 않은 반환 값에 대한 ARC 참조를 참조하세요.
wbyoungXcode 4.2의 LLVM 3.0 컴파일러에서 다음과 같이 경고를 표시하지 않을 수 있습니다.
#pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self.ticketTarget performSelector: self.ticketAction withObject: self]; #pragma clang diagnostic pop
여러 위치에서 오류가 발생하고 C 매크로 시스템을 사용하여 pragma를 숨기려면 매크로를 정의하여 경고를 더 쉽게 표시하지 않도록 할 수 있습니다.
#define SuppressPerformSelectorLeakWarning(Stuff) \ do { \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \ Stuff; \ _Pragma("clang diagnostic pop") \ } while (0)
다음과 같이 매크로를 사용할 수 있습니다.
SuppressPerformSelectorLeakWarning( [_target performSelector:_action withObject:self] );
수행된 메시지의 결과가 필요한 경우 다음을 수행할 수 있습니다.
id result; SuppressPerformSelectorLeakWarning( result = [_target performSelector:_action withObject:self] );
Scott Thompson이에 대한 제 추측은 다음과 같습니다. 선택기가 컴파일러에 알려지지 않았기 때문에 ARC는 적절한 메모리 관리를 시행할 수 없습니다.
실제로 메모리 관리가 특정 규칙에 따라 메서드 이름과 연결되는 경우가 있습니다. 특히, 편의 생성자 대 make 메소드를 생각하고 있습니다. 전자는 관례에 따라 자동 릴리스된 객체를 반환합니다. 후자는 보유 객체입니다. 규칙은 선택기의 이름을 기반으로 하므로 컴파일러가 선택기를 알지 못하면 적절한 메모리 관리 규칙을 적용할 수 없습니다.
이것이 맞다면 메모리 관리와 관련하여 모든 것이 괜찮은지(예: 메서드가 할당한 개체를 반환하지 않음) 코드를 안전하게 사용할 수 있다고 생각합니다.
sergio프로젝트 빌드 설정 에서 기타 경고 플래그 ( WARNING_CFLAGS
) 아래에 다음을 추가합니다.
-Wno-arc-performSelector-leaks
이제 호출하는 선택기로 인해 개체가 유지되거나 복사되지 않는지 확인하십시오.
0xced컴파일러가 경고 무시를 허용할 때까지 해결 방법으로 런타임을 사용할 수 있습니다.
objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));
대신에
[_controller performSelector:NSSelectorFromString(@"someMethod")];
당신은해야 할거야
#import <objc/message.h>
jluckyiv수행 선택기가 있는 파일에서만 오류를 무시하려면 다음과 같이 #pragma를 추가합니다.
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
이렇게 하면 이 줄의 경고가 무시되지만 나머지 프로젝트에서는 계속 허용됩니다.
Barlow Tucker이상하지만 사실입니다. 허용되는 경우(예: 결과가 무효이고 런루프가 한 번만 순환하도록 하는 데 신경 쓰지 않는 경우) 지연을 추가합니다. 이 값이 0인 경우에도 마찬가지입니다.
[_controller performSelector:NSSelectorFromString(@"someMethod") withObject:nil afterDelay:0];
이것은 아마도 어떤 객체도 반환될 수 없고 어떻게든 잘못 관리된다는 것을 컴파일러를 안심시키기 때문에 경고를 제거합니다.
matt다음은 위에 제공된 답변을 기반으로 업데이트된 매크로입니다. 이렇게 하면 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이 코드는 컴파일러 플래그나 직접 런타임 호출을 포함하지 않습니다.
SEL selector = @selector(zeroArgumentMethod); NSMethodSignature *methodSig = [[self class] instanceMethodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; [invocation setSelector:selector]; [invocation setTarget:self]; [invocation invoke];
NSInvocation
사용하면 여러 인수를 설정할 수 있으므로 performSelector
와 달리 모든 메서드에서 작동합니다.
Benedict Cohen글쎄, 여기에 많은 답변이 있지만 이것은 조금 다르기 때문에 몇 가지 답변을 결합하여 내가 넣을 것이라고 생각했습니다. 선택기가 void를 반환하는지 확인하고 컴파일러를 억제하는 NSObject 범주를 사용하고 있습니다. 경고.
#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후세를 위해 모자를 링에 던지기로 했어요 :)
최근에 나는 프로토콜, 블록 등과 같은 것에 찬성 target
/ selector
패러다임에서 점점 더 많은 재구성을 보고 있습니다. 그러나 지금 몇 번 사용한 performSelector
:
[NSApp sendAction: NSSelectorFromString(@"someMethod") to: _controller from: nil];
이것들은 깨끗하고 ARC 안전 objc_msgSend()
로 많은 것을 할 필요 없이 performSelector
를 거의 동일한 대체품으로 보입니다.
하지만 iOS에서 아날로그를 사용할 수 있는지 여부는 알 수 없습니다.
Patrick Perini이 스레드에 대한 Matt Galloway의 답변은 그 이유를 다음과 같이 설명합니다.
다음을 고려하세요:
id anotherObject1 = [someObject performSelector:@selector(copy)]; id anotherObject2 = [someObject performSelector:@selector(giveMeAnotherNonRetainedObject)];
이제 ARC는 첫 번째는 유지 횟수가 1인 객체를 반환하고 두 번째는 자동 해제된 객체를 반환한다는 것을 어떻게 알 수 있습니까?
반환 값을 무시하는 경우 일반적으로 경고를 억제하는 것이 안전한 것 같습니다. "하지 마십시오"가 아닌 performSelector에서 보유된 객체를 실제로 가져와야 하는 경우 모범 사례가 무엇인지 잘 모르겠습니다.
c roald@c-road는 여기에 문제 설명과 함께 올바른 링크를 제공합니다. 아래에서 performSelector가 메모리 누수를 일으키는 내 예를 볼 수 있습니다.
@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; }
내 예제에서 메모리 누수를 일으키는 유일한 방법은 CopyDummyWithLeak입니다. 그 이유는 ARC가 그 copySelector가 보유된 객체를 반환한다는 것을 모르기 때문입니다.
Memory Leak Tool을 실행하면 다음 그림을 볼 수 있습니다. ...다른 경우에는 메모리 누수가 없습니다.
Pavel OsipovScott Thompson의 매크로를 보다 일반적으로 만들려면:
// 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")
그런 다음 다음과 같이 사용하십시오.
MYSilenceWarning(-Warc-performSelector-leaks, [_target performSelector:_action withObject:self]; )
Ben Flynn경고를 억제하지 마십시오!
컴파일러를 수정하는 데 12 가지 이상의 대안 솔루션이 있습니다.
첫 번째 구현 당시에는 영리했지만 지구상의 엔지니어는 당신의 발자취를 따라갈 수 있는 사람이 거의 없으며 이 코드는 결국 깨질 것입니다.
안전한 경로:
이러한 모든 솔루션은 원래 의도와 어느 정도 차이가 있지만 작동합니다. 원하는 경우 param
nil
이 될 수 있다고 가정합니다.
안전한 경로, 동일한 개념적 동작:
// 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]];
안전한 경로, 약간 다른 동작:
( 이 응답 참조)
[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]];
위험한 경로
일종의 컴파일러 침묵이 필요하며 이는 중단될 수밖에 없습니다. 현 시점에서, 그것은 스위프트에서 휴식을했다합니다.
// AT YOUR OWN RISK [_controller performSelector:selector]; [_controller performSelector:selector withObject:anArgument]; [_controller performSelector:selector withObject:anArgument withObject:nil];
SwiftArchitectARC를 사용하고 있기 때문에 iOS 4.0 이상을 사용해야 합니다. 이것은 블록을 사용할 수 있음을 의미합니다. 수행을 위해 선택기를 기억하는 대신 블록을 사용하면 ARC가 실제로 진행 중인 일을 더 잘 추적할 수 있고 실수로 메모리 누수가 발생할 위험을 감수할 필요가 없습니다.
honus블록 접근 방식을 사용하는 대신 몇 가지 문제가 발생했습니다.
IMP imp = [_controller methodForSelector:selector]; void (*func)(id, SEL) = (void *)imp;
다음과 같이 NSInvocation을 사용할 것입니다.
-(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]; }
supersabbath인수를 전달할 필요가 없는 경우 쉬운 해결 방법은 valueForKeyPath
를 사용하는 것입니다. Class
객체에서도 가능합니다.
NSString *colorName = @"brightPinkColor"; id uicolor = [UIColor class]; if ([uicolor respondsToSelector:NSSelectorFromString(colorName)]){ UIColor *brightPink = [uicolor valueForKeyPath:colorName]; ... }
arsenius여기에서 프로토콜을 사용할 수도 있습니다. 따라서 다음과 같은 프로토콜을 만듭니다.
@protocol MyProtocol -(void)doSomethingWithObject:(id)object; @end
선택자를 호출해야 하는 클래스에는 @property가 있습니다.
@interface MyObject @property (strong) id<MyProtocol> source; @end
@selector(doSomethingWithObject:)
를 호출해야 하는 경우 다음을 수행하십시오.
[self.source doSomethingWithObject:object];
Damon출처 : http:www.stackoverflow.com/questions/7017281/performselector-may-cause-a-leak-because-its-selector-is-unknown