web-dev-qa-db-ja.com

performSelectorは、そのセレクターが不明なためにリークを引き起こす

ARCコンパイラから次のような警告が出ます。

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

これが私がしていることです:

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

なぜ私はこの警告を受けるのですか?私はコンパイラがセレクタが存在するかどうかをチェックできないことを理解していますが、なぜそれがリークを引き起こすのでしょうか?そして、この警告が表示されないようにコードを変更するにはどうすればよいですか。

1234
Eduardo Scoz

溶液

コンパイラは理由でこれについて警告しています。この警告が単に無視されるべきであることは非常にまれであり、回避するのは簡単です。方法は次のとおりです。

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関数ポインタをコントローラに要求しているということです。すべてのNSObjectmethodForSelector:に応答しますが、Objective-Cランタイムでclass_getMethodImplementationを使用することもできます(id<SomeProto>のようにプロトコル参照のみがある場合に便利です)。これらの関数ポインタはIMPsと呼ばれ、単純なtypedefed関数ポインタです(id (*IMP)(id, SEL, ...)1。これはメソッドの実際のメソッドシグネチャに近いかもしれませんが、必ずしも完全に一致するとは限りません。

IMPを取得したら、ARCが必要とするすべての詳細(すべてのObjective-Cメソッド呼び出しの2つの暗黙の隠し引数self_cmdを含む)を含む関数ポインタにキャストする必要があります。これは3行目で処理されます(右側の(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では、ランタイムは呼び出しているメソッドの結果をどう処理するかを知る必要があるためです。結果は何でも構いません:voidintcharNSString *idなど。通常、ARCは作業しているオブジェクトタイプのヘッダからこの情報を取得します。3

ARCが戻り値に対して考慮することは、実際には4つだけです。4

  1. 非オブジェクト型を無視する(voidintなど)
  2. オブジェクトの値を保持し、使用されなくなったら解放する(標準的な想定)
  3. 使用されなくなったときに新しいオブジェクト値を解放する(init/copyファミリーのメソッド、またはns_returns_retainedで属性付けされたメソッド)
  4. 何もしないで、返されたオブジェクト値がローカルスコープで有効であると仮定します(最も内側のリリースプールが空になるまでは、ns_returns_autoreleasedで属性付けられます)。

methodForSelector:の呼び出しは、それが呼び出しているメソッドの戻り値がオブジェクトであると仮定しますが、それを保持または解放しません。そのため、上記の#3のようにオブジェクトが解放されることになっている場合は、リークが発生する可能性があります(つまり、呼び出すメソッドは新しいオブジェクトを返します)。

voidやその他の非オブジェクトを返すように呼び出そうとしているセレクタの場合、コンパイラ機能が警告を無視することを可能にすることができますが、それは危険かもしれません。 Clangが、ローカル変数に割り当てられていない戻り値を処理する方法を何度か繰り返して見ました。 ARCを有効にしても、methodForSelector:から返されるオブジェクト値を保持したり解放したりできないという理由はありません。コンパイラの観点からは、結局のところオブジェクトです。つまり、呼び出しているメソッドsomeMethodが非オブジェクト(voidを含む)を返している場合、ガベージポインタ値が保持/解放されてクラッシュする可能性があります。

追加の引数

考慮すべき点の1つは、これはperformSelector:withObject:でも同じ警告が発生し、そのメソッドがパラメータをどのように消費するかを宣言しないことで同様の問題が発生する可能性があることです。 ARCは 消費されたパラメータ を宣言することを可能にし、もしメソッドがパラメータを消費するなら、あなたはおそらくメッセージをゾンビに送ってクラッシュするでしょう。ブリッジキャストでこれを回避する方法はいくつかありますが、実際には上記のIMPおよびfunction pointerメソドロジーを単に使用する方が良いでしょう。消費されたパラメータがめったに問題にならないので、これは思いつかないでしょう。

静的セレクタ

興味深いことに、コンパイラは静的に宣言されたセレクタについて文句を言うことはありません。

[_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. SwiftはperformSelector:にアクセスできないし、これらのメソッドを "本質的に安全でない"と記録しています

名前付きセレクタに基づいてメッセージを送信するという考えは、「本質的に安全でない」機能ではありません。このアイデアは、Objective-Cや他の多くのプログラミング言語で長い間成功しています。


1 すべてのObjective-Cメソッドには、メソッドを呼び出すときに暗黙的に追加されるself_cmdの2つの隠し引数があります。

2 NULL関数を呼び出すことは、Cでは安全ではありません。コントローラの存在をチェックするために使用されるガードは、オブジェクトがあることを保証します。したがって、私たちはmethodForSelector:からIMPを取得することを知っています(_objc_msgForward、メッセージ転送システムへのエントリかもしれません)。基本的には、ガードを設置した状態で、呼び出す機能があることがわかります。

3 実際、オブジェクトをidとして宣言し、すべてのヘッダをインポートしていないと、間違った情報が得られる可能性があります。あなたは、コンパイラが問題ないと思うコードでクラッシュすることになるかもしれません。これは非常にまれですが、起こる可能性があります。通常、2つのメソッドシグネチャのどちらから選択するのかわからないという警告が表示されるだけです。

4 詳細については、 保持された戻り値 および 保持されていない戻り値 に関するARCの参照を参照してください。

1184
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マクロシステムを使用してプラグマを非表示にしたい場合は、警告を抑制しやすくするためにマクロを定義できます。

#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]
);
1178
Scott Thompson

これについての私の推測はこれです:セレクタはコンパイラに知られていないので、ARCは適切なメモリ管理を強制することができません。

実際、メモリ管理が特定の規約によってメソッドの名前に結び付けられていることがあります。具体的には、 便利なコンストラクタ vs. make メソッドを考えています。前者は慣例により自動解放されたオブジェクトを返します。後者は保持されたオブジェクトです。規約はセレクタの名前に基づいているため、コンパイラがセレクタを認識しないと、適切なメモリ管理規則を適用できません。

これが正しければ、メモリ管理に関して問題がないことを確認していれば、コードを安全に使用できると思います(たとえば、メソッドが割り当てたオブジェクトを返さないなど)。

208
sergio

あなたのプロジェクトで ビルド設定 その他の警告フラグ WARNING_CFLAGS)の下に追加
-Wno-arc-performSelector-leaks

呼び出しているセレクターによって、オブジェクトが保持またはコピーされないようにしてください。

120
0xced

コンパイラが警告の上書きを許可するまでの回避策として、ランタイムを使用できます。

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

の代わりに

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

あなたがする必要があります

#import <objc/message.h>
111
jluckyiv

Performセレクタがあるファイルでのみエラーを無視するには、次のように#pragmaを追加します。

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

これはこの行の警告を無視しますが、それでもプロジェクトの残りの部分では許可します。

88
Barlow Tucker

奇妙だが真:許容できる場合(すなわち、結果が無効で、ランループサイクルを一度実行させてもかまわない場合)、これがゼロであっても遅延を追加します。

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

これは、おそらくオブジェクトを返すことができず、どういうわけか誤って管理されていることをコンパイラに保証するためであるため、警告が削除されます。

68
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]
);
34
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とは異なり、これはどのメソッドでも機能します。

