(оригинал — Mike Ash, взято отсюда)
Многие Cocoa разработчики имеют довольно смутное представление об Objective-C Runtime API. Они знают, что он существует где-то там(некоторые не знают даже этого!), что он важен, и Objective-C без него неработоспособен, но обычно этим все знания и ограничиваются.
Сегодня я расскажу о том, как устроен Objective-C на уровне Runtime и о том, как конекретно вы можете это использовать.
Объекты
В Objective-C мы постоянно имеем дело с объектами, но что же такое объект на самом деле? Давайте попробуем соорудить что-то, что поможет пролить нам свет на этот вопрос.
Во-первых, всем нам известно, что мы ссылаемся на объекты с помощью указателей, например, NSObject *. Также мы знаем, что создаём мы их с помощью +alloc. Всё, что мы може узнать об этом из документации, так это то, что это происходит путём вызова +allocWithZone:. Продолжая нашу цепочку исследований, мы обнаруживаем NSDefaultMallocZone, который создаётся с помощью обыкновенного malloc. И всё!
Но что из себя представляют созданные объекты? Что ж, посмотрим:
#import <Foundation/Foundation.h>
@interface A : NSObject { @public int a; } @end
@implementation A @end
@interface B : A { @public int b; } @end
@implementation B @end
@interface C : B { @public int c; } @end
@implementation C @end
int main(int argc, char **argv)
{
[NSAutoreleasePool new];
C *obj = [[C alloc] init];
obj->a = 0xaaaaaaaa;
obj->b = 0xbbbbbbbb;
obj->c = 0xcccccccc;
NSData *objData = [NSData dataWithBytes:obj length:malloc_size(obj)];
NSLog(@"Object contains %@", objData);
return 0;
}
Мы соорудили иерархию классов, каждый из которых содержет в себе переменные, и заполнили их вполне очивидными значениями. Затем мы извлекли данные в удобоваримый вид, исползьуя malloc_size, дабы получить правильную длину и воспользовались NSData, чтобы распечатать всё в hex. Вот, что у нас получилось на выходе:
2009-01-27 15:58:04.904 a.out[22090:10b] Object contains <20300000 aaaaaaaa bbbbbbbb cccccccc>
Мы видим, что класс последовательно заполнил ячейки памяти—сначала переменную A, потом его наследника B, а потом C. Всё просто!
Но что за 20300000 в самом начале? Они идут перед A, и потому, скорее всего, принадлежат NSObject. Посмотрим-ка на определение NSObject.
/*********** Base class ***********/
@interface NSObject {
Class isa;
}
Как видим, вновь какая-то пременная. Но что это за Class такой? Переходим по определению, которое нам предлагает Xcode и попадаем в usr/include/objc/objc.h, в котором находим следущее:
typedef struct objc_class *Class;
Идём дальше в /usr/include/objc/runtime.h и видим:
struct objc_class {
Class isa;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
Таким образом, Class это указатель на структуру, которая… Начинается с ещё одного Class
Посмотрим ещё один класс, NSProxy
@interface NSProxy {
Class isa;
}
И тут он есть. Ещё один, id, за которым может скрываться любой объект в Objective-C
typedef struct objc_object {
Class isa;
} *id;
И снова он. Очевидно, что каждый объект в Objective-C должен начинаться с Class isa, даже объекты классов. Так что же это такое?
Как следует из названия и типа, переменная isa указывает, какому классу принадлежит тот или иной объект. Каждый объект в Objective-C должен начинаться с isa, иначе runtime не будет знать, что же с ним делать. Вся информация о типе каждого конкретного объекта скрывается за этим крохотным указателем. Оставшийся кусок объекта, с точки зрения runtime, представляет из себя просто огромный BLOB, не дающий никакой информации. Только лишь классы могут придать этому куску какой-то смысл.
Классы
Что же тогда на самом деле содержится в классах? Ответ на этот вопрос нам поможет найти «недоступные » поля структуры (те, что после #if !__OBJC2__, они оставлены здесь для совместимости с пре-Леопардом, и вы не должны пользоваться ими, если занимаеться разработкой пол Леопард и выше, однако, они помогут понять нам, что же за информация там скрывается). Сначала идет isa, позволяющий работать с классом, как с объектом. Потом идет ссылка на Class — предок, дабы не нарушалась иерархия классов. Далее идет некоторая базовая информация о классе. Но самое интересное — в конце. Это список переменных, список методов и список протоколов. Все это доступно во время исполнения, и может быть изменено там же!
Я пропустил кэш, так как он не слишком интересен с точки зрения манипуляции во время исполнения, но стоит рассказать о том, какую роль он играет в принципе. Каждый раз, когда вы посылаете сообщение ([foo bar]), Runtime ищет его в списке методов класса объекта. Но так как это просто линейный список, этот процесс достаточно продолжительный. Кэш же — это хэш таблица, в которой содержатся уже вызывавшиеся до этого методы. Именно поэтому первый вызов метода может быть значительно дольше, чем все последующие.
Исследуя runtime.h, вы можете обнаружить множество функций для доступа и изменению этих элементов. Каждая функция начинается с префикса, который показывает, с чем она имеет дело. Базовые начинаются на objc_, функции для работы с классами на class_, и так далее. Например, вы можете вызвать class_getInstanceMethod, чтобы узнать информацию о конкретном методе, такую как список аргументов/тип возвращаемого значения. Или же можете добавить новый метод с помощью class_addMethod. Вы даже можете создавать целые классы с помощью objc_allocateClassPair прямо во время исполнения!
Практическое применеие
Есть множество вариантов применеия этой Runtime мета-информации, вот только некоторые из них
- Автоматический поиск переменных/методов. Apple уже реализовало это в виде Key-Value Coding: вы задаете имя, в соответсвии с этим именем получаете переменную или метод. Вы можете делать это и сами, если вас чем-то не устраивает реализация Apple.(прим. переводчика — например, вот так Better key-value observing for Cocoa)
- Автоматическая регистрация/вызов подклассов. Используя objc_getClassList вы можете получить список классов, уже известных Runtime и, проследив иерархию классов, выяснить, какие подклассы наследуются из данного класса. Это дает вам возможность писать подклассы, которые будут отвечать за специфичиские форматы данных, или что-то в этом роде, и потом дать суперклассу возможность находить их самому, избавив себя от утомительной необходимости регистрировать их все руками.
- Автоматически вызвывать метод для каждого класса. Это может быть полезно для unit-testing фреймворков и подобного рода вещей. Очень похоже на #2, но с акцентом на поиск возможных методов, а не на иерархию классов
- Перегрузка методов во время исполнения. Runtime предоставляет вам полный набор инструментов для изменения реализации методов классов, без необходимости изменять что-либо в их исходном коде
- Bridging Имеея возможность динамически создавать классы и просматривать необходимые поля, вы можете создать мост между Objective-C и другим (достаточно динамичным) языком.
- И многое, многое другое! Не ограничивайте себя списком, представленным выше.
Заключение
Objective-C — это мощный язык, ключевую роль в динамичности которого играет всеохватывающий Runtime API. Быть может, не так уж и приятно возиться со всем этим C кодом, но возможности, которые он открывает, поистине огромны.
(прим. переводчика — для тех, кому уже не терпится поиграться со всей этой бесконечной динамичностью Objective-C, но не хочется разбираться с runtime.h, Mike Ash выложил на GitHub проект — обертку над runtime.h, предоставляющую полный доступ ко всем вкусностям, описаным выше, но в привычном Objective-C синтаксисе.)
Автор: pestrov