実行時のマジック:Objective-Cメソッドをオンザフライで変更する

Mac OS X 10.6リファレンスライブラリを読んで、私はさまざまな感情を経験しました。非常に多くの新機能がありますが、それらを使用すると、プログラムはPowerPCポピーで実行できなくなります。 最も簡単な解決策は、これらの機会を使用しないことですが、それは自分自身を制限することを意味します。 私はあなたのことは知りませんが、もし彼らが私を制限するなら、私はそれが好きではありません。 このプログラムでSnow Leopardを最大限に活用したいのですが、同時に以前のバージョンのMac OS Xでも動作します。これは可能ですか?

たぶん! また、Objective-Cを使用すると、可能な限り透明にすることができます。 Objective-Cランタイムライブラリ(objc / runtime.h)の関数を使用して、オブジェクトにメソッドと変数を追加し、メソッドの実装を置き換え、関数からオブジェクト変数の値を取得および設定できることを想像してください(これはほんの一部です)プログラム実行中! 他の(特にコンパイルされた)言語では、同じ柔軟性を実現できません。

バージョン定義


CocoaはMac OS Xのマイナーバージョンでのみ変更され、Cocoaの新しいバージョンはバックポートされず、Mac OS Xの古いバージョンにはインストールできません。これらの事実を考えると、Cocoaの機能を判断するのは非常に簡単です。システムのバージョンを調べるだけです Gestalt Managerを使用すると、システムに関するほとんどすべてのことを学習できますが、現在はそのバージョンのみが必要です。

SInt32 version;

Gestalt(gestaltSystemVersionMinor, &version);

if (version <= 5) {
// 10.5
}
else {
// 10.6 and later
}


新しいメソッドを登録する


プロトコルまたはインターフェイスでメソッドを宣言するだけで、通常は実装を拒否できます! したがって、プログラムの起動時には、このメソッドもその実装も存在せず、プログラムの実行中にクラスに追加することができます(対応するメッセージを受信したときに例外をスローしないようにする必要があります)。

class_addMethod関数(クラスcls、SEL名、IMP imp、const char *型)を使用して、新しいメソッドを登録し、クラスまたはインスタンスメソッドのresolveInstanceMethod:またはresolveClassMethod:メソッドを実装します。 これらのメッセージは、メッセージ転送メカニズムを開始する前にCocoaによって送信されます。

+ (BOOL) resolveInstanceMethod: (SEL) aSEL {

if (aSEL == @selector(null)) {
class_addMethod([self class], aSEL, (IMP) _null, "v@:");
return YES;
}
else if (aSEL == @selector(isHidden)) {
class_addMethod([self class], aSEL, (IMP) _isHidden, "c@:");
return YES;
}
else if (aSEL == @selector(setHidden:)) {
class_addMethod([self class], aSEL, (IMP) _setHidden, "v@:c");
return YES;
}

return [super resolveInstanceMethod: aSEL];
}


class_addMethodで最も興味深いのは、その3番目の引数-戻り値の型のスキーマを持つ文字列へのポインタと、メソッドを実装する関数の引数です。 各基本タイプは文字列に対応し、最初に戻り値のタイプが書き込まれ、次に明示的なパラメーターのタイプの後に必要な2つの引数のタイプ: セルフオブジェクトと_cmdセレクターが書き込まれます。

文字コード表:

: :

c char
i int
s short
l long, l 32 64-
q long long
C unsigned char
I unsigned int
S unsigned short
L unsigned long
Q unsigned long long
f float
d double
B C++ bool C99 _Bool
v void
* (char *)
@
# (Class)
: (SEL)
[array type]
{name=type...} (struct)
(name=type...) (union)
b num num
^ type type
? ( )


@overcode()ディレクティブを使用してスキーマを持つ行を構成することは、特にオーバーライドされた複雑な型の場合、はるかに優れています。たとえば、

const char *types = [[NSString stringWithFormat: @"%s%s%s%s", @encode(void), @encode(id), @encode(SEL), @encode(BOOL)] UTF8String];

どのように機能しますか? メソッドが欠落しているオブジェクトにメッセージを送信すると、そのセレクターは、動的解決のためにresolveInstanceMethod:またはresolveClassMethod:に渡されます。 オブジェクトがこのメッセージを転送できるようにするには、セレクターに対してNOを返す必要があります。

if (aSEL == @selector(setHidden:)) {
class_addMethod([self class], aSEL, (IMP) _setHidden, "v@:c");
return NO;
}


