この記事は Objective-Cランタイムに関する私の講演の要約です。もしあなたがObjective-Cランタイムに興味があるなら、この記事、特に記事中のリンクをチェックしてみてください。
Objective-Cランタイムとは何ですか?
簡単に言えば、Objective-CランタイムはObjective-C言語を実装したCライブラリです。オブジェクトはC言語の構造体として表現でき、メソッドはC言語の関数として実装できます。実際、Objective-Cランタイムはそのほとんどを実現し、さらにいくつかの機能を追加しています。これらの構造と関数はランタイム関数にカプセル化され、Objective-Cプログラマが実行時にクラス、オブジェクト、およびそれらのメソッドを作成、検査、変更できるようにします。
カプセル化に加えて、Objective-Cのランタイム・ライブラリはメソッドの最終的な実行コードを見つける役割も担っています。アプリケーションが[object doSomething]を実行するとき、単にメソッドを見つけて呼び出すのではなく、オブジェクトにメッセージが送られます。その代わりに、メッセージがオブジェクトに送られ、ランタイム・ライブラリはオブジェクトにメッセージに応答して何をするかを決めるチャンスを与えます。 Alan Kayは、メッセージングがSmalltalk(Objective-CSmalltalkから開発いる)の最も重要な部分であり、オブジェクトではないことを繰り返し強調しています:
私は過去にこのトピックで "オブジェクト "という言葉を作りました。
ここでもっと重要なのは、Smalltalkの核心である「メッセージ・コマンド」です。日本語には「間」という短い言葉がありますが、これは2つのオブジェクトの間にあるものという意味です。大規模でスケーラブルなシステムを作る鍵は、内部のプロパティや振る舞いに注目するのではなく、モジュールが互いに通信する方法を設計することです。
実際、Smalltalk記事では、このプログラミング技法はメッセージパッシングまたはメッセージングパラダイムと呼ばれています。「オブジェクト指向」はメモリ管理システムを説明するときによく使われます。
ObjCランタイムという言葉はプレゼンテーションや記事で使われ、1つしかないように見えるかもしれませんが、実際には多くのランタイム・ライブラリが存在します。それらはすべて、オブジェクトのイントロスペクション・チェックとメッセージ受信をサポートしていますが、異なる機能と実装を持っています。以下の議論はすべてAppleの***ランタイム・ライブラリに基づいています。
動的なクラス作成
Key-Value Observingを実装するには?
この講演の準備をしているときに、KVO considered harmful 記事が多くのファンを獲得し始めました。KVOに対する多くの正当な批判が書かれていましたが、私はオブザーバー・パターンを放棄することよりも、新しい実装を探求することに興味がありました。
KVOのオブザーバー・パターンの実装の鍵は、被観察オブジェクトのクラスをこっそりと変更し、元のクラスをサブクラス化した後、そのオブジェクトのメソッドをカスタマイズしてKVOのコールバック・メソッドを呼び出せるようにすることです。これはすべて objc_duplicateClass メソッドを通して行われますが、残念ながらこのメソッドは public ではないため、private で呼び出すことはできません。
すべての道はローマに通ず、そして良い知らせは、objc_duplicateClassの他に、"クラス・ペア "を作成して登録するなど、秘密のサブクラス化を使用してオブザーバー・パターンを実装する方法があるということです。では、クラス・ペアとは何でしょうか?Objective-C のクラスでは、それを定義する Class オブジェクトのペアがあります。Class オブジェクトはクラスのインスタンス・メソッドを定義し、メタクラスはクラスのクラス・メソッドを定義します。クラス・オブジェクトはクラスのインスタンス・メソッドを定義し、メタクラスはクラスのクラス・メソッドを定義します。
このコードオブザーバー・パターンがどのように機能するかを示しています。オブジェクトにオブザーバを追加すると、オブジェクトはまずそれがobservableかどうかをチェックし、observableであれば新しいクラスを作成し、元のクラスのメソッドを独自の -dealloc で置き換えます。
このセッターメソッドは、変更前と変更後の属性の値を取得し、オブザーバにこれら 2 つの値を伝えるためにブロック形式のコールバック関数を呼び出します。このコードでは、必要に応じてこのブロックを非同期に呼び出すことができます。
addObserverForKey:withBlock:は、object_setClass()を使って観測オブジェクトのクラスを新しく生成されたクラスに置き換えることに注意してください。この主な目的は、メッセージがメソッドに変換される方法を変更することですが、これは非常に注意深く行う必要があります。 メンバ変数もランタイムによってアクセスされるため、オブジェクトのクラスを変更すると、ランタイムが対応する変数を見つけられなくなる可能性があります。
オブザーバー・コレクションを格納する場所がなかったため、格納に苦労しました。ObserverPatternクラスにメンバ変数を追加しても、そのクラスのオブジェクトが全く生成されなかったため、うまくいきませんでした。観測されたオブジェクトのメンバ変数は、これらのオブザーバを考慮しなかった元のクラスのものです。
Objective-Cのランタイムは、associated objects導入することで、このジレンマから抜け出す手助けをします。ランタイムでは、すべてのオブジェクトは理論上、他のオブジェクトを含む辞書を持つことができます。関連参照により、オブザーバオブジェクトは、メンバ変数を追加することなく、オブザーバを格納し、アクセスすることができます。
何度も実行すると、ObserverPatternがまだ少し不具合があることに気づくでしょう。オブザーバのコールバックは非同期に呼び出されるので、オブザーバは
また、受け取った変更イベントも無秩序です。つまり、オブザーバは観測されたプロパティの最終的な状態が何であるかを実際に区別することができず、コールバックの新しい値はかなり前に変更されている可能性があります。KVOでコールバックを同期的に呼び出すことは、バグではなく便利な機能であることを示すために、このようにしました。
オブジェクトの作成
その余分なバイトは何のため?
Objective-Cのオブジェクトを作成すると、ランタイムはインスタンス変数格納領域の後ろに少し余分なスペースを割り当てます。これは何のためでしょうか?この領域の開始点へのポインタを取得し、インスタンス変数にインデックスを付けることができます。では、カスタム配列を使ってインデックス付き変数の使い方を説明しましょう。
配列!このSimpleArrayには2つのことが見て取れます。最も明らかなのは、クラス・クラスタ・パターンいることです。 あるオブジェクトが +alloc メソッドを使って返されるとき、通常はそのオブジェクトのためにすべてのメモリがすでに割り当てられていますが、この場合 +alloc の時点ではどれだけのメモリ領域が必要なのかはわかりません。そのため、 +alloc はプレースホルダを返すだけで、実際の配列オブジェクトが割り当てられて返されるのは初期化後となります。
なぜクラス・クラスターを使って物事を複雑にし、calloc()を使って適切なサイズの別個のキャッシュを確保し、そこにオブジェクト・ポインターを格納する必要があるのかと質問されるかもしれません。答えは、局所性の原則利用してアクセス性能を向上させたいからです。配列の設計からわかるように、配列ポインタがアクセスされるたびに、高い確率でキャッシュポインタもアクセスされます。
派遣
メッセージはどのように転送されますか?
Objective-Cの強力な特徴の1つは、コンパイル時にセレクタを宣言しても、オブジェクトはメソッドを実装する必要がないということです。しかし、実行時にメソッドを実装するか、メッセージを別のオブジェクトに転送するか、例外を投げるか、何か他のことをするかを決めることができます。メッセージ転送は-forwardInvocation:を呼び出し、NSInvocationオブジェクトを渡します。しかし、NSInvocationクラスはFoundationライブラリで定義されているので、ランタイムが動作するためにはFoundationが必要なのでしょうか?
ランタイムはプログラムに転送関数を定義させ、objc_msgSend()がそのセレクタの実装を見つけられない場合、その転送関数が呼び出されます。アプリケーションが起動するとすぐに、CoreFoundationは転送関数として-forwardInvocation:を定義します。
Rubyには#method_missingという関数があり、オブジェクトが実装していないメッセージを受け取ったときに呼び出されます。RubyのようなmethodMissing:メソッドは、Objc_setForwardHandlerを使ってObjective-Cのクラスにも実装できます。
概要
Objective-Cのランタイムは、プログラムに多くの動的な動作を追加するのに役立ちます。実際のプログラムでは、デバッグのためにメソッドをスウィズリングする以外は使わない開発者もいますが、ランタイム・プログラミングには、実際のアプリケーションのコードを書くための重要なツールになるはずの機能がたくさんあります。