Pull to refresh

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

Reading time 6 min
Views 31K


При первом знакомстве с 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.



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

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


Посмотрев в документацию, мы узнаем что сигнатура функции objc_msgSend(...) имеет следующий вид:
id objc_msgSend ( id self, SEL op, ... );


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

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

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


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

SEL NSSelectorFromString ( NSString *aSelectorName );

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


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

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

SEL sel_registerName ( const char *str );

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

В принципе, этого нам достаточно для того, чтобы понять каким образом работает конструкция &#64selector(). А если недостаточно, то можете посмотреть исходный код этой функции, он доступен прямо на сайте 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().

Здесь я прошу обратить внимание, что я привел именно две строки вывода ошибок. Дело в том, что когда вы просто оперируете селектором, который получили с помощью &#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.

Update

Спасибо за замечание пользователю 1101_debian о том, каким образом мы пришли к выводу о том, что селектор это обычная C-строка. Попробую пояснить.

К такому выводу мы пришли на основании вывода команды «print mySelector» в консоли LLDB. Это можно увидеть на скриншоте. LLDB сообщил нам о том, что это именно строка. Если бы это была не строка, то LLDB и вывел бы наш селектор соответствующим образом. Например, если мы запустим код:

#import <Foundation/Foundation.h>

struct MyStruct {
  char * someString;
};

typedef struct MyStruct* MyNewType;

int main(int argc, const char * argv[]) {
  MyNewType someObj = malloc(sizeof(struct MyStruct));
  someObj->someString = "fakeSelector";
  
  SEL myRealSelector = (SEL)NSSelectorFromString(@"myMethodIsJustString");
  SEL myCStringSelector = (SEL)"myCStringSelector";
  SEL myFakeSelector = (SEL)someObj;
  
  return 0;
}


То в результат мы получим следующий вывод в отладчике (обратите внимание на значения наших переменных):



Но это уже тема другой статьи, имеющей отношение не к Objective-C Runtime, а к разработке на языке C, представлению данных в памяти и использованию отладчиков.
Tags:
Hubs:
+17
Comments 31
Comments Comments 31

Articles