このメソッドは、たとえば、Cocoaから既存のメソッドを自分で作成する場合など、メソッドを追加するだけの場合に最適です。 この場合、システムのバージョンを確認する必要はありません:プログラムが10.5で実行された場合、もちろんメソッドが存在しない場合は登録され、10.6で実行されメソッドが存在する場合、そのセレクターはresolveInstanceMethodの引数引数になりません何も変わりません。

すべてがうまくいきますが、残念ながら、システムメソッドのすべての機能の実装は非常に困難であることが判明し、第二に、これは重要です、使用されているプラ​​イベートクラスと構造、またはメソッドと関数をそれぞれ知らない場合がありますオプション。

さらに、条件に応じて1つの(非Cocoa)メソッドの複数の実装を使用する必要がある場合、これはあまり便利ではありません:メッセージは各セレクターで送信されるため、回路およびクラスメソッドとインスタンスメソッドの独立した実装で自分で回線を作成する必要があります。 このすべてのために、コードは繰り返し複製され、if`miでいっぱいになります。if`miは見栄えがよくなく、さらに悪いデバッグになります。 一般的に、あいまいなアーキテクチャは避けてください。

メソッドのいくつかの実装を使用します


これらの問題から逃れるために、 + initializeメソッドを実装します 。このメッセージは各クラスに1回だけCocoaに送信され、他のメッセージの前に受信されることが保証されています。 class_replaceMethod関数(クラスcls、SEL名、IMP imp、const char * types)も使用します。たとえば、メソッドが存在しない場合は、クラスにメソッドがある場合、単にclass_addMethodとして追加するため、 method_setImplementationとして実装を置き換え、 types行を無視します 。 これは、空のメソッドを記述し、次のコードを使用できることを意味します。

+ initialize {

SInt32 version;

Gestalt(gestaltSystemVersionMinor, &version);

const char *types = [[NSString stringWithFormat: @"%s%s%s", , @encode(BOOL), @encode(id), @encode(SEL)] UTF8String];

if (version < 6) {
class_replaceMethod([self class], @selector(isHidden), (IMP) Legacy_HSFileSystemItem_IsHidden, types);
}
else {
class_replaceMethod([self class], @selector(isHidden), (IMP) HSFileSystemItem_isHidden, types);
}
}

- (BOOL) isHidden {

return NO;
}


メソッドの実装を変更します


それでも、これは最良の方法ではありません。いくつかの関数を作成し、それらの適切な動作を監視する必要があるためです。 たとえば、Mac OS X 10.5のサポートを拒否する場合は、メソッドを書き直して、他の非創造的なナンセンスを行う必要があります。 これを行わないためには、Cocoaの最新バージョンに基づいてメソッドを記述し、レガシーシステムで使用される関数を1つだけ記述し、必要に応じて実装に置き換えることをお勧めします。

さらに、タイプ文字列の書き込みを回避するには、関数class_getClassMethod(クラスcls、SEL名)class_getInstanceMethod(クラスcls、SEL名) 、およびmethod_setImplementation(メソッドm、IMP imp) メソッドを使用します。これはメソッドの構造へのポインターです。

最終バージョン:

// HSFileSystemItem.m

#import "HSFileSystemItem.h"

#ifdef __HS_Legacy__

#import "HSFileSystemItem_Legacy.h"
#import <CoreServices/CoreServices.h>

#endif // __HS_Legacy__

@implementation HSFileSystemItem

#ifdef __HS_Legacy__

+ initialize {

SInt32 version;

Gestalt(gestaltSystemVersionMinor, &version);

if (version < 6) {
Method _isHidden_method = class_getInstanceMethod([self class], @selector(isHidden));
method_setImplementation(_isHidden_method, (IMP) HSFileSystemItem_isHidden);
}
}

#endif // __HS_Legacy__

- (BOOL) isHidden {

id value = nil;
[_url getResourceValue: &value forKey: NSURLIsHiddenKey error: nil];

return [value boolValue];
}

// Other

@end


しかし、クラスが初期化されているかどうかわからない場合、カテゴリの実装を変更するにはどうすればよいですか? + Loadメソッドが助けになります。クラスと各カテゴリはこのメソッドの独自の実装を持つことができ、それらは再定義されたり競合したりすることはありません、素晴らしいですね。

インスタンス変数


それはついにですか? 未回答の質問は、関数でインスタンス変数を取得する方法です。 setter、getter、またはKVCを使用するか、簡単な方法を探していない場合は、 object_getInstanceVariable(id obj、const char * name、void ** outValue)object_setInstanceVariable(id obj、const char * name、void * value)、 class_getInstanceVariable (クラスcls、const char * name)

