etc./StackOverFlow

performSelector는 선택자를 알 수 없기 때문에 누수를 일으킬 수 있습니다.

청렴결백한 만능 재주꾼 2023. 4. 20. 02:24
반응형

질문자 :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

  1. 객체가 아닌 유형 무시( void , int 등)
  2. 객체 값을 유지하고 더 이상 사용되지 않을 때 해제(표준 가정)
  3. 더 이상 사용되지 않을 때 새 객체 값을 해제합니다( init / copy 패밀리 ns_returns_retained 속성 부여).
  4. 아무것도 하지 않고 반환된 객체 값이 로컬 범위에서 유효하다고 가정합니다( 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에서 사용할 수 없습니다.

시간이 지남에 따라 우리는 다음과 같은 진행 상황을 보았습니다.

  1. Objective-C의 초기 버전은 performSelector: 허용합니다: (수동 메모리 관리)
  2. ARC가 있는 Objective-C는 performSelector:
  3. performSelector: 액세스할 수 없으며 이러한 메서드를 "본질적으로 안전하지 않음"으로 문서화합니다.

그러나 명명된 선택기를 기반으로 메시지를 보내는 아이디어는 "본질적으로 안전하지 않은" 기능이 아닙니다. 이 아이디어는 Objective-C와 다른 많은 프로그래밍 언어에서 오랫동안 성공적으로 사용되었습니다.


1 모든 Objective-C 메서드에는 메서드를 호출할 때 암시적으로 추가되는 self_cmd

2 NULL 함수를 호출하는 것은 C에서 안전하지 않습니다. 컨트롤러의 존재를 확인하는 데 사용되는 가드는 객체가 있는지 확인합니다. _objc_msgForward methodForSelector: 에서 IMP 를 얻을 것이라는 것을 알고 있습니다. 기본적으로 가드가 있으면 호출할 함수가 있다는 것을 알 수 있습니다.

3 id 로 선언하고 모든 헤더를 가져오지 않는 경우 잘못된 정보를 얻을 수 있습니다. 컴파일러가 괜찮다고 생각하는 코드에서 충돌이 발생할 수 있습니다. 이것은 매우 드물지만 발생할 수 있습니다. 일반적으로 두 가지 메서드 시그니처 중에서 선택할 수 없다는 경고가 표시됩니다.

4 자세한 내용은 유지된 반환 값유지되지 않은 반환 값에 대한 ARC 참조를 참조하세요.


wbyoung

Xcode 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 Osipov

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

SwiftArchitect

ARC를 사용하고 있기 때문에 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

반응형