Pull to refresh

Магия в рантайме: меняем Objective-C метод на лету

Reading time8 min
Views6.4K
Читая Mac OS X 10.6 Reference Library, я испытал смешанные эмоции: столько новых возможностей, но если их использовать, программы не смогут запуститься на PowerPC маках, и к тому же не все захотят ставить Снежного Барса, если их вполне устраивает Лео. Самым простым решением кажется не использовать эти возможности, но это значит ограничить себя. Не знаю как вы, но я не люблю, если меня ограничивают. Хочется чтобы программа использовала все преимущества Снежного Барса, но в то же время могла работать на прежней версии Mac OS X. Возможно ли это?

Возможно! Причём с Objective-C вы сделаете это настолько прозрачно, насколько это вообще возможно. Представьте (и это лишь малая часть), что с помощью функций из Objective-C Runtime Library (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(Class cls, SEL name, IMP imp, const char *types) чтобы зарегистрировать новый метод и реализуйте методы resolveInstanceMethod: или resolveClassMethod:, для методов класса или экземпляра. Эти сообщения посыляются Cocoa перед запуском механизма пересылки сообщений (message forwarding mechanism).

+ (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, это третий аргумент – указатель на строку со схемой типов возвращаемого значения и аргументов функции, реализующей метод. Каждому базовому типу соответствует строка символов, сначала пишется тип возвращаемого значения, затем типы двух обязательных аргументов: объекта self и селектора _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)
bnum битовое поле из num бит
^type указатель на type
? неизвестный тип (используется для указателей на функции)


Намного лучше, особенно с переопределёнными и сложными типами, использовать директиву @encode(), чтобы составить строку со схемой, пример:

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: и ничего не изменится.

Всё бы ничего, но, к сожалению, реализация всех особенностей системных методов может оказаться весьма сложной, во вторых, и это главное, мы просто можем не знать используемые приватные классы и структуры, или методы и функции, соответственно, в некоторых случаях это вообще не вариант.

К тому же, если вам нужно использовать несколько реализаций одного (не Cocoa) метода в зависимости от условий, то это не очень удобно: сообщение посылается с каждым селектором, нужно самим формировать строку со схемой, независимые реализации для методов класса и экземпляра. Из-за всего этого код многократно дублируется и изобилует if` ами, что смотрится не очень и отлаживается ещё хуже. В общем, избегайте неясной архитектуры.

Используем несколько реализаций метода


Чтобы уйти от этих проблем мы реализуем метод + initialize, это сообщение посылается Cocoa всего один раз каждому классу, и гарантируется его получение перед любым иным сообщением, что нам и нужно. Ещё мы используем функцию class_replaceMethod(Class cls, SEL name, 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, затем написать всего по одной функции использующейся на legacy системе, и, если нужно, заменить ею реализацию!

Кроме того, чтобы не писать строку типов, используйте функции class_getClassMethod(Class cls, SEL name), class_getInstanceMethod(Class cls, SEL name) и method_setImplementation(Method m, IMP imp) Method — это указатель на структуру метода.

Итоговый вариант:

// 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


Но как изменить реализацию в категории, если вы не знаете есть ли у класса initialize? На помощь приходят методы + load, у класса и каждой категории может быть своя реализация этого метода и они не будут переопределяться или конкурировать, замечательно, не так ли?

Переменные экземпляра


Наконец-то всё, или нет? Вопрос, оставшийся без ответа – как получить переменные экземпляра в функции. Можно просто использовать setter, getter или KVC, или, если вы не ищите лёгких путей, object_getInstanceVariable(id obj, const char *name, void **outValue), object_setInstanceVariable(id obj, const char *name, void *value), class_getInstanceVariable(Class cls, const char *name).

Первые две функции могут получать и устанавливать значение переменной экземпляра. Кроме этого, все три функции возвращают Ivar – указатель на структуру с информацией о переменной экземпляра, которую можно кешировать. Если нужно несколько раз получать или устанавливать значение переменных, используйте Ivar с функциями object_getIvar(id obj, Ivar ivar), object_setIvar(id ob, Ivar ivar, id value).

// 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_getInstanceVariable, object_setInstanceVariable, object_getIvar, object_setIvar, если у вас переменные экземпляра – обычные C типы! Они предполагают, что переменные экземпляра – указатели на объекты. Фишка в том, что указатели, не зависимо от того, на что они указывают, имеют одинаковый размер: 32 или 64 бита. Если размер переменной отличается от размера указателя, у вас скопируется совсем не то, что вы хотите. Вместо этого вам нужно немного поиграться с указателями:

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


Или вы может использовать категорию NSObject, написанную John Calsbeek, с ней вы можете получить переменную экземпляра у любого объекта (не зависимо от желания программиста ;-)):

@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/reference.html
ObjCRuntimeGuide/Introduction/Introduction.html
ObjCRuntimeRef/Reference/reference.html
NSObject_Class/Reference/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
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 4: ↑3 and ↓1+2
Comments0

Articles