最初の2つの関数は、インスタンス変数の値を受け取って設定できます。 さらに、3つの関数はすべて、キャッシュ可能なインスタンス変数に関する情報を持つ構造体へのポインターであるIvarを返します。 変数の値を数回取得または設定する必要がある場合は、関数object_getIvar(id obj、Ivar ivar)object_setIvar(id ob、Ivar ivar、id value)でIvarを使用します

// HSFileSystemItem_Legacy.m

#import <ApplicationServices/ApplicationServices.h>
#import <CoreServices/CoreServices.h>
#import <objc/runtime.h>
#import "HSFileSystemItem.h"

static BOOL HSFileSystemItem_isHidden(id self, SEL _cmd)
{
NSURL *_url = nil;
object_getInstanceVariable(self, "_url", &_url);

LSItemInfoRecord itemInfo;

// Get file`s item info
OSStatus err = LSCopyItemInfoForURL((CFURLRef) _url, kLSRequestAllFlags, &itemInfo);

if (err != noErr) {
NSLog(@"LSCopyItemInfoForURL: error getting item info for %@. The error returned was: %d", _url, err);
}

return itemInfo.flags & kLSItemInfoIsInvisible;
}


または

static BOOL HSFileSystemItem_isHidden(id self, SEL _cmd)
{
static Ivar _url_ivar = class_getInstanceVariable([self class], "_url");
NSURL *_url = object_getIvar(self, _url_ivar);

// Get file`s item info
OSStatus err = LSCopyItemInfoForURL((CFURLRef) _url, kLSRequestAllFlags, &itemInfo);

if (err != noErr) {
NSLog(@"LSCopyItemInfoForURL: error getting item info for %@. The error returned was: %d", _url, err);
}

return itemInfo.flags & kLSItemInfoIsInvisible;
}


インスタンス変数がある場合、 object_getInstanceVariableobject_setInstanceVariableobject_getIvarobject_setIvarを使用しないでください-通常のC型! インスタンス変数はオブジェクトへのポインターであると想定しています。 秘Theは、ポインターが指すものに関係なく、同じサイズ(32ビットまたは64ビット)を持つことです。 変数のサイズがポインターのサイズと異なる場合、必要なものはまったくコピーされません。 代わりに、ポインターを少し試す必要があります。

static Ivar _int_ivar = class_getInstanceVariable([self class], "_num");
int *_num = (int *) ((uint8_t *) self + ivar_getOffset(ivar));


または、John Calsbeekによって書かれたNSObjectカテゴリを使用して、任意のオブジェクトからインスタンス変数を取得できます(プログラマの要望に関係なく;-))。

@implementation NSObject (InstanceVariableForKey)

- (void *) instanceVariableForKey: (NSString *) aKey {
if (aKey) {
Ivar ivar = object_getInstanceVariable(self, [aKey UTF8String], NULL);
if (ivar) {
return (void *)((char *)self + ivar_getOffset(ivar));
}
}
return NULL;
}

@end


int _num = *(int *) [self instanceVariableForKey: "_num"];

エピローグ


ボーナス:メソッドが最初の呼び出しで実装を決定することを許可するオプション(あなたは変態である必要があります;-))

- (BOOL) isHidden {

SInt32 version;

Gestalt(gestaltSystemVersionMinor, &version);

const char *types = [[NSString stringWithFormat: @"%s%s%s", , @encode(BOOL), @encode(id), @encode(SEL)] UTF8String];

if (version < 6) {
class_replaceMethod([self class], _cmd, (IMP) HSFileSystemItem_Legacy_IsHidden, types);
}
else {
class_replaceMethod([self class], _cmd, (IMP) HSFileSystemItem_isHidden, types);
}

return [self isHidden];
}


参照資料



主なリンク:
Gestalt_Manager /リファレンス/ reference.html
ObjCRuntimeGuide /はじめに/ Introduction.html
ObjCRuntimeRef / Reference / reference.html
NSObject_Class /リファレンス/ Reference.html

参照:
http://cocoasamurai.blogspot.com/2010/01/understanding-objective-c-runtime.html
http://mikeash.com/pyblog/friday-qa-2009-03-13-intro-to-the-objective-c-runtime.html
http://stackoverflow.com/questions/1219081/object-getinstancevariable-works-for-float-int-bool-but-not-for-double

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


All Articles