Objective-C

メッセージの転送を理解する

More than 3 years have passed since last update.

objective-cは、オブジェクトのメソッド呼び出しをランタイムが仲介するメッセージ式と呼ばれる方法で実現している。

以下に示すようなクラス定義を想定する。

Foo.h
#import <Foundation/Foundation.h>

@interface Foo : NSObject
-(void)bar;
@end
Foo.m
#import "Foo.h"

@implementation Foo
-(void)bar
{
    NSLog(@"bar method is called");
    return;
}

@end

当たり前だがインスタンスを生成して
[foo bar]
とすればメソッド呼び出しが可能。

ここでクラス定義に存在しないメソッド例えばfoobarを呼び出すとする。
[foo foobar]として呼び出そうとするとコンパイラから怒られるが
objc_msgSend関数を使うとメッセージを送信することができる。
objc_msgSend(foo,@selector(foobar));
この時コンパイルエラーにはならないが実行すると
"unrecognized selector sent to instance 0xxxxxx"
要するに、よくわからないセレクタ(メソッドの呼び出し情報)がfooインスタンスに送られてるよ
というエラーが発生してアプリがハングする。

つまり、実行時にメッセージを解析してメソッドが存在するかチェックをしているという仕組みに
なっている。(こういうのを動的結合という。)

オブジェクトに送信されたメッセージは該当するメソッドが見つからないと
+(BOOL)resolveInstanceMethod:(SEL)sel
-(id)forwardingTargetForSelector:(SEL)sel
-(void)forwardInvocation:(NSInvocation *)anInvocation
という順番でメッセージが転送されていく。

実験のためにFoo.mを以下の用にしてみる。

Foo.m
#import "Foo.h"
#import <objc/runtime.h>

@implementation Foo


+(BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"resolveInstanceMethod:'%@'", NSStringFromSelector(sel));
    class_addMethod([self class], sel, (IMP)newInstanceMethod, "v@:");
    return YES;
}

void newInstanceMethod(id self, SEL _cmd) {
    NSLog(@"new instance method is called! sel:'%s'",_cmd);
}

-(void)bar
{
    NSLog(@"bar function");
    return;
}
@end

ここで
objc_msgSend(foo,@selector(foobar));
を実行すると
+(BOOL)resolveInstanceMethod:(SEL)sel
が呼び出されることが確認できる。

その後はここではランタイム関数のclass_addMethodを利用して
動的にクラス定義にセレクタに対応する関数の追加を行ってみた。
(コンパイル時に定義されていないメソッドを実行時にクラスに追加)

objective-cでは動的結合でメソッドの呼び出しを行っている仕組み上
このように動的にメソッドを追加することも可能。

ちなみに

    objc_msgSend(foo,@selector(foobar));
    objc_msgSend(foo,@selector(foobar));

のように連続してメッセージを送信してみると分かるが、2度目のメッセージ送信では
+(BOOL)resolveInstanceMethod:(SEL)sel
の呼び出しは行われない。
つまり、クラス定義にfoobarメソッドが動的に追加されたままになっていることがわかる。
(対象のオブジェクトだけにメソッドが追加されている訳ではないことに注意)

クラスにメソッドが追加されていることを確かめるために上記のメソッド追加が行われた後に
インスタンスを新しく作ってメッセージを送ってみる。
id bar = [[Foo alloc] init];
objc_msgSend(bar,@selector(foobar));
このときも同様にメッセージ転送されることなくメソッド呼び出しが行われる。
つまり、クラス自体にメソッドが追加されている。

参考までに検証に用いたmain.mを以下に置いときます。

main.m
#import <Foundation/Foundation.h>
#import "Foo.h"

int main(void)
{
    id foo = [[Foo alloc] init];
    [foo bar];

    objc_msgSend(foo,@selector(foobar));
    objc_msgSend(foo,@selector(foobar));

    id bar = [[Foo alloc] init];
    objc_msgSend(bar,@selector(foobar));

    return 0;
}