Hatena::Groupiphone-dev

iPhone will be interactive!

 | 

2008-10-31

NSInputStream を継承する方法 20:03  NSInputStream を継承する方法 - iPhone will be interactive! を含むブックマーク はてなブックマーク -  NSInputStream を継承する方法 - iPhone will be interactive!  NSInputStream を継承する方法 - iPhone will be interactive! のブックマークコメント

開発中に NSInputStream を継承する必要があったのだけど、次のように普通に継承して、メソッドをオーバーライドしても全然うまく動かない。


@interface CustomInputStream : NSInputStream {
}
@end

@implementation CustomInputStream

- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len {
	
	// ...
	
	return [super read:buffer maxLength:len];
}

@end

調べてみると、原因は Objective-C (Cocoa?) の「クラスクラスタ」というヤツのせいだった。こいつが相当にキモイ。メソッド呼び出しの角括弧以上にキモイ。というわけで以下に書いておく。

クラスクラスタ

すごく簡単に重要な所だけ言うと、「呼び出したコンストラクタが属するクラスと、実際にインスタンス化されるクラスが違う」ということが起こります。Cocoa では、NSString だとか NSArray だとか NSInputStream だとか、多くの公開されているクラス (インターフェイス) は実は抽象クラスで、インターフェイスのみが定義されており、これらのクラスに対して alloc して init すると、内部的に、状況にあった適切なプライベートに定義されているコンクリートクラスが選択され、そいつの方がインスタンス化されて返ります。

例えば、次のようにして NSInputStream を作って NSLog で見てみると、実際には NSCFInputStream というクラスのインスタンスであることが分かります。

NSInputStream* stream = [[NSInputStream alloc] initWithData:data]; // data は NSData*
NSLog(@"%@", stream);

こういった、パブリッククラス + 実際にインスタンス化されるプライベートなクラスの集合を、「クラスクラスタ」と呼ぶらしいです。

というわけで、NSInputStream を継承するには、実際のところ、NSCFInputStream の方を継承してやらないと、結局のところ、全く実装を引き継げないことになります。だけれども、NSCFInputStream は、非公開のクラスなので、当然のことながら継承する手段がありません。

じゃあ、委譲すれば良いんじゃね

継承出来ないなら、内部で NSInputStream のインスタンスを作って委譲してやればいいんじゃね、と思うのは至極当然の流れですね。ということで、やってみます。ここで、インターフェイスだけは統一したいので、CustomInputStream は NSInputStream から派生させます。

@interface CustomInputStream : NSInputStream {
	
	NSInputStream* stream;
}

- (id)initWithData:(NSData*)data;

@end

@implementation CustomInputStream

- (id)initWithData:(NSData*)data {
	
	if (self = [super init]) {
		stream = [[NSInputStream alloc] initWithData:data];
	}
	return self;
}

- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len {
	return [stream read:buffer maxLength:len];
}

- (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len {
	return [stream getBuffer:buffer length:len];
}

- (BOOL)hasBytesAvailable {
	return [stream hasBytesAvailable];
}

- (void)close {
	[stream close];
}

- (void)open {
	[stream open];
}

- (id)delegate {
	return [stream delegate];
}

- (void)setDelegate:(id)delegate {
	[stream setDelegate:delegate];
}

- (id)propertyForKey:(NSString *)key {
	return [stream propertyForKey:key];
}

- (BOOL)setProperty:(id)property forKey:(NSString *)key {
	return [stream setProperty:property forKey:key];
}

- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode {
	[stream scheduleInRunLoop:aRunLoop forMode:mode];
}

- (void)removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode {
	[stream removeFromRunLoop:aRunLoop forMode:mode];
}

- (NSError *)streamError {
	return [stream streamError];
}

- (NSStreamStatus)streamStatus {
	return [stream streamStatus];
}

- (void)dealloc {
	[stream release];
	[super dealloc];
}

@end

これで万事オッケーとか思いつつ、例えば NSURLRequest の setHTTPBodyStream: とかに食わせてやると、「_scheduleInCFRunLoop:forMode:」やらなんやら、謎のメソッドを呼び出そうとして、「そんなメソッドがない」というエラーが出ます。

プライベートメソッドへの応答

実は、「_scheduleInCFRunLoop:forMode:」などは、NSCFInputStream の方で定義されている、プライベート (というか internal?) メソッドなのです。そして、これに対する呼び出しにもちゃんと応答して委譲してやらなければなりません。

それには、対応するメソッドが自分のクラスに定義されていない場合に呼び出される、「fowardInvocation:」というメソッド (これは NSObject で定義されている) をオーバーライドして、委譲してやるようにするとうまくいきます。実装は次のような感じ。

- (void)forwardInvocation:(NSInvocation*)anInvocation {
	
	// stream で応答可能であれば委譲する
	if ([stream respondsToSelector:[anInvocation selector]]) {
		[anInvocation invokeWithTarget:stream];
	}
	else {
		[super forwardInvocation:anInvocation];
	}
}

- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector {
	
	NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
	
	// 自分になければ stream の方から持ってくる
	if (signature == nil) {
		signature = [stream methodSignatureForSelector:aSelector];
	}
	
	return signature;
}

「forwardInvocation:」だけでなく、「methodSignatureForSelector:」もオーバーライドして、そういうメソッドがあることを伝えるようにしなければなりません。

これでやっと、めでたく CustomInputStream がうまく動くようになります。何でクラス一個継承するのにこんな大変な思いをしなければならないんでしょう。ますます ActionScript が好きになる一方です (まさかの締め)。

ちなみに、これよりもっと頭いい方法があれば教えてください。

saqooshasaqoosha2008/11/01 16:48何をしたいかにもよるとけど、カテゴリ使うのが楽なんじゃないかしら。

http://journal.mycom.co.jp/column/objc/008/index.html
"メソッドの追加によるクラスの拡張 " のとこ。

yossy44yossy442008/11/02 13:13カテゴリだと、新しいメンバ変数作れなかったり、元のメソッド呼べなかったりで要件を満たせなかったんですじゃ。

takuma104takuma1042008/11/06 01:22むむ、試してはないのですが、リファレンスによると

> To create a subclass of NSInputStream you may have to implement initializers
> for the type of stream data supported and suitably reimplement existing initializers.
> You must also provide complete implementations of the following methods:
> read:maxLength:
> getBuffer:length:
> hasBytesAvailable

な記述がありました。ちょっとこのNSInputMethodクラスは特殊なんでしょかね。

takuma104takuma1042008/11/06 01:23NSInputMethodってイミフですね。。NSInputStreamです。

 |