UAObfuscatedString / バイナリ上での文字列難読化ライブラリー

UrbanApps/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 です。

といった感じで今年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];

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

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

では該当のソースファイルとなるNSObject+RACSelectorSignal.m(v2.1.8時点)を見てみましょう。

クラス生成

L247-L257では

  • 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の動的特性は非常に強力ですが、こういった処理を行うライブラリーを複数組み合わせてしまうとデバッグが難しくなったり実装の衝突で大変なことになってしまうことも否定できません。

黒魔術は用法・容量を守って正しくお使いください!!


参考リンク

メッセージフォワーディング
KVO
関連するIssues / Pull Requests

逆引き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: 副作用だけのシグナルをリフティング/バインディングに使用する

元ネタ: Best practice for signals that capture completed/error operations · Issue #653 · ReactiveCocoa/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/@NORACUnit.defaultUnit を送る必要がなくなります。

補足

元ネタでは [RACSignal return:@YES]; を返す部分に -then: を使用していましたが、そう遠くない内にリリースされるであろう ReactiveCocoa 3.0 では -then:非推奨 (deprecated) になりそうなので置き換えています。