Pull to refresh

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

Reading time 8 min
Views 20K


Снова здравствуйте. Мой цикл статей посвящён тем программистам, которые перешли с языка C на Objective-C, и хотели бы ответить для себя на вопросы «каким именно образом Objective-C основывается на языке C?» и «как это все происходит изнутри?».

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

  1. Мои статьи — не руководство по Objective C. Мы изучаем именно Objective-C Runtime настолько низкоуровнево, чтобы понимать его на уровне языка C.
  2. Мои статьи — не руководство по языку C и дебаггерам. Мы опускаемся до уровня языка C, но не ниже. Поэтому такие вопросы, как представление данных в памяти, я не затрагиваю. Предполагается, что вы знаете все это и без меня.


Конечно, статьи будут интересны так же и другим категориям программистов. Но имейте ввиду эти два пункта.

Если вы еще не читали первую статью, то настоятельно рекомендую прочитать сначала её: http://habrahabr.ru/post/250955/. А если уже читали, то добро пожаловать под кат.

Мы «вызываем методы», а педанты «посылают сообщения»



В предыдущей статье мы с вами разбирались с «вызовом методов» или, как его еще называют, «посылкой сообщений»:

[myObj someMethod];


Мы пришли к выводу, что во время выполнения такая конструкция в итоге сводится к вызову функции objc_msgSend() и хорошенько разобрались с селекторами.

Теперь давайте так же подробно разберемся с функцией objc_msgSend(), чтобы понять принципы этой пресловутой посылки сообщений объекту.

Эта функция вызывается каждый раз, когда вы вызываете метод какого-либо объекта. Логично предположить, что скорость её работы очень сильно влияет на скорость работы всего приложения. Поэтому если вы посмотрите на исходный код этой функции, то обнаружите, что реализована она на ассемблере для каждой из платформ.

Прежде чем разбираться с исходным кодом, я предлагаю ознакомиться с документацией:



Функция посылки сообщения делает всё, что нужно для динамического связывания:

  • В первую очередь она находит процедуру (реализацию метода), на которую ссылается селектор. Так как один и тот же метод может быть реализован абсолютно разными классами, та самая процедура, которую она (функция objc_msgSend, прим. автора) ищет, зависит от класса получателя (которому мы посылаем сообщение, прим. автора).
  • Затем она вызывает эту процедуру, передавая ей объект получателя (указатель на него) и все аргументы, которые были переданы в вызове метода.
  • И, наконец, она возвращает результат работы процедуры как свой собственный результат.

...


Лирическое отступление
Уже на основе одной только документации мы понимаем, что словосочетание «вызвать метод» абсолютно корректно в применении к языку Objective C. Поэтому, если какой-то умник поправляет вас, мол корректно говорить «послать сообщение» а не «вызвать метод», то можете смело отправить его на два известных слова — чтение документации.


Что же, со вторым и третьим пунктом все итак понятно. А вот с первым нужно разобраться чуть подробнее: каким именно образом вполне абстрактный селектор преобразовывается во вполне конкретную функцию.

Общаемся с методами классов на языке C



Раз знакомая нам функция objc_msgSend() в первую очередь ищет функцию, которая реализует вызываемый метод, значит мы можем найти эту функцию и вызвать её самостоятельно.

Давайте напишем небольшую тестовую программу, которая позволит нам познакомиться с вызовом методов чуточку ближе:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface TestClass : NSObject
- (void)someMethod;
- (void)callSomeMethod;
- (void)methodWithParam:(const char *)param;
@end

@implementation TestClass
- (void)someMethod {
  NSLog(@"Hello from %p.%s!", self, _cmd);
}
- (void)callSomeMethod {
  NSLog(@"Hello from %p.%s!", self, _cmd);
  [self someMethod];
}
- (void)methodWithParam:(const char *)param {
  NSLog(@"Hello from %p.%s! My parameter is: <%s>", self, _cmd, param);
}
@end

int main(int argc, const char * argv[]) {
  TestClass * myObj = [[TestClass alloc] init];
  [myObj someMethod];
  [myObj callSomeMethod];
  [myObj methodWithParam:"I'm a parameter"];
  return 0;
}


Из документации нам становится известно, что при вызове нужной функции, objc_msgSend() передает в нее параметры в следующем порядке:

  1. Указатель на объект, метод которого мы вызвали
  2. Селектор, по которому мы вызвали метод
  3. Остальные аргументы, которые мы передали методу


