iOS Advent Calendar 2013 14日目担当の @ikesyo です。
- Function Reactive Programming Framework - Reactive Cocoa | Cocoaの日々情報局
- iOS - ReactiveCocoaについて - Qiita [キータ]
- ReactiveCocoaのまとめ資料 - Qiita [キータ]
- Objective-C - ReactiveCocoaに出てくる用語の整理 - Qiita [キータ]
- [iOS]ReactiveCocoaFramework入門 | アドカレ2013 : SP #11
といった感じで今年1年で大分有名になってきた感がある ReactiveCocoa というリアクティブプログラミングライブラリーがあります。
このエントリーではその中でもObjective-CのランタイムAPIの利用箇所について解説してみたいと思います。
ランタイムAPI
まずObjective-CのランタイムAPIですが、大変有名なダイナミックObjective-Cでも紹介されているように、<objc/runtime.h>
に定義されている、Objective-Cの言語機能を実現しているCの関数郡となります。
代表的には動的なメソッド定義や実装の追加、既存メソッドの実装の差し替え、動的なクラス定義などができ、比較的よく使用されているのがobjc_setAssociatedObject()
/ objc_getAssociatedObject()
によるカテゴリーでのプロパティ実装だと思います。これはReactiveCocoaでも既存クラスの拡張で広く使用されています。
ReactiveCocoaの場合
ReactiveCocoaでのランタイムAPIの使用例として-[NSObject rac_signalForSelector:]
および -[NSObject rac_signalForSelector:fromProtocol:]
を取り上げます。これらのメソッドは、引数のセレクターのメソッドが実行された際に、メソッドの引数を要素に持つRACTuple
を値としてnext
を送信するシグナルを返します。
- 既存メソッドのフック
@optional
のデリゲートメソッドに対して使用することでデリゲートパターンを容易にシグナルの世界に持ち込める
といった用途に使用することができる、非常に強力なメソッドです。
このメソッドの実装には以下のように多数のランタイムAPIの機能が使われています。
- クラス生成
- インスタンスのクラス変更
- メソッド生成
- インスタンスメソッドの追加
- インスタンスメソッドの差し替え
- メッセージフォワーディング
以下のようなコードの時、
NSObject *object = [[NSObject alloc] init];
RACSignal *signal = [object rac_signalForSelector:@selector(description)];
[signal subscribeNext:^(RACTuple *args) {
// -description は引数がないため、`args`は空のタプル。
NSLog(@"-description invoked.");
}];
[object description];
概要を説明すると内部では以下のような処理が行われています。
- 対象インスタンス(メッセージのレシーバー)のクラスのサブクラスを動的に生成し、
-forwardInvocation:
,-respondsToSelector:
の実装を差し替える。-
NSObject_RACSelectorSignal
クラスが生成される。
-
- 対象インスタンスのクラスを 1. で動的生成したサブクラスに置き換える。
-
[object class]
がNSObject
ではなく、NSObject_RACSelectorSignal
を返すようになる。
-
-
RACSubject
(値を自由に送れるRACSignal
のサブクラス)をAssociated Objectとして保持する。 - 以下の2パターン。
- 対象のメソッド(セレクター)がオリジナルのクラスに実装されていない場合、
_objc_msgForward()
を実装とするメソッドを追加する。 - 対象のメソッド(セレクター)がオリジナルのクラスに実装されている場合、既存メソッドの実装をプレフィックス付きの別名セレクターに退避した上で、対象セレクターに紐づく実装を
_objc_msgForward()
に差し替える。-
@selector(description)
=>_objc_msgForward()
-
@selector(rac_alias_description)
=> オリジナルの実装
-
- 対象のメソッド(セレクター)がオリジナルのクラスに実装されていない場合、
- 対象メソッドを実行する。
[object description]
- (
-respondsToSelector:
の置き換えは説明省略。) -
[object description]
のメッセージ送信は実態として_objc_msgForward()
が実行され、その結果として-forwardInvocation:
が実行される。 -
-forwardInvocation:
の置き換えられた実装の中で、オリジナルの実装を実行した後、保持しているRACSubject
に対して-sendNext:
を行う。
では該当のソースファイルとなるNSObject+RACSelectorSignal.m(v2.1.8時点)を見てみましょう。
クラス生成
objc_allocateClassPair()
objc_registerClassPair()
を使用し、対象インスタンスのクラスをスーパークラスとするサブクラスを動的に生成し、ランタイム上に登録しています。クラス名として、オリジナルのクラス名に _RACSelectorSignal というサフィックスを付加した名前にし、衝突を回避するようにしています。
インスタンスのクラス変更
L259では
object_setClass(self, subclass)
を使用し、対象インスタンスのクラスを動的生成したサブクラスに置き換えています。こんなことまで出来て気持ち悪いですね!!
メソッド生成、インスタンスメソッドの差し替え
1.の-forwardInvocation:
, -respondsToSelector:
の実装差し替えのために、
L58-L117では
-
imp_implementationWithBlock()
によるメソッドの実装生成 -
class_replaceMethod()
によるメソッドの実装の差し替え
を行っています。
imp_implementationWithBlock()
の引数のブロックは、method_return_type ^(id self, self, method_args …)
というシグネチャが要求されるものになっています。-respondsToSelector:
の場合は以下のようになりますね。
BOOL (^newRespondsToSelector)(id, SEL) = ^ BOOL (id self, SEL selector) {
return // some bool value;
};
インスタンスメソッドの追加・差し替え
4.の処理としてL154-L195では
- 既存メソッドがない場合:
class_addMethod()
によるインスタンスメソッドの追加 - 既存メソッドがある場合:
-
method_getImplementation()
による既存メソッドの実装の取得 -
class_addMethod()
による既存メソッドの実装の別セレクターへの退避 -
class_replaceMethod()
によるメソッドの実装の差し替え
-
を行っています。
既存メソッドに対しては、@selector(rac_alias_someSelector:)
のように rac_alias_ をプレフィックスとしたセレクターを生成し、それとmethod_getImplementation()
で取得したオリジナルの実装を紐付けています。
ここでメソッドの差し替え後の実装としている関数の_objc_msgForward()
が全体のキモとなっている部分で、メソッドの実装がこの関数となっていると、メソッド実行に関する情報(メッセージのレシーバー、セレクター、引数、実行後の戻り値など)がNSInvocation
のインスタンスとして生成され、それを引数として-forwardInvocation:
が実行されます。
メッセージフォワーディング
-forwardInvocation:
の置き換え後の実装の中身の一部では、シグナルの値送信の前のオリジナルの実装の実行のためにNSInvocation
を使用しています。
-rac_signalForSelector:
に_objc_msgForward()
=> -forwardInvocation:
というメッセージフォワーディングの仕組みが利用されているのは、このオリジナルの実装を実行するためです。
引数として渡されたinvocationオブジェクトの.selector
はオリジナルのセレクターを示していますが、このままでは実装が_objc_msgForward()
となってしまうので、4.でオリジナルの実装を退避していた別名セレクター(@selector(rac_alias_hogehoge)
)を.selector
にセットして[invocation invoke];
とすることで、メソッドの戻り値型や引数の型・数を意識することなくオリジナルの実装の実行が完了します。
ただし、メソッドの戻り値が構造体の場合、構造体のサイズによって_objc_msgForward()
と_objc_msgForward_stret()
が本来使い分けられないといけないのですが、それを判断するのは難しいことから-rac_signalForSelector:
では構造体を戻り値とするメソッドはサポートされていません。
KVOという例外
上記が基本的なロジックですが、例外パターンが存在します。それは-addObserver:forKeyPath:options:context:
によってKVO(Key-value observing: キー値監視)の対象となったインスタンスです。ReactiveCocoa的には、RACObserve()
マクロを使用したインスタンスも内部ではKVOを使用しているため同様となります。
実はKVOも上記のような動的サブクラス生成・インスタンスのクラス変更を行っており、KVOの対象となったインスタンスはランタイム上で NSKVONotifying_ というプレフィックスが付いたクラスになっています。また-class
メソッドがオリジナルのクラスを返すようにオーバーライドされています。
ReactiveCocoaでは動的サブクラスの更なるサブクラス化は避けるために、1.の処理の際にL219-L245で
-
-class
によるクラス取得 -
object_getClass()
によるクラス取得
の結果を比較し、異なるクラスであればKVOなどによる動的サブクラス化されているインスタンスだと判断をして-forwardInvocation:
, -respondsToSelector:
の実装差し替えだけを行うようにしています。
また、KVOサブクラスによる-respondsToSelector:
は-class
で返されるオリジナルのクラスに対する+instancesRespondToSelector:
に基づいているようで、そのままでは-rac_signalForSelector:
によってKVOサブクラス側に実装されたセレクターには反応してくれないため、それを回避するように実装を差し替えています。
まとめ
カテゴリーや、上記のようなランタイムAPIを使用したObjective-Cの動的特性は非常に強力ですが、こういった処理を行うライブラリーを複数組み合わせてしまうとデバッグが難しくなったり実装の衝突で大変なことになってしまうことも否定できません。
黒魔術は用法・容量を守って正しくお使いください!!
参考リンク
メッセージフォワーディング
- Hamster Emporium: [objc explain]: objc_msgSend_stret
- Wincent Colaiuta's weblog: More than I ever wanted to know about Apple's Objective-C runtime
- A Look Under the Hood of objc_msgSend() - Extra Cookies
- mikeash.com: Friday Q&A 2009-03-27: Objective-C Message Forwarding
KVO
関連するIssues / Pull Requests
- Obtain a signal for any arbitrary method invocation
- Added -rac_signalForSelector:
- Add NSObject -rac_signalForSelectorInvocation:
- Signal for selector using forwarding
- Signal for selector using forwarding 2.0
- -rac_signalForSelector: the third
- -rac_signalForSelector:fromProtocol:
- -rac_signalForSelector:fromProtocol: & refactored UIKit signals
- Fix -rac_signalForSelector: doesn't invoke original method on previously KVO'd receiver
- -rac_signalForSelector: may fail for struct returns
- -rac_signalForSelector: struct returning selectors support.
- Selector signal documentation clarification.
- Implement -respondsToSelector: when using -rac_signalForSelector: on KVO'd target
- Fix to -rac_signalForSelector: properly implement -respondsToSelector: for optional method from a protocol