Objective-Cランタイム。 理論と実用化

この投稿では、多くの初心者のiPhone開発者がしばしばあいまいな考えを持っている、Objective-C Runtimeというトピックに対処したいと思います。 多くの人はそれが存在することを知っていますが、その機能は何であり、実際にどのように使用するのですか?
このライブラリの基本的な機能を理解してみましょう。 この資料は、 コアラの従業員を訓練するために使用する講義に基づいています。

ランタイムとは何ですか?


Objective-CはC言語のアドオンとして考えられ、オブジェクト指向のパラダイムのサポートを追加しました。 実際、構文の観点から見ると、Objective-Cは通常のCのキーワードと制御構造のかなり小さなセットです。ランタイムライブラリであるランタイムは、言語に命を吹き込む機能セットを提供し、その動的機能を実現し、OOPの機能を保証します。

基本的なデータ構造

ランタイムライブラリの関数と構造は、いくつかのヘッダーファイルで定義されています: objc.hruntime.hおよびmessage.h 。 まず、 objc.hファイルを見て、ランタイムに関してオブジェクトが何であるかを見てみましょう。

 typedef struct objc_class *Class; typedef struct objc_object { Class isa; } *id; 

プログラムの処理中のオブジェクトは、通常のC構造で表されることがわかります。 各Objective-Cオブジェクトには、そのクラスへの参照(いわゆるisaポインター)があります。 アプリケーションのデバッグ中にオブジェクトの構造を表示すると、誰もがそれを見たと思います。 また、クラスは同様の構造を表します。

 struct objc_class { Class isa; }; 

Objective-Cのクラスは本格的なオブジェクトであり、「クラスのクラス」へのisaポインター、つまりObjective-Cの観点からはメタクラスも持っています。 同様に、C構造は言語の他のエンティティに対して定義されます。

 typedef struct objc_selector *SEL; typedef struct objc_method *Method; typedef struct objc_ivar *Ivar; typedef struct objc_category *Category; typedef struct objc_property *objc_property_t; 


ランタイムライブラリ関数

ライブラリには、言語の基本構造の定義に加えて、これらの構造で機能する一連の関数が含まれています。 それらは条件付きでいくつかのグループに分けることができます(関数の目的は、原則として、その名前から明らかです):

例1.オブジェクトのイントロスペクション

ランタイムライブラリを使用した例を考えてみましょう。 私たちのプロジェクトの1つでは、データモデルはいくつかのプロパティセットを持つ単純な古いObjective-Cオブジェクトです。

 @interface COConcreteObject : COBaseObject @property(nonatomic, strong) NSString *name; @property(nonatomic, strong) NSString *title; @property(nonatomic, strong) NSNumber *quantity; @end 

デバッグの便宜上、 <COConcreteObject: 0x71d6860>ようなものではなく、ログに出力するときにオブジェクトプロパティの状態に関する情報を出力したいと思います。 データモデルは非常に分岐しているため、さまざまなサブクラスが多数存在するため、プロパティの値を手動で収集するクラスごとに個別のdescriptionメソッドをdescriptionすることは望ましくありません。 Objective-Cランタイムが役立ちます:

 @implementation COBaseObject - (NSString *)description { NSMutableDictionary *propertyValues = [NSMutableDictionary dictionary]; unsigned int propertyCount; objc_property_t *properties = class_copyPropertyList([self class], &propertyCount); for (unsigned int i = 0; i < propertyCount; i++) { char const *propertyName = property_getName(properties[i]); const char *attr = property_getAttributes(properties[i]); if (attr[1] == '@') { NSString *selector = [NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding]; SEL sel = sel_registerName([selector UTF8String]); NSObject * propertyValue = objc_msgSend(self, sel); propertyValues[selector] = propertyValue.description; } } free(properties); return [NSString stringWithFormat:@"%@: %@", self.class, propertyValues]; } @end 

モデルオブジェクトの一般的なスーパークラスで定義されたメソッドは、 class_copyPropertyList関数を使用して、オブジェクトのすべてのプロパティのリストを受け取ります。 次に、プロパティ値がNSDictionaryに収集されます。これは、オブジェクトの文字列表現を構築するときに使用されます。 このアルゴリズムは、Objective-Cオブジェクトのプロパティでのみ機能します。 型チェックはproperty_getAttributes関数を使用して行われます。 メソッドの結果は次のようになります。

2013-05-04 15:54:01.992テスト[40675:11303] COConcreteObject:{
name = Foo;
数量= 10;
title = bar;
}

