[Из песочницы] Objective-C Runtime для Си-шников. Часть 1
При первом знакомстве с Objective C он произвёл на меня впечатление уродливого и нелогичного языка. На тот момент я уже имел достаточно сильную базу в C/C++ и ассемблере x86, а так же был знаком с другими высокоуровневыми языками. В документации писалось, что Objective C это расширение языка C. Но, как бы я ни старался, мне всё же не удавалось применить свой опыт в разработке приложений для iOS.Сегодня он всё так же кажется мне уродливым. Но однажды окунувшись в глубины Objective-C Runtime я влюбился в него. Изучение Objective-C Runtime позволило мне найти те тонкие ниточки, которые связывают Objective C с его «отцом» — великолепным и непревзойдённым языком C. Это тот самый случай, когда любовь превращает недостатки в достоинства.
Если вам интересно взглянуть на Objective C не просто как на набор операторов и базовых фреймворков, а понять его низкоуровневое устройство, прошу под кат.
Небольшое уточнение В своих статьях я буду часто путать Objective C, Objective-C Runtime, iOS SDK, iOS, iPhone и т.д. Не потому что я не понимаю разницы между ними, а потому что так будет проще объяснить суть вещей, не раздувая статьи до всеобъемлющего мануала по языку C и BSD-based системам. Поэтому большая просьба писать комментарии с уточнениями в терминологии только там, где это действительно имеет принципиальное значение.
Давайте взглянем на привычную нам конструкцию: [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
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
@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 (0×100000f6d) for message 'someMethod' does not match selector known to Objective C runtime (0×100000f82)-- abort2015–02–18 14:03:23.178 ObjCRuntimeTest[4756:1861470] -[TestClass someMethod]: unrecognized selector sent to instance 0×1002069c0
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.