UAObfuscatedString / バイナリ上での文字列難読化ライブラリー
NSLog(@"%@", @"".T.h.i.s._.i.s._.a._.t.e.s.t.dot); > This is a test. NSString *identifier = @"".c.o.m.dot.u.r.b.a.n.a.p.p.s.dot.e.x.a.m.p.l.e;
サンプルを見れば分かるように、NSString
のカテゴリーとして英数字や記号のメソッドが追加されており、メソッド呼び出しによって文字連結で文字列を生成していくというライブラリー。
アプリのバックエンドAPIのURLやAPIキーといった知られたくない情報をそのまま文字列定数としてソースコードに記載してしまうと、アプリのバイナリ解析で見つけられたり書き換えができてしまうので、バイナリ上には単一文字としてしか含まれないようにするというアイデア。
Objective-CのランタイムAPIをReactiveCocoaで解説する
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パターン。
- 対象メソッドを実行する。
[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
逆引きReactiveCocoa: シグナルにイベント発生時に対する処理を挟み込む
ReactiveCocoaのドキュメントにもあるように、-do...
系のメソッドを使用すると、シグナルを購読せずに副作用を挟み込むことができます。
RACSignal *newSignal = [[[signal doNext:^(id x) { NSLog(@"next"); }] doError:^(NSError *error) { NSLog(@"error"); }] doCompleted:^{ NSLog(@"completed"); }];
シグナルを購読する終端であれば、各種処理を-subscribe...:
系のメソッドのブロック内で行うこともできますが、シグナルに手を加えてシグナルを返すようなメソッドでは便利に使用することができます。
また、例えば-doNext:
を重ねて記述した場合、記述順(lexical order)に処理が実行されます。
[[[signal doNext:^(id x) { NSLog(@"1st"); }] doNext:^(id x) { NSLog(@"2nd"); }] subscribeNext:^(id x) { NSLog(@"-subscribeNext:"); }]; // 1st // 2nd // -subscribeNext:
シグナルの終了時の処理として正常系/異常系のどちらでも同じ処理を行いたい場合、-finally:
が使用できます。
// 以下の2つは同義。 [signal finally:^{ [self doSomeStuff]; }]; [[signal doError:^(NSError *error) { [self doSomeStuff]; }] doCompleted:^{ [self doSomeStuff]; }];
補足
ReactiveCocoa 3.0では-finally:
は非推奨となり、-doFinished:
が追加(としてリネーム)されています。
逆引きReactiveCocoa: 毎時0分に処理を行う
元ネタ: ios - Perform action on the hour, every hour, with ReactiveCocoa - Stack Overflow
現在時刻から1時間毎だと毎時0分とならないので、まずは次の時までの分を得ておきます。
NSDateComponents *components = [[[NSCalendar sharedCalendar] calendar] components:NSMinuteCalendarUnit fromDate:[NSDate date]];
NSInteger minutesToNextHour = 60 - components.minute;
いったん現在時刻から次の時までのタイマーシグナルを生成し、1回限りで終了させた後に改めて60分間隔のタイマーシグナルを繋げて完成です。
RACSignal *updateEventSignal = [[[RACSignal interval:(60 * minutesToNextHour)] take:1] concat:[RACSignal interval:3600]];
逆引きReactiveCocoa: @YES/@NOをトグルで送るRACCommandを用意する
元ネタ: Command which sends alternating YES/NO values · Issue #767 · ReactiveCocoa/ReactiveCocoa
ボタン・ジェスチャーなどのUI操作とRACSignal
を仲介するクラスとしてRACCommand
があります。これを用いて、状態(フラグとなるインスタンス変数/プロパティ)を持たずにトグルボタンを表現するパターンです。
// ブロックの戻り値が`[command execute:senderOrValue]`の戻り値となる。 RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^(id _) { return [RACSignal return:RACUnit.defaultUnit]; }]; RACSignal *alternating = [[[command.executionSignals // `-execute:`の度にその戻り値のシグナルを送るシグナル。 flatten] startWith:RACUnit.defaultUnit] // ユーザー操作前に初期値がセットされるようにする。 scanWithStart:@NO combine:^(NSNumber *previous, id _) { // 前回の値を反転したブール値を返し、 // コマンドの実行によって得られた値は無視する(_ == RACUnit.defaultUnit)。 // // 最初に`@NO`を反転するため、シグナルの初回の値は`@YES`となる。 return @(!previous.boolValue); }]; RAC(self, onOff) = alternating; // `UIControlEventTouchUpInside`のイベントで`-execute:`される。 button.rac_command = command;
逆引きReactiveCocoa: シグナルの現在の値だけでなく、直前の値にアクセスする
元ネタ: How to obtain KVO old value using ReactiveCocoa 2.0 API? · Issue #762 · ReactiveCocoa/ReactiveCocoa
KVOのオプションを使用する場合
ReactiveCocoaではRACObserve()
マクロによってKVOを非常に簡単に扱うことができますが、それだけでは、通常のKVOで使用するaddObserver:forKeyPath:options:context:
の場合には指定できるNSKeyValueObservingOptions
(NSKeyValueObservingOptionOldなど) を指定できません。
KVOのオプション指定を行いたい場合、-[NSObject rac_valuesAndChangesForKeyPath:options:observer:]
を使用するとKVOのオプション指定を反映したシグナルを得ることができます。ただし、この戻り値のシグナルはRACObserve()
とは違い、next
として、変更後の値とチェンジディクショナリーからなるRACTuple
を送るため、-reduceEach:
を使用するなどしてタプルの値を分解・チェンジディクショナリーから変更前の値を取得するなどの必要があります。
[[self rac_valuesAndChangesForKeyPath:keyPath options:NSKeyValueObservingOptionOld observer:nil] reduceEach:^(id value, NSDictionary *changeDictionary) { id oldValue = changeDictionary[NSKeyValueChangeOldKey]; return // 何かしらの値; }];
オペレーターを使用する場合
-[RACSignal combinePreviousWithStart:reduce:]
を使用すると、reduce:
のブロック引数で以下のように直前の値を使用することができます。
RACSignal *signal = [@[ @1, @2, @3, @4 ].rac_sequence signal]; [[signal // 最初の`next`では`previous`に1つ目の引数(この場合は`@0`)が渡され、 // 2回目以降の`next` では前回の`current`が渡される。 combinePreviousWithStart:@0 reduce:^(NSNumber *previous, NSNumber *current) { return @(previous.integerValue + current.integerValue); }] subscribeNext:^(NSNumber *value) { NSLog(@"%d", value.integerValue); }]; // 1 (<= 0 + 1) // 3 (<= 1 + 2) // 5 (<= 2 + 3) // 7 (<= 3 + 4)
逆引きReactiveCocoa: 副作用だけのシグナルをリフティング/バインディングに使用する
ログインやデータの永続化など、特に next
として送る値がないシグナルの結果を -[NSObject rac_liftSelector:withSignals:]
や RAC()
マクロでのバインディングに使用したい場合にどうするべきかというパターンです。
RACSignal *login = [[[self // ログイン成功時には `completed`, 失敗時には `error` を送る。 loginWithUserName:userName password:password] concat:[RACSignal return:@YES]] catchTo:[RACSignal return:@NO]]; RAC(self, loggedIn) = login; // or [self rac_liftSelector:@selector(loginDidSuccess:) withSignals:login, nil];
とすると、-loginWithUserName:password:
の実装側では、next
として @YES/@NO
や RACUnit.defaultUnit
を送る必要がなくなります。
補足
元ネタでは [RACSignal return:@YES];
を返す部分に -then:
を使用していましたが、そう遠くない内にリリースされるであろう ReactiveCocoa 3.0 では -then:
が非推奨 (deprecated) になりそうなので置き換えています。