Objective-CのランタイムAPIをReactiveCocoaで解説する

[iOS Advent Calendar 2013](http://qiita.com/advent-calendar/2013/ios) 14日目担当の @ikesyo です。

- [Function Reactive Programming Framework - Reactive Cocoa | Cocoaの日々情報局](http://cocoadays-info.blogspot.jp/2013/02/function-reactive-programming-framework.html)
- [iOS - ReactiveCocoaについて - Qiita [キータ]](http://qiita.com/somtd@github/items/8409ddd6d0927c04c1dd)
- [ReactiveCocoaのまとめ資料 - Qiita [キータ]](http://qiita.com/ikesyo/items/ff0fdc179baa92a144ee)
- [Objective-C - ReactiveCocoaに出てくる用語の整理 - Qiita [キータ]](http://qiita.com/yaotti/items/fb8156b2252b5bff1529)
- [[iOS]ReactiveCocoaFramework入門 | アドカレ2013 : SP #11](http://dev.classmethod.jp/smartphone/iphone/reactivecocoaintroduction/)

といった感じで今年1年で大分有名になってきた感がある **[ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa)** というリアクティブプログラミングライブラリーがあります。

このエントリーではその中でもObjective-CのランタイムAPIの利用箇所について解説してみたいと思います。

-----

## ランタイムAPI

まずObjective-CのランタイムAPIですが、大変有名な[ダイナミックObjective-C](http://news.mynavi.jp/column/objc/)でも紹介されているように、`<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の機能が使われています。

- **クラス生成**
- **インスタンスのクラス変更**
- **メソッド生成**
- **インスタンスメソッドの追加**
- **インスタンスメソッドの差し替え**
- **メッセージフォワーディング**

以下のようなコードの時、

```objc
NSObject *object = [[NSObject alloc] init];

RACSignal *signal = [object rac_signalForSelector:@selector(description)];
[signal subscribeNext:^(RACTuple *args) {
    // -description は引数がないため、`args`は空のタプル。
	NSLog(@"-description invoked.");
}];

[object description];
```

概要を説明すると内部では以下のような処理が行われています。

1. 対象インスタンス（メッセージのレシーバー）のクラスのサブクラスを動的に生成し、`-forwardInvocation:`, `-respondsToSelector:`の実装を差し替える。
    - `NSObject_RACSelectorSignal`クラスが生成される。
1. 対象インスタンスのクラスを 1. で動的生成したサブクラスに置き換える。
    - `[object class]`が`NSObject`ではなく、`NSObject_RACSelectorSignal`を返すようになる。
1. `RACSubject`（値を自由に送れる`RACSignal`のサブクラス）をAssociated Objectとして保持する。
1. 以下の2パターン。
    - 対象のメソッド（セレクター）がオリジナルのクラスに実装されていない場合、`_objc_msgForward()` を実装とするメソッドを追加する。
    - 対象のメソッド（セレクター）がオリジナルのクラスに実装されている場合、既存メソッドの実装をプレフィックス付きの別名セレクターに退避した上で、対象セレクターに紐づく実装を`_objc_msgForward()`に差し替える。
        - `@selector(description)` => `_objc_msgForward()`
        - `@selector(rac_alias_description)` => オリジナルの実装
1. 対象メソッドを実行する。
    - `[object description]`
1. (`-respondsToSelector:`の置き換えは説明省略。)
1. `[object description]`のメッセージ送信は実態として`_objc_msgForward()`が実行され、その結果として`-forwardInvocation:`が実行される。
1. `-forwardInvocation:`の置き換えられた実装の中で、オリジナルの実装を実行した後、保持している`RACSubject`に対して`-sendNext:`を行う。

では該当のソースファイルとなる[NSObject+RACSelectorSignal.m](https://github.com/ReactiveCocoa/ReactiveCocoa/blob/v2.1.8/ReactiveCocoaFramework/ReactiveCocoa/NSObject%2BRACSelectorSignal.m)（v2.1.8時点）を見てみましょう。

### クラス生成

[L247-L257](https://github.com/ReactiveCocoa/ReactiveCocoa/blob/v2.1.8/ReactiveCocoaFramework/ReactiveCocoa/NSObject%2BRACSelectorSignal.m#L247-L257)では

- `objc_allocateClassPair()`
- `objc_registerClassPair()`

を使用し、対象インスタンスのクラスをスーパークラスとするサブクラスを動的に生成し、ランタイム上に登録しています。クラス名として、オリジナルのクラス名に *_RACSelectorSignal* というサフィックスを付加した名前にし、衝突を回避するようにしています。

### インスタンスのクラス変更

[L259](https://github.com/ReactiveCocoa/ReactiveCocoa/blob/v2.1.8/ReactiveCocoaFramework/ReactiveCocoa/NSObject%2BRACSelectorSignal.m#L259)では

- `object_setClass(self, subclass)`

を使用し、対象インスタンスのクラスを動的生成したサブクラスに置き換えています。こんなことまで出来て気持ち悪いですね！！

### メソッド生成、インスタンスメソッドの差し替え

1.の`-forwardInvocation:`, `-respondsToSelector:`の実装差し替えのために、

[L58-L117](https://github.com/ReactiveCocoa/ReactiveCocoa/blob/v2.1.8/ReactiveCocoaFramework/ReactiveCocoa/NSObject%2BRACSelectorSignal.m#L58-L117)では

- `imp_implementationWithBlock()`によるメソッドの実装生成
- `class_replaceMethod()`によるメソッドの実装の差し替え

を行っています。

`imp_implementationWithBlock()`の引数のブロックは、`method_return_type ^(id self, self, method_args …)`というシグネチャが要求されるものになっています。`-respondsToSelector:`の場合は以下のようになりますね。

```objc
BOOL (^newRespondsToSelector)(id, SEL) = ^ BOOL (id self, SEL selector) {
	return // some bool value;
};

```

### インスタンスメソッドの追加・差し替え

4.の処理として[L154-L195](https://github.com/ReactiveCocoa/ReactiveCocoa/blob/v2.1.8/ReactiveCocoaFramework/ReactiveCocoa/NSObject%2BRACSelectorSignal.m#L154-L195)では

- 既存メソッドがない場合: `class_addMethod()`によるインスタンスメソッドの追加
- 既存メソッドがある場合:
    - `method_getImplementation()`による既存メソッドの実装の取得
    - `class_addMethod()`による既存メソッドの実装の別セレクターへの退避
    - `class_replaceMethod()`によるメソッドの実装の差し替え

を行っています。

既存メソッドに対しては、`@selector(rac_alias_someSelector:)`のように *rac_alias_* をプレフィックスとしたセレクターを生成し、それと`method_getImplementation()`で取得したオリジナルの実装を紐付けています。

ここでメソッドの差し替え後の実装としている関数の`_objc_msgForward()`が全体のキモとなっている部分で、メソッドの実装がこの関数となっていると、メソッド実行に関する情報（メッセージのレシーバー、セレクター、引数、実行後の戻り値など）が`NSInvocation`のインスタンスとして生成され、それを引数として`-forwardInvocation:`が実行されます。

### メッセージフォワーディング

`-forwardInvocation:` の[置き換え後の実装の中身の一部](https://github.com/ReactiveCocoa/ReactiveCocoa/blob/v2.1.8/ReactiveCocoaFramework/ReactiveCocoa/NSObject%2BRACSelectorSignal.m#L41-L56)では、シグナルの値送信の前のオリジナルの実装の実行のために`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:`では[構造体を戻り値とするメソッドはサポートされていません](https://github.com/ReactiveCocoa/ReactiveCocoa/blob/v2.1.8/ReactiveCocoaFramework/ReactiveCocoa/NSObject%2BRACSelectorSignal.m#L119-L135)。

### KVOという例外

上記が基本的なロジックですが、例外パターンが存在します。それは`-addObserver:forKeyPath:options:context:`によってKVO（Key-value observing: キー値監視）の対象となったインスタンスです。ReactiveCocoa的には、`RACObserve()`マクロを使用したインスタンスも内部ではKVOを使用しているため同様となります。

実はKVOも上記のような動的サブクラス生成・インスタンスのクラス変更を行っており、KVOの対象となったインスタンスはランタイム上で *NSKVONotifying_* というプレフィックスが付いたクラスになっています。また`-class`メソッドがオリジナルのクラスを返すようにオーバーライドされています。

ReactiveCocoaでは動的サブクラスの更なるサブクラス化は避けるために、1.の処理の際に[L219-L245](https://github.com/ReactiveCocoa/ReactiveCocoa/blob/v2.1.8/ReactiveCocoaFramework/ReactiveCocoa/NSObject%2BRACSelectorSignal.m#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](http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html)
- [Wincent Colaiuta's weblog: More than I ever wanted to know about Apple's Objective-C runtime](http://www.wincent.com/a/about/wincent/weblog/archives/2006/02/more_than_i_eve.php)
- [A Look Under the Hood of objc_msgSend() - Extra Cookies](http://blog.zhengdong.me/2013/07/18/a-look-under-the-hood-of-objc-msgsend)
- [mikeash.com: Friday Q&A 2009-03-27: Objective-C Message Forwarding](http://www.mikeash.com/pyblog/friday-qa-2009-03-27-objective-c-message-forwarding.html)

##### KVO

- [mikeash.com: Friday Q&A 2009-01-23](http://www.mikeash.com/pyblog/friday-qa-2009-01-23.html)
- [(旧) Cocoaの日々: +initialize は２度呼ばれる（場合あり）](http://xcatsan.blogspot.jp/2010/01/initialize.html?showComment=1263628976279#c6173479227238651721)

##### 関連するIssues / Pull Requests

- [Obtain a signal for any arbitrary method invocation](https://github.com/ReactiveCocoa/ReactiveCocoa/issues/267)
- [Added -rac_signalForSelector:](https://github.com/ReactiveCocoa/ReactiveCocoa/pull/384)
- [Add NSObject -rac_signalForSelectorInvocation:](https://github.com/ReactiveCocoa/ReactiveCocoa/pull/488)
- [Signal for selector using forwarding](https://github.com/ReactiveCocoa/ReactiveCocoa/pull/554)
- [Signal for selector using forwarding 2.0](https://github.com/ReactiveCocoa/ReactiveCocoa/pull/573)
- [-rac_signalForSelector: the third](https://github.com/ReactiveCocoa/ReactiveCocoa/pull/590)
- [-rac_signalForSelector:fromProtocol:](https://github.com/ReactiveCocoa/ReactiveCocoa/issues/606)
- [-rac_signalForSelector:fromProtocol: & refactored UIKit signals](https://github.com/ReactiveCocoa/ReactiveCocoa/pull/608)
- [Fix -rac_signalForSelector: doesn't invoke original method on previously KVO'd receiver](https://github.com/ReactiveCocoa/ReactiveCocoa/pull/748)
- [-rac_signalForSelector: may fail for struct returns](https://github.com/ReactiveCocoa/ReactiveCocoa/issues/783)
- [-rac_signalForSelector: struct returning selectors support.](https://github.com/ReactiveCocoa/ReactiveCocoa/pull/784)
- [Selector signal documentation clarification.](https://github.com/ReactiveCocoa/ReactiveCocoa/pull/787)
- [Implement -respondsToSelector: when using -rac_signalForSelector: on KVO'd target](https://github.com/ReactiveCocoa/ReactiveCocoa/pull/891)
- [Fix to -rac_signalForSelector: properly implement -respondsToSelector: for optional method from a protocol](https://github.com/ReactiveCocoa/ReactiveCocoa/pull/926)