Edited at

Blockで動的にメソッドを実装できるクラスの作り方

More than 5 years have passed since last update.

========================================

Objective-Cのランタイムの機能を使うと、プログラムの実行中にクラスにインスタンスメソッドを追加することが可能です。

この機能を使って、メソッド名(SEL)とメソッド本体(Block)と型情報を渡すとメソッドとして実装してくれるクラスを作成します。

ただしこれだけだと実は使い勝手が悪いため(理由は後述)、別記事で動的にサブクラスを登録しインスタンスを生成する機能を追加します。

また動的に実装したメソッドを削除する機能もありません。これも別記事で一緒に解決します。


Objective-Cのメソッド


メソッドの本体はC言語関数

Objective-Cのメソッドの本体はIMP型、要するにC言語の関数となっています。

- (void)methodWithParameter:(NSString *)parameter

というメソッドを

    [self methodWithParameter:@"abc"];

と呼び出した場合、ランタイムは

void function(id self, SEL _cmd, NSString *parameter)

という型のC言語関数を呼び出します(関数名は適当)。この関数の第一引数 self や 第二引数 _cmd はデバッガで適当なメソッド内で停止させると確認できます。下は application:didFinishLaunchingWithOptions: が呼ばれた時のデバッガのスクリーンショットです。

メソッド本体のC言語関数は(メソッドによって引数が異なるはずですが)IMP型と呼ばれるようです。

http://news.mynavi.jp/column/objc/022/


メソッドの型情報

SEL型はメソッドの名前を表します。例えば @selector(application:didFinishLaunchingWithOptions: とするとSEL型の値を取得できます。

このSEL型は名前だけで、型情報(戻り値の型と引数の型)が含まれていません。型情報は内部的にはC言語文字列で表されます。

メソッドに対応するIMPが以下の場合

void function(id self, SEL _cmd, NSString *parameter)

型は v0@0:0@0 となります。(数字は適当です)

voidなら'v'、idなら'@'というように対応する文字が決められていて、これは @encode(void) 等とすれば取得できます。そして型と一緒に型のサイズを記載しますが、現在はその数字は使われないらしく 0 としておけば問題ありません。(runtimeから取得した場合は 0 以外の場合もあるため、解析するならきちんとみないとだめです)

型とサイズのペアを順番に並べたもの(戻り値型、第一引数、第二引数……の順番)がそのメソッドの型となります。


Blockでメソッドを実装するクラス


runtime関数

runtime関数を使う場合以下のファイルをインポートします。

#import <objc/runtime.h>

今回は主に以下の関数を使用します。

// blockを呼び出してくれるIMPを返す。

IMP imp_implementationWithBlock(id block);

/*
クラスにメソッドを追加する。
NSObjectのインスタンスにメソッドを追加したい場合はclsに[NSObject class]を渡す
メソッドの情報として、メソッド名(name)とIMP(imp)と型情報(types)を指定する。
*/
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);


Blockの引数型の注意点

BlockはIMPの代わりのような役割を果たします。戻り値の型はIMP型と同じになります。ただし IMP型の第二引数SELはBlockには渡されません。以下のようなIMPを想定する場合

void function(id self, SEL _cmd, NSString *parameter)

Blockの型は以下のようになります。

void (^)(id self, NSString *parameter)

また class_addMethod のtypesには "v0@0:0@0" を渡すのが正式かと思うのですが(多分) "v0@0@0" を渡してもBlockは正しく呼ばれるようです。


動的なメソッドの実装

動的にメソッドを実装できるSIAExpandableObjectを実装してみます。

メソッドの実装にはメソッド名(SEL)と本体(IMP)と型情報(char *)が必要ですが、IMPは imp_implementationWithBlock で取得するので メソッド名(SEL)型情報(char *)Block を引数で受け取ります。


SIAExpandableObject.h

@interface SIAExpandableObject : NSObject

- (void)addMethodWithSelector:(SEL)selector types:(const char *)types block:(id)block;

@end



SIAExpandableObject.m

@implementation SIAExpandableObject

- (void)addMethodWithSelector:(SEL)selector types:(const char *)types block:(id)block
{
IMP oldImp = class_getMethodImplementation(self.class, selector);
if (oldImp != NULL) {
imp_removeBlock(oldImp);
}

IMP imp = imp_implementationWithBlock(block);
class_addMethod(self.class, selector, imp, types);
}

@end


特に説明していないruntime関数を使っていますが、同じメソッド名(SEL)で2回以上呼ばれた場合に古いものの後始末をしています。

あとはruntime関数を使いIMPを取得し自身のclassにメソッドを追加すればOKです。


このクラスの問題点

実は今回作成したクラスはそのままだと使い勝手が悪いです。というのも メソッドはインスタンスではなくクラスに実装されます。つまり SIAExpandableObjectに同名のメソッドを実装すると、後から実装したほうで上書きされてしまいます

    SIAExpandableObject *object1 = [[SIAExpandableObject alloc] init];

SIAExpandableObject *object2 = [[SIAExpandableObject alloc] init];

[object1 addMethodWithSelector:@selector(test:) types:"v0@0:0@0" block:^ (SIAExpandableObject *object, NSString *string){
NSLog(@"object1#test %@", string);
}];
[object2 addMethodWithSelector:@selector(test:) types:"v0@0:0@0" block:^ (SIAExpandableObject *object, NSString *string){
NSLog(@"object2#test %@", string);
}];

[object1 performSelector:@selector(test:) withObject:@"1"];
[object2 performSelector:@selector(test:) withObject:@"2"];

object1とobject2にそれぞれ別のメソッドが実装されて欲しいところですが、上記を実行すると object2#test のNSLogが2回表示されます。

この対策として別記事で動的なサブクラスの登録とインスタンス生成、サブクラスの解除を実装します。

さらにもう一点、この機能で実装したメソッドを消す機能を作っていません。よっていつまでもBlockがメモリ上に残ることになり、Block内から外部のオブジェクトを参照しているとそのオブジェクトはリークしてしまいます。もちろん__weakを使えば(Block本体は置いておいて)オブジェクトのリークは防げますが、この部分も別記事で解決を目指します。


使用例

この記事はBlocksKitもどきの作り方の一部として書いています。興味のあるかたはこちらも御覧ください。

この記事の機能の利用例となっています。