任意のメソッドを 3 つ以上の引数を添えて実行したい場合 : Objective-C プログラミング
PROGRAM
任意のメソッドを 3 つ以上の引数を添えて実行したい場合
Objective-C では "performSelector" メソッドを使用することで、オブジェクト内の任意のメソッドを簡単に呼び出すことができます。"performSelectorInBackground" などで簡単に、別スレッドでメソッドを実行できるところも魅力です。
ただ、標準の "performSelector" メソッドでは、最大で 2 つまでしかメソッドを指定することができないので、それ以上の引数を取る場合には、"NSInvocation" クラスを使って、メソッドを呼び出す必要があります。
たとえば、とあるオブジェクト "argTarget" のメソッド "argSelector" を任意の数の引数 "argWithObjects" を添えて実行するメソッドを次のように定義してみます。
// "target" のメソッド "selector" を任意の数の引数 "withObjects" を添えて実行するメソッドです。
- (NSInvocation*)performSelector:(SEL)argSelector target:(id)argTarget withObjects:(NSArray*)argWithObjects
{
NSInvocation* invocation;
NSMethodSignature* signature;
// メソッドの実行に必要な署名を取得します。
signature = [argTarget methodSignatureForSelector:argSelector];
// 先ほどの署名を使って、メソッドの実行と戻り値取得のための NSInvocation クラスを用意します。
invocation = [NSInvocation invocationWithMethodSignature:signature];
// Invocation に、実行したいメソッドを持つオブジェクトと、そのセレクタを設定します。
[invocation setTarget:argTarget];
[invocation setSelector:argSelector];
// 渡された引数の全てを、それを Invocation に登録します。
for (id object in argWithObjects)
{
// atIndex の 2 番から順に、Invocation に引数を登録しています。
[invocation setArgument:&object atIndex:(index + 2)];
}
// Invocation に登録したメソッドを実行します。
[invocation invoke];
// ここでは、戻り値の処理を呼び出し元に委ねるため、invocation をそのまま返しています。
return invocation;
}
このようなメソッドを用意することで、引数を好きな数だけ NSArray に格納して渡すことで、それら全てをメソッドに渡して実行できます。
マルチスレッドで invoke を実行する際の注意点
ここでの注意事項として、NSInvocation では "setArgument:atIndex:" で登録する引数は、プリミティブ型や構造体はセットした時点で値がコピーされるのですけど、Objective-C インスタンスは retain されません。
そのため、performSelectorInBackground: などでタイミングをずらして invoke メソッドを実行しようとした場合には、"argWithObjects" で渡した引数が、何も配慮しないでいると invoke 実行時に解放されてしまう可能性があります。
NSInvocation では、引数として設定された Objective-C インスタンスを retain するメソッドが用意されています。
// 引数に設定されているもののうち、Objective-C インスタンスだけを retain します。
[invocation retainArguments];
これで、引数に指定された Objective-C インスタンスを retain してくれるようになります。
NSObject が提供している "performSelectorInBackground:withObject:" や "performSelector:withObject:AfterDelay:" などでは "withObject" で渡したオブジェクトが実行時まで自動的に retain されますが、これで丁度それと同じ状況になります。
なお、この retainArguments メソッドの呼び出しタイミングは、引数を設定する前でも設定した後でも大丈夫なようです。
retainArguments が呼び出された段階で、既に引数として指定されている Objective-C インスタンスは retain されて、これから新たに設定される Objective-C インスタンスについては、設定したタイミングで retain されるようになるようでした。
プリミティブ型の引数を取るメソッドの実行
NSInvocation を使うことで、引数として NSInteger や BOOL といったプリミティブ型や、構造体 (struct) を取るメソッドも実行できます。
実行方法は Objective-C インスタンスの時と同じで、setArgument:atIndex: メソッドで引数を設定するときに、プリミティブ型のポインターを渡すだけで大丈夫です。
たとえば、メインスレッド以外のスレッドから UITableView をスクロールさせたいとします。
このとき、UI 関連の処理はメインスレッドから実行しないといけない決まりになっているため、scrollToRowAtIndexPath:atScrollPosition:animated: メソッドを performSelectorOnMainThread:withObject:waitUntilDone: を使ってメインスレッドで実行する必要があります。
ただ、performSelectorOnMainThread:withObject:waitUntilDone: では、引数を 1 つしか取れないのと、その引数も id 型として渡す必要があるため、スクロールを行うメソッドを直接呼び出すことができません。
このようなときには、次のように NSInvocation を利用することで実行できるようになります。
// このような情報で self.tableView をスクロールしたいとします。
NSIndexPath* scrollIndexPath = [NSIndexPath indexPathForRow:5 inSection:0];
UITableViewScrollPosition scrollPosition = UITableViewScrollPositionTop;
BOOL scrollAnimated = YES;
// NSInvocation を準備します。
NSInvocation* invocation;
id target = self.tableView;
SEL selector = @selector(scrollToRowAtIndexPath:atScrollPosition:animated:);
NSMethodSignature* signature = [target methodSignatureForSelector:selector];
invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = target;
invocation.selector = selector;
// 今回はスレッドをまたぐため、引数に渡した Objective-C インスタンスが retain されるようにします。
[invocation retainArguments];
// NSInvocation に引数を設定して行きます。プリミティブ型もポインタとして渡せば大丈夫です。
[invocation setArgument:&scrollIndexPath atIndex:2];
[invocation setArgument:&scrollPosition atIndex:3];
[invocation setArgument:&scrollAnimated atIndex:4];
// invoke メソッドをメインスレッドで実行することで、NSInvocation に設定したメソッドをメインスレッドで実行できます。
[invocation performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:NO];
今回はスレッドをまたいで実行されるため、引数として指定した NSIndexPath がメインスレッドで実行される前に解放されないように、retainArguments を実行しておくのを忘れないようにします。
他の UITableViewScrollPosition や BOOL といったプリミティブ型は、setArgument:atIndex: ではポインタでセットしていますけど、NSInvocation は受け取ったそれをコピーして持ってくれるので、これらの値については、別スレッドでの実行だからといって特別な配慮は必要ありません。
ちなみに performSelector:withObject: メソッドでも、withObject: のところにしれっとプリミティブ型のデータを渡してあげると、試してみた感じでは正しく呼び出し先に受け渡されているようでした。
ただ、そもそも NSObject の performSelector:withObject: の withObject に渡す型は id と定義されていることもありますし、このときコンパイラが "Incompatible integer to pointer conversion sending 'int' to parameter of type 'id'" というような警告メッセージを表示するので、これで大丈夫という確証が余程ない限り、使ってはいけない気がします。
NSInvocation で実行したメソッドの戻り値について
NSInvocation で実行したメソッドの戻り値は、その NSInvocation インスタンスが持つ "methodSignature:" プロパティに格納されます。
このプロパティが持つ戻り値の値を取得したい場合には、NSInvocation のインスタンスに対して、次のようにします。
// 引数の "result" に戻り値が格納されます。"result" の型は呼び出したメソッドの戻り値に合わせます。
[invocation getReturnValue(void*)&result];
このように getReturnValue メソッドでは、void* 型として戻り値を扱う点に注意が必要です。
また、このとき NSString* などのオブジェクトが返されるような場合でも、既にそれが retain されたり autolerease 設定されていたりといった、呼び出し先のメソッドが返す状態に手を加えずに渡されるので、受け取った戻り値は、そのメソッドを直接呼び出したときと同じように扱えば問題ないようでした。
戻り値の型については、呼び出すメソッドによって決まってくるでしょうから、適切な型の変数を用意して受け取る形になります。
参考として、次のプロパティを参照することで、戻り値の型の詳細を知ることもできるようになっていました。
// 戻り値の種類を文字列で取得できます。
NSString* type = invocation.methodSignature.methodReturnType;
// 戻り値のサイズ(バイト数)を数値で取得できます。
NSInteger length = invocation methodSignature.methodReturnLength;
戻り値の種類と長さは、これを以って適切な受け皿を用意して保存するといったものでもないような気はしますけど、とりあえずどんなものが返ってくるのか、いくつかのデータ型で試してみると、次のような感じでした。
実際の戻り値の型 | methodReturnType | methodReturnLength |
---|---|---|
void | v | 0 |
NSString* | @ | 4 |
NSString* (nil を返した場合) | @ | 4 |
NSMutableString* | @ | 4 |
NSArray* | @ | 4 |
NSDictionary* | @ | 4 |
NSNull* | @ | 4 |
BOOL | c | 1 |
NSInteger | i | 4 |
NSUInteger | l | 4 |
NSNumber* | @ | 4 |
Float32 | f | 4 |
Float64 | d | 8 |
Float80 (struct) | {Float80=s[4S]} | 10 |
Float96 (struct) | {Float96=[2s][4S]} | 12 |
NSTimeInterval | d | 8 |
float | f | 4 |
double | d | 8 |
char | c | 1 |
unsigned char | C | 1 |
short | s | 2 |
unsigned short | S | 2 |
int | i | 4 |
unsigned int | I | 4 |
long | l | 4 |
unsigned long | L | 4 |
long long | q | 8 |
unsigned long long | Q | 8 |
int64_t | q | 8 |
void* | ^v | 4 |
int* | ^i | 8 |
id | @ | 4 |
id* | ^@ | 4 |
この感じから、たとえば signed と unsigned との間には、種類として表現する文字が、英語の小文字か大文字かという違いで現れてくるような様子ですね。また、BOOL と char はともに "c" が取得されるようなので、この情報で区別することはできなそうです。
オブジェクトは、どの型であれ一律に "@" が返される感じです。
また、構造体については、構造体名に続いて、"(配列の場合は要素数)型を意味する文字" というような表現になっているような感じでした。ポインターを返す場合には、型を意味する文字の先頭に "^" が付けられる感じです。
[ もどる ]