メッセージ


Objective-Cのメソッド呼び出しシステムは、オブジェクトにメッセージを送信することにより実装されます。 各メソッド呼び出しは、対応するobjc_msgSend関数objc_msgSend変換されobjc_msgSend

 //   [array insertObject:foo atIndex:1]; //    Runtime- objc_msgSend(array, @selector(insertObject:atIndex:), foo, 1); 

objc_msgSentの呼び出しは、関数に渡されたセレクターに対応するメソッドの実装を見つけるプロセスを開始します。 メソッドの実装は、いわゆるクラスディスパッチテーブルで求められます。 このプロセスは非常に時間がかかるため、メソッドキャッシュは各クラスに関連付けられています。 メソッドの最初の呼び出しの後、その実装の検索結果はクラスにキャッシュされます。 メソッドの実装がクラス自体で見つからない場合、このクラスのスーパークラスで継承階層を検索し続けます。 ただし、階層内で検索が行われなかった場合は、動的検索メカニズムが機能しますresolveInstanceMethodまたはresolveClassMethodという特別なメソッドの1つが呼び出さresolveClassMethodます。 これらのメソッドをオーバーライドすることは、ランタイムに影響を与える最後の可能性の1つです。

 + (BOOL)resolveInstanceMethod:(SEL)aSelector { if (aSelector == @selector(myDynamicMethod)) { class_addMethod(self, aSelector, (IMP)myDynamicIMP, "v@:"); return YES; } return [super resolveInstanceMethod:aSelector]; } 

ここで、呼び出されたメソッドの実装を動的に示すことができます。 何らかの理由でこのメカニズムが適切でない場合は、メッセージ転送を使用できます。

例2.メソッドのスウィズリング

Objective-Cのカテゴリの機能の1つは、カテゴリで定義されたメソッドが基本クラスメソッドを完全にオーバーライドすることです。 オーバーライドする必要はないが、既存のメソッドの機能を拡張する必要がある場合があります。 たとえば、何らかの理由で、 NSMutableArray配列に追加するすべての要素を誓約したいとします。 標準言語ツールはこれを行いません。 ただし、メソッドスウィズリングと呼ばれる手法を使用できます。

 @implementation NSMutableArray (CO) + (void)load { Method addObject = class_getInstanceMethod(self, @selector(addObject:)); Method logAddObject = class_getInstanceMethod(self, @selector(logAddObject:)); method_exchangeImplementations(addObject, logAddObject); } - (void)logAddObject:(id)aObject { [self logAddObject:aObject]; NSLog(@"  %@   %@", aObject, self); } @end 

loadメソッドをオーバーロードしload -これは、他のメソッドが呼び出される前に、クラスで定義されている場合、このクラスの初期化中に呼び出される特別なコールバックです。 ここで、ベースのaddObject:メソッドとlogAddObject: addObject:実装を交換します。 logAddObject: 「再帰的」呼び出しに注意してlogAddObject:これは、mainメソッドのオーバーロード実装への呼び出しです。

例3.連想リンク

カテゴリのもう1つの既知の制限は、カテゴリ内に新しいインスタンス変数を作成できないことです。 たとえば、 UITableViewライブラリクラスに新しいプロパティを追加する必要があるとしUITableView -テーブルが空のときに表示される「スタブ」へのリンク:

 @interface UITableView (Additions) @property(nonatomic, strong) UIView *placeholderView; @end 

そのままでは、このコードは機能せず、プログラムの実行中に例外が発生します。 この問題は、連想リンク機能を使用して回避できます。

 static char key; @implementation UITableView (Additions) -(void)setPlaceholderView:(UIView *)placeholderView { objc_setAssociatedObject(self, &key, placeholderView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -(UIView *) placeholderView { return objc_getAssociatedObject(self, &key); } @end 

objc_setAssociatedObject関数を使用して他のオブジェクトを関連付けることにより、任意のオブジェクトを連想配列として使用できます。 その操作には、 objc_getAssociatedObjectの呼び出しを使用して必要なオブジェクトを取得できるキーが必要です。 同時に、コピーされたキー値を使用することはできませんobjc_setAssociatedObject呼び出しで渡されたオブジェクト(この例ではポインター)でなければなりません。

おわりに


これで、Objective-Cランタイムとは何か、実際に開発者にとってどのように役立つかについての基本的な理解ができました。 ライブラリの可能性をより深く知りたい人のために、以下の追加リソースをアドバイスできます。

Source: https://habr.com/ru/post/J177421/


All Articles