Именно потому наша тестовая программа выглядит так: в каждом из методов мы выводим в лог self и _cmd, в которых находятся указатель «на себя» и селектор соответственно.

Если вы запустите эту программу, то вывод будет примерно следующим:

2015-02-21 12:43:18.817 ObjCRuntimeTest[7092:2454834] Hello from 0x1002061f0.someMethod!
2015-02-21 12:43:18.818 ObjCRuntimeTest[7092:2454834] Hello from 0x1002061f0.callSomeMethod!
2015-02-21 12:43:18.819 ObjCRuntimeTest[7092:2454834] Hello from 0x1002061f0.someMethod!
2015-02-21 12:43:18.819 ObjCRuntimeTest[7092:2454834] Hello from 0x1002061f0.methodWithParam:! My parameter is: <I'm a parameter>


Теперь попробуем вызвать эти методы средствами языка C. Для этого мы возьмем из объекта указатель на функцию, реализующую метод нашего класса. Учитывая то, что работаем мы на уровне языка C, нам следуем определить типы, которые позволят нам работать с указателями на наши функции. Учитывая все это, имеем следующий код в функции main():

int main(int argc, const char * argv[]) {
  typedef void (*MethodWithoutParams)(id, SEL);
  typedef void (*MethodWithParam)(id, SEL, const char *);
  
  TestClass * myObj = [[TestClass alloc] init];
  
  MethodWithoutParams someMethodImplementation = [myObj methodForSelector:@selector(someMethod)];
  MethodWithoutParams callSomeMethodImplementation = [myObj methodForSelector:@selector(callSomeMethod)];
  MethodWithParam methodWithParamImplementation = [myObj methodForSelector:@selector(methodWithParam:)];
  
  someMethodImplementation(myObj, @selector(someMethod));
  callSomeMethodImplementation(myObj, @selector(callSomeMethod));
  methodWithParamImplementation(myObj, @selector(methodWithParam:), "I'm a parameter");
  
  return 0;
}


Что же, мы уже вызвали методы исключительно средствами языка C. Исключение в данном случае составили только селекторы, с которыми мы уже достаточно разобрались в предыдущей статье. А чёрным ящиком для нас остался лишь метод methodForSelector:.

Механизм сообщений в Objective C Runtime



Ключевой момент в реализации механизма сообщений в Objective C Runtime заключается в том, каким образом компилятор представляет ваши классы и объекты.

Если выражаться в терминах языка C++, то объекты в оперативной памяти создаются не только для каждого из экземпляров ваших классов, но и для каждого класса. То есть, описав класс, который наследует базовый класс NSObject, и создав два экземпляра этого класса, во время исполнения вы получите два созданных вами объекта и один объект вашего класса.

Этот самый объект класса содержит в себе указатель на объект родительского класса и таблицу соответствия селекторов и адресов функций, называемую dispatch table. Именно с помощью этой таблицы функция objc_msgSend() и ищет нужную функцию, которую нужно вызвать для переданного ей селектора.

Каждый класс, который наследуется от NSObject или NSProxy, имеет поле isa, которое как раз таки и является указателем на объект класса. Когда вы вызываете метод у какого либо объекта, функция objc_msgSend() переходит по указателю isa на объект класса, и ищет в нем адрес функции, реализующей этот метод. Если он не находит такой функции, то он переходит на объект класса родительского объекта и ищет эту функцию там. Так происходит до тех пор, пока нужная функция не будет найдена. Если функция не была найдена нигде, в том числе и в объекте класса NSObject, то выдается всем нам известное исключение:

unrecognized selector sent to instance ...


А на самом деле...
В настоящее время достаточно медленный процесс поиска функций немного улучшен. Если вы вызываете метод какого-либо объекта, то он, будучи найденным однажды, будет помещен в некую кеш-таблицу. Таким образом, если вы вызовете метод methodForSelector: у какого-либо объекта, то в первый раз будет произведён поиск нужной функции, и когда функция будет найдена в объекте класса NSObject, она будет закеширована в таблице вашего класса, и в следующий раз поиск этой функции уже не займёт много времени.

Кроме того, исключение произойдет не сразу, если реализация метода не будет найдена. Существует так же и такой механизм, как Message Forwarding.


Давайте подтвердим это реальными исследованиями на основе исходного кода Objective-C Runtime и класса NSObject.

Как мы уже поняли, у NSObject есть метод methodForSelector:, исходный код которого выглядит так:

