Edited at

動的にサブクラスを登録しインスタンスを生成する

More than 5 years have passed since last update.

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

別記事にてBlockで動的にメソッドを実装できるクラスを作りました。このクラスに


  • 動的にサブクラスを登録する

  • サブクラスのインスタンスを生成する

  • インスタンス解放時にサブクラスを破棄する

  • インスタンス解放時に動的に実装したメソッドのBlockを解放する

という機能を追加します。


何故必要か?

別記事のSIAExpandableObjectは動的にメソッドを実装できるクラスです。そして メソッドはインスタンスではなくクラスに実装されます。つまり複数の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回表示されます。

この問題はSIAExpandableObjectのサブクラスを作ることで解決できます。サブクラスAにメソッドを追加してもサブクラスBには影響がないからです。しかし、せっかく動的にメソッドを実装できるようにしたのに、サブクラスを静的に定義するのは中途半端です。そこで動的にサブクラスを作れるようにSIAExpandableObjectを変更します。


サブクラスのインスタンス生成


runtime関数

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

#import <objc/runtime.h>

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

/*

親クラスとクラス名を指定してサブクラスを作る。
extraBytesは通常0を指定する。(多分0以外だと余分にメモリ領域を確保するようになるはず……)
*/
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes);

// クラスを登録する。runtimeがクラスを認識できるようになります。
void objc_registerClassPair(Class cls);


SIAExpandableObjectの改造

SIAExpandableObjectにサブクラスのインスタンスを返すメソッドを追加します。


SIAExpandableObject.h

@interface SIAExpandableObject : NSObject

+ (instancetype)createSubclassInstance;

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

@end


まずはサブクラスを登録します。


SIAExpandableObject.m

+ (Class)registerSubclass

{
NSString *subclassName = [NSString stringWithFormat:@"%@/%@", NSStringFromClass(self), SIACreateUUID()];
Class subclass = objc_allocateClassPair(self, subclassName.UTF8String, 0);
objc_registerClassPair(subclass);
return subclass;
}

クラス名とUUIDを組み合わせた名前のサブクラスを作り、それを登録しています。SIACreateUUIDはCFUUIDCreateを使ってUUIDのNSStringを返しています。何らかの方法でユニークな名前を作ることができれば問題ありません。

次にサブクラスのインスタンスを生成します。


SIAExpandableObject.m

+ (SIAExpandableObject *)createSubclassInstance

{
Class subclass = [self registerSubclass];
id object = [[subclass alloc] init];
return object;
}

registerSubclassで取得したclassをalloc/initしてインスタンス化します。


サブクラスと動的に実装したメソッドのBlockの解放


runtime関数

// クラスを破棄する。

void objc_disposeClassPair(Class cls);

登録したメソッドのBlockを解放するには以下を使います。

// クラスからメソッド一覧を取得する。

Method * class_copyMethodList(Class cls, unsigned int *outCount);
IMP method_getImplementation(Method method);

// imp_implementationWithBlockで指定したBlockを解放します。
BOOL imp_removeBlock(IMP anImp);


解放処理の実装


SIAExpandableObject.m

- (void)dealloc

{
Class class = self.class;
if (class != [SIAExpandableObject class]) {
dispatch_async(dispatch_get_main_queue(), ^{
objc_disposeClassPair(class);
});

unsigned int count = 0;
Method *methodList = class_copyMethodList(self.class, &count);
for (unsigned int i = 0; i < count; i++) {
Method method = methodList[i];
IMP imp = method_getImplementation(method);
imp_removeBlock(imp);
}
free(methodList);
}
}


deallocが抜けた後はselfは存在しないため、最初にself.classを別変数に確保しています。

SIAExpandableObjectクラスではない(≒継承したクラス)場合にclassを破棄します。deallocが動いている間はまだclassが存在するので、gcdを使い後で破棄するように登録します。

次にメソッド一覧を取得(コピー)します。一覧のひとつひとつのMethodからIMPを取得し、そのIMPのBlockを解放します。

ここで取得(コピー)したメソッド一覧はfreeで解放する必要があります。


使い方

SIAExpandableObject(とそのサブクラス)は必ず継承したクラスのインスタンスを使う想定としています。

    SIAExpandableObject *object1 = [SIAExpandableObject createSubclassInstance];

SIAExpandableObject *object2 = [SIAExpandableObject createSubclassInstance];
NSLog(@"%@", NSStringFromClass(object1.class));
NSLog(@"%@", NSStringFromClass(object2.class));

[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"];

createSubclassInstanceを使用しているため、object1とobject2のクラスは異なります。異なるclassのインスタンスメソッドをBlockで実装することになるため、object1とobject2は同名のメソッドを実装しても問題なくそれぞれ別個に動作します。


使用例

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

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