При первом знакомстве с Objective C он произвёл на меня впечатление уродливого и нелогичного языка. На тот момент я уже имел достаточно сильную базу в C/C++ и ассемблере x86, а так же был знаком с другими высокоуровневыми языками. В документации писалось, что Objective C это расширение языка C. Но, как бы я ни старался, мне всё же не удавалось применить свой опыт в разработке приложений для iOS.
Сегодня он всё так же кажется мне уродливым. Но однажды окунувшись в глубины Objective-C Runtime я влюбился в него. Изучение Objective-C Runtime позволило мне найти те тонкие ниточки, которые связывают Objective C с его «отцом» — великолепным и непревзойдённым языком C. Это тот самый случай, когда любовь превращает недостатки в достоинства.
Если вам интересно взглянуть на Objective C не просто как на набор операторов и базовых фреймворков, а понять его низкоуровневое устройство, прошу под кат.
Немного о «вызовах методов»
Давайте взглянем на привычную нам конструкцию:
[myObject someMethod];
Обычно это называют «вызвать метод». Дотошные iOS-разработчики называют это «послать сообщение объекту», в чем они, безусловно правы. Потому что какие бы «методы» и каких бы объектов вы ни вызывали, в конечном итоге такая конструкция будет преобразована компилятором в самый обычный вызов функции objc_msgSend:
objc_msgSend(myObject, @selector(someMethod));
Таким образом, все, что делает взятая нами конструкция — всего лишь вызывает функцию objc_msgSend.
Из названия можно понять, что происходит какая-то посылка сообщения. Об этой функции мы поговорим позже, потому что уже с имеющейся у нас на руках информацией мы сталкиваемся с неизвестной для нас конструкцией @selector(), в которой я и предлагаю разобраться в первую очередь.
Знакомимся с селекторами
Посмотрев в документацию, мы узнаем что сигнатура функции objc_msgSend(...) имеет следующий вид:
id objc_msgSend ( id self, SEL op, ... );
Раз обычная Си-шная функция принимает в качестве параметра аргумент типа SEL, значит, об этом самом типе SEL мы можем узнать подробнее, если захотим.
Исходя из документации, мы узнаем что существует два способа получить селектор (для нас — объект типа SEL):
- Во время компиляции: SEL aSelector = @selector(methodName);
- Во время выполнения: SEL aSelector = NSSelectorFromString(@"methodName");
Что же, нас интересует именно runtime, поэтому опять же из документации имеем следующую информацию:
SEL NSSelectorFromString ( NSString *aSelectorName );
Чтобы создать селектор, NSSelectorFromString передаёт aSelectorName в функцию sel_registerName в виде строки UTF-8 и возвращает значение, полученное из вызываемой функции. Заметьте также, что если селектор не существует, то будет возвращён вновь зарегистрированный селектор.
Вот это уже интереснее и ближе к нашему Си-шному мировосприятию. Просыпается интерес копнуть чуть глубже.
Тут я, конечно же, понимаю, что уже изрядно надоел вам своими ссылками на документацию, но по другому никак. Поэтому снова из читаем документацию к методу sel_registerName и, о чудо, эта функция принимает в качестве аргумента самую обычную C-строку!
SEL sel_registerName ( const char *str );
Что ж, это максимальный уровень, до которого мы можем опуститься на основе документации. Все что пишется об этой функции, так это то, что она регистрирует метод в Objective-C Runtime, преобразовывает имя метода в селектор и возвращает его.
В принципе, этого нам достаточно для того, чтобы понять каким образом работает конструкция @selector(). А если недостаточно, то можете посмотреть исходный код этой функции, он доступен прямо на сайте Apple. В первую очередь в этом файле интересна функция
static SEL __sel_registerName(const char *name, int lock, int copy) {
Однако, непонятным остается момент с типом SEL. Все, что мне удалось найти, так это то, что он является указателем на структуру objc_selector:
typedef struct objc_selector *SEL;
Исходного кода структуры objc_selector я не нашел. Где-то были упоминания, что это обычная C-строка, но этот тип является полностью прозрачным и я ни в коем случае не должен вдаваться в детали его реализации, потому что Apple может в любой момент её изменить. Но для нас с вами это не ответ на вопрос. Поэтому все что нам остается делать, это вооружиться нашим любимым LLDB и получить эту информацию самостоятельно.
Для этого мы напишем простой код:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
SEL mySelector = NSSelectorFromString(@"mySelector");
return 0;
}
И добавим точку останова на строку «return 0;».
Путём нехитрых манипуляций с LLDB в Xcode мы узнаем, что переменная mySelector в конечном итоге является обычной C-строкой.
Так что же это за структура objc_selector, которая странным образом превращается в строку? Если вы попытаетесь создать объект типа objc_selector, то вряд ли у вас это получится. Дело в том, что структуры objc_selector просто не существует. Разработчики Apple использовали этот хак, чтобы C-строки не были совместимы с объектами типа SEL. Почему? Потому что механизм селекторов в любой момент может измениться, и абстрагирование от понятия C-строк позволит вам избежать неприятностей при дальнейшей поддержке своего кода.
Тогда логично сделать вывод, что мы можем написать следующий код, который должен работать:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface TestClass : NSObject
- (void)someMethod;
@end
@implementation TestClass
- (void)someMethod {
NSLog(@"Hello from method!");
}
@end
int main(int argc, const char * argv[]) {
TestClass * myObj = [[TestClass alloc] init];
SEL mySelector = (SEL)"someMethod";
objc_msgSend(myObj, mySelector);
return 0;
}
Но такой код падает со следующим пояснением:
2015-02-18 14:03:23.152 ObjCRuntimeTest[4756:1861470] *** NSForwarding: warning: selector (0x100000f6d) for message 'someMethod' does not match selector known to Objective C runtime (0x100000f82)-- abort
2015-02-18 14:03:23.178 ObjCRuntimeTest[4756:1861470] -[TestClass someMethod]: unrecognized selector sent to instance 0x1002069c0
Objective C Runtime сказал нам, что он не знает о таком селекторе, которым мы попытались оперировать. И мы уже знаем почему — мы должны зарегистрировать селектор с помощью функции sel_registerName().
Здесь я прошу обратить внимание, что я привел именно две строки вывода ошибок. Дело в том, что когда вы просто оперируете селектором, который получили с помощью @selector(someMethod), и посылаете сообщение какому-то объекту, то вам выдается только ошибка «unrecognized selector sent to instance». Но в нашем случае нам перед этим сказали, что Objective C Runtime не знает такого селектора. На основе этого можно сделать вывод, что селекторы не имеют никакого отношения к объектам. То есть, если у двух объектов совершенно разных классов будет метод:
- (void)myMegaMethod;
, то для вызова этого метода для обоих объектов будет использоваться один и тот же селектор, зарегистрированный вами в runtime с помощью конструкции
SEL myMegaSelector = @selector(myMegaMethod);
Что же значит «зарегистрированный селектор»? Чтобы не вдаваться в детали реализации sel_registerName(), я объясню это так: вы передаете этой функции C-строку, а в ответ она вам возвращает копию этой строки. Почему копию? Потому что он копирует переданный вами идентификатор в свою, более понятную ему область памяти, и отдает вам указатель на строку, которая находится именно в этой самой «понятной» для него области памяти. О том, что это за область памяти, мы поговорим с вами позже.
Подведение итогов
Мы вдоволь начитались документации, поигрались с отладчиком, но теперь надо бы это дело подытожить в наших с вами Си-шных головах.
Теперь мы может сделать «вызов метода» исключительно средствами языка C:
SEL mySelector = sel_registerName("myMethod");
objc_msgSend(myObj, mySelector);
То есть нам не врали: Objective-C действительно совместим с языком C, являясь его расширением.
На этом мне хотелось бы закончить первую часть и позволить вам насладиться этим приятным ощущением хоть и небольшого, но все же понимания принципов работы Objective-C Runtime.
Автор: aperechnev