+ (IMP)methodForSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return object_getMethodImplementation((id)self, sel); // self - указатель на объект класса
}

- (IMP)methodForSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return object_getMethodImplementation(self, sel); // self - указатель на наш объект
}


Как мы видим, этот метод реализован и для самого класса, и для объектов класса. В обоих случаях используется одна и та же функция object_getMethodImplementation():

IMP object_getMethodImplementation(id obj, SEL name)
{
    Class cls = (obj ? obj->getIsa() : nil);
    return class_getMethodImplementation(cls, name);
}


Стоп! Что это за конструкция "(obj? obj->getIsa(): nil)" !? Ведь во всех статьях нам говорят…



А все дело начинается с build settings файла проекта Objective C Runtime:

CLANG_CXX_LANGUAGE_STANDARD = «gnu++0x»;
CLANG_CXX_LIBRARY = «libc++»;


И вот вам реализация вполне себе си-плю-плюшного метода getIsa():

inline Class 
objc_object::getIsa() 
{
    if (isTaggedPointer()) {
        uintptr_t slot = ((uintptr_t)this >> TAG_SLOT_SHIFT) & TAG_SLOT_MASK;
        return objc_tag_classes[slot];
    }
    return ISA();
}


В общем, так уж получилось, что любой объект в Objective C должен содержать в себе поле isa. И объект класса — не исключение.

Вся эта порнография довольно запутанна. Метод methodForSelector: имеет абсолютно идентичную реализацию как в качестве метода объекта, так и для метода класса. Отличие заключается лишь в том, что в первом случае self указывает на наш объект, а во втором — на объект класса.

Чёрт возьми, какого хрена!? Каким образом мы можем вызвать obj->getIsa() у объекта класса? Что там вообще происходит?

А дело в том, что у объекта класса действительно есть такое же поле, которое указывает на «объект класса для этого класса». Если выражать правильно, то оно указывает на метакласс. Если вы вызываете метод объекта (тот метод, который начинается со знака "-"), то его реализация ищется в его классе. Если же вы вызываете метод класса (начинается со знака "+"), то его реализация ищется в его метаклассе.

Я немного наврал вам в начале статьи, сказав что во время исполнения при создании двух объектов своего класса вы получаете три объекта: два экземпляра вашего класса и объект класса. На самом деле объект класса всегда создается в паре с объектом метакласса. То есть, в конечном итоге вы получите 4 объекта.

Чтобы визуально представить себе всю суть этого беспредела, я вставлю тут картинку из этой статьи:



Вернемся к нашему случаю, где через self в конце концов вызывается функция class_getMethodImplementation():

IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

    if (!cls  ||  !sel) return nil;

    imp = lookUpImpOrNil(cls, sel, nil, 
                         YES/*initialize*/, YES/*cache*/, YES/*resolver*/);

    // Translate forwarding function to C-callable external version
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}


Любознательные могут проследить, что функция lookUpImpOrNil() использует функцию lookUpImpOrForward(), реализация которой лежит опять таки на сайте Apple. Функция написана на C, что позволит убедиться в том, что все работает именно так, как и написано в документации.

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



Ну и напоследок, как в прошлый раз давайте вызовем метод исключительно средствами языка C:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface TestClass : NSObject
@end

@implementation TestClass
+ (void)someClassMethod {
  NSLog(@"Hello from some class method!");
}
- (void)someInstanceMethod {
  NSLog(@"Hello from some instance method!");
}
@end

int main(int argc, const char * argv[]) {
  typedef void (*MyMethodType)(id, SEL);
  
  TestClass * myObj = [[TestClass alloc] init];
  
  Class myObjClassObject = object_getClass(myObj);
  Class myObjMetaclassObject = object_getClass(myObjClassObject);
  
  MyMethodType instanceMethod = class_getMethodImplementation(myObjClassObject, @selector(someInstanceMethod));
  MyMethodType classMethod = class_getMethodImplementation(myObjMetaclassObject, @selector(someClassMethod));
  
  instanceMethod(myObj, @selector(someInstanceMethod));
  classMethod(myObjClassObject, @selector(someClassMethod));
  
  return 0;
}


На самом деле, мы все еще далеки от понимания механизма сообщений в Objective C. Например, мы не разобрались с возвратом результата из вызываемых методов. Но об этом читайте в следующих частях :).
Tags:
Hubs:
+36
Comments 16
Comments Comments 16

Articles