1. ホーム
  2. ios

[解決済み] performSelectorのセレクタが不明なため、リークが発生する可能性があります。

2022-03-16 15:56:04

質問

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 が応答する methodForSelector: を使用することもできますが class_getMethodImplementation を Objective-C ランタイムで使用することができます (プロトコルの参照だけがある場合に便利です。 id<SomeProto> ). これらの関数ポインタは IMP であり、単純な typedef ed関数ポインタ( id (*IMP)(id, SEL, ...) ) 1 . これは、実際のメソッドのシグネチャに近いかもしれませんが、必ずしも正確に一致するわけではありません。

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

ARCが戻り値として考慮するのは、本当に4つだけです。 4

  1. 非オブジェクト型は無視する ( void , int など)
  2. オブジェクトの値を保持し、使われなくなったら解放する(標準的な前提)。
  3. 使用されなくなった新しいオブジェクトの値を解放する(MODE) init / copy を持つファミリーまたはアトリビュート ns_returns_retained )
  4. 何もしない & 返されたオブジェクトの値はローカルスコープで有効であると仮定します(最も内側のリリースプールが枯渇するまで、属性は ns_returns_autoreleased )

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

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

追加引数

を使用した場合にも同じ警告が発生することを考慮しています。 performSelector:withObject: そのメソッドがどのようにパラメータを消費するかを宣言していないと、同様の問題が発生する可能性があります。ARCでは 消費されるパラメータ メソッドがパラメータを消費してしまうと、最終的にはゾンビにメッセージを送ってクラッシュしてしまうでしょう。ブリッジキャストでこれを回避する方法もありますが、実際には、単純に IMP と関数ポインタの方法論を紹介しました。消費されるパラメータが問題になることはほとんどないので、このような問題は発生しないと思われます。

静的セレクタ

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

[_controller performSelector:@selector(someMethod)];

この理由は、コンパイラがコンパイル時にセレクタとオブジェクトに関するすべての情報を実際に記録することができるためです。コンパイラは何も仮定する必要がないのです。(これは1年ほど前にソースを見て確認したのですが、今はリファレンスがありません)。

サプレッション

この警告を抑制することが必要であり、良いコードデザインであるような状況を考えようとすると、空白になってしまいます。どなたか、この警告の抑制が必要だった(そして、上記では適切に処理できない)経験があれば、教えてください。

もっと見る

を構築することが可能です。 NSMethodInvocation を使うこともできますが、そうすると入力が多くなり、速度も遅くなるので、そうする理由はあまりありません。

歴史

このとき performSelector: このメソッド群が最初にObjective-Cに追加されたとき、ARCは存在しませんでした。AppleはARCを作成する際に、名前付きセレクタで任意のメッセージを送信する際に、メモリをどのように処理すべきかを明示的に定義する他の手段を使うよう開発者を誘導する方法として、これらのメソッドに対して警告を発生させるべきであると判断したのです。Objective-C では、開発者は生の関数ポインタに C スタイルのキャストを使用することで、これを実現することができます。

Swiftの導入に伴い、Appleは が文書化されています。 performSelector: ファミリーのメソッドは本質的に安全でないため、Swift では使用できません。

時を経て、私たちはこのような経過をたどってきました。

  1. Objective-Cの初期バージョンでは performSelector: (手動メモリ管理)
  2. Objective-CでARCの利用を警告する performSelector:
  3. Swiftは performSelector: で、これらのメソッドは本質的に安全でないものとして文書化されています。

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


1 Objective-Cのメソッドはすべて、2つの隠し引数を持っています。 self_cmd は、メソッドを呼び出したときに暗黙のうちに追加されるものです。

2 を呼び出すと NULL コントローラの存在を確認するためのガードにより、オブジェクトが存在することが確認されます。そのため IMP から methodForSelector: (である可能性もありますが)。 _objc_msgForward メッセージ転送システムへのエントリー)。基本的に、ガードがあることで、呼び出すべき関数があることがわかる。

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

4 に関するARCのリファレンスを参照してください。 保持された戻り値 非保持復帰値 をご覧ください。