31
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
20
Chris Prince

後世のために、私は帽子を輪に投げることにしました:)

最近、プロトコル、ブロックなどの理由で、target/selectorパラダイムからの再構築がますます増えています。ただし、performSelectorの代わりにドロップインを1つ使用しています。 :

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

これらは、objc_msgSend()を使って大したことをしなくても、performSelectorをきれいに、ARCセーフに、そしてほぼ同じに置き換えるようです。

ただし、iOSにアナログがあるかどうかはわかりません。

16
Patrick Perini

このスレッド でMatt Gallowayが答えた理由

次の点を考慮してください。

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

では、最初のユーザーが保持カウント1のオブジェクトを返し、2番目のオブジェクトが自動解放されたオブジェクトを返すことをARCはどうやって知ることができるでしょうか。

戻り値を無視している場合は、一般に警告を抑制するのが安全であるようです。 "do do do"以外の方法でperformSelectorから保持オブジェクトを取得する必要がある場合、そのベストプラクティスがどうなるかわかりません。

15
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を実行すると、次の図が見えます。 enter image description here ...それ以外の場合はメモリリークはありません。 enter image description here

14
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];
                )
6
Ben Flynn

警告を抑制しないでください。

コンパイラをいじくり回すための12以上の代替ソリューションがあります。
最初の実装時点で巧妙になっている間は、あなたの足跡をたどることができる地球上のエンジニアはほとんどいないので、このコードは最終的には壊れます。

安全なルート:

あなたの最初の意図からのある程度の変動で、これらの解決策はすべてうまくいくでしょう。必要ならば、paramnilにすることができます。

安全な経路、同じ概念上の動作:

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

安全な経路、やや異なる動作:

this response)を参照
[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]];

危険なルート

ある種のコンパイラによるサイレンシングが必要で、これはbreakにバインドされています。現在のところ、Swiftではdid breakになっています。

// AT YOUR OWN RISK
[_controller performSelector:selector];
[_controller performSelector:selector withObject:anArgument];
[_controller performSelector:selector withObject:anArgument withObject:nil];
5
SwiftArchitect

ARCを使用しているため、iOS 4.0以降を使用している必要があります。これはあなたがブロックを使用できることを意味します。実行するセレクターを覚えているのではなくブロックを取った場合、ARCは実際に何が起こっているのかをよりよく追跡できるため、誤ってメモリーリークが発生する危険性を冒す必要はありません。

4
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];
    }
2
supersabbath

引数を渡す必要がない場合は、valueForKeyPathを使用するのが簡単な回避策です。これはClassオブジェクトでも可能です。

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