- PVSM.RU - https://www.pvsm.ru -

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.

Objective-C Runtime для Си-шников. Часть 1 - 1

Из названия можно понять, что происходит какая-то посылка сообщения. Об этой функции мы поговорим позже, потому что уже с имеющейся у нас на руках информацией мы сталкиваемся с неизвестной для нас конструкцией &#64selector(), в которой я и предлагаю разобраться в первую очередь.

Знакомимся с селекторами

Посмотрев в документацию [1], мы узнаем что сигнатура функции objc_msgSend(...) имеет следующий вид:

id objc_msgSend ( id self, SEL op, ... );

Раз обычная Си-шная функция принимает в качестве параметра аргумент типа SEL, значит, об этом самом типе SEL мы можем узнать подробнее, если захотим.

Исходя из документации [2], мы узнаем что существует два способа получить селектор (для нас — объект типа SEL):

  1. Во время компиляции: SEL aSelector = &#64selector(methodName);
  2. Во время выполнения: SEL aSelector = NSSelectorFromString(&#64&#34methodName&#34);

Что же, нас интересует именно runtime, поэтому опять же из документации [3] имеем следующую информацию:

SEL NSSelectorFromString ( NSString *aSelectorName );

Чтобы создать селектор, NSSelectorFromString передаёт aSelectorName в функцию sel_registerName в виде строки UTF-8 и возвращает значение, полученное из вызываемой функции. Заметьте также, что если селектор не существует, то будет возвращён вновь зарегистрированный селектор.

Вот это уже интереснее и ближе к нашему Си-шному мировосприятию. Просыпается интерес копнуть чуть глубже.

Тут я, конечно же, понимаю, что уже изрядно надоел вам своими ссылками на документацию, но по другому никак. Поэтому снова из читаем документацию [4] к методу sel_registerName и, о чудо, эта функция принимает в качестве аргумента самую обычную C-строку!

SEL sel_registerName ( const char *str );

Что ж, это максимальный уровень, до которого мы можем опуститься на основе документации. Все что пишется об этой функции, так это то, что она регистрирует метод в Objective-C Runtime, преобразовывает имя метода в селектор и возвращает его.

В принципе, этого нам достаточно для того, чтобы понять каким образом работает конструкция &#64selector(). А если недостаточно, то можете посмотреть исходный код этой функции, он доступен прямо на сайте [5] Apple. В первую очередь в этом файле интересна функция

static SEL __sel_registerName(const char *name, int lock, int copy) {

Однако, непонятным остается момент с типом SEL. Все, что мне удалось найти [6], так это то, что он является указателем на структуру objc_selector:

typedef struct objc_selector 	*SEL;

Исходного кода структуры objc_selector я не нашел. Где-то были упоминания, что это обычная C-строка, но этот тип является полностью прозрачным и я ни в коем случае не должен вдаваться в детали его реализации, потому что Apple может в любой момент её изменить. Но для нас с вами это не ответ на вопрос. Поэтому все что нам остается делать, это вооружиться нашим любимым LLDB [7] и получить эту информацию самостоятельно.

Для этого мы напишем простой код:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
  SEL mySelector = NSSelectorFromString(@"mySelector");
  return 0;
}

И добавим точку останова на строку «return 0;».

Objective-C Runtime для Си-шников. Часть 1 - 2

Путём нехитрых манипуляций с 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().

Здесь я прошу обратить внимание, что я привел именно две строки вывода ошибок. Дело в том, что когда вы просто оперируете селектором, который получили с помощью &#64selector(someMethod), и посылаете сообщение какому-то объекту, то вам выдается только ошибка «unrecognized selector sent to instance». Но в нашем случае нам перед этим сказали, что Objective C Runtime не знает такого селектора. На основе этого можно сделать вывод, что селекторы не имеют никакого отношения к объектам. То есть, если у двух объектов совершенно разных классов будет метод:

- (void)myMegaMethod;

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

SEL myMegaSelector = &#64selector(myMegaMethod);

Что же значит «зарегистрированный селектор»? Чтобы не вдаваться в детали реализации sel_registerName(), я объясню это так: вы передаете этой функции C-строку, а в ответ она вам возвращает копию этой строки. Почему копию? Потому что он копирует переданный вами идентификатор в свою, более понятную ему область памяти, и отдает вам указатель на строку, которая находится именно в этой самой «понятной» для него области памяти. О том, что это за область памяти, мы поговорим с вами позже.

Подведение итогов

Мы вдоволь начитались документации, поигрались с отладчиком, но теперь надо бы это дело подытожить в наших с вами Си-шных головах.

Теперь мы может сделать «вызов метода» исключительно средствами языка C:

SEL mySelector = sel_registerName("myMethod");
objc_msgSend(myObj, mySelector);

То есть нам не врали: Objective-C действительно совместим с языком C, являясь его расширением.

На этом мне хотелось бы закончить первую часть и позволить вам насладиться этим приятным ощущением хоть и небольшого, но все же понимания принципов работы Objective-C Runtime.

Автор: aperechnev

Источник [8]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/ios/83342

Ссылки в тексте:

[1] документацию: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ObjCRuntimeRef/index.html#//apple_ref/c/func/objc_msgSend

[2] документации: https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/Selector.html#//apple_ref/doc/uid/TP40008195-CH48-SW2

[3] документации: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Functions/#//apple_ref/c/func/NSSelectorFromString

[4] документацию: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ObjCRuntimeRef/index.html#//apple_ref/c/func/sel_registerName

[5] сайте: http://www.opensource.apple.com/source/objc4/objc4-371.1/runtime/objc.h

[6] найти: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ObjCRuntimeRef/index.html#//apple_ref/c/tdef/SEL

[7] LLDB: https://ru.wikipedia.org/wiki/LLDB

[8] Источник: http://habrahabr.ru/post/250955/