0,0
рейтинг
22 февраля 2015 в 11:47

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



Снова здравствуйте. Мой цикл статей посвящён тем программистам, которые перешли с языка 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. Например, мы не разобрались с возвратом результата из вызываемых методов. Но об этом читайте в следующих частях :).
Alex Krzyżanowski @aperechnev
карма
12,0
рейтинг 0,0
iOS Developer
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (16)

  • +1
    Казалось бы, это уже все известно, но приятно прочитать структурированную статью со ссылками. Спасибо.
  • 0
    Например, мы не разобрались с возвратом результата из вызываемых методов

    Хорошо бы коснуться type encoding у методов классов. Как это работает и зачем оно вообще нужно. Потому как на уровне асссемблера это все не нужно. Есть calling conventions, они все сделают — все методы используют стандартные ARM calling conventions, если говорить об iOS. С переопределением методов очень просто передать в метод аргументы совершенно не тех типов, либо вообще передать меньше, чем надо. Получишь или UB, или segmentation fault. Поэтому интересно, что с этой информацией делает рантайм.

    Единственное место, где реально нужно знать type encoding, это NSInvocation, где аргументы передаются как массивы байтов, а типы берутся как раз из type encoding. Естественно, передать неправильные аргументы там можно запросто.

    Насчет возвращаемого значения, опять же, calling conventions все за нас сделают. Они на то и придуманы, чтобы все работало само собой, так сказать.
    • 0
      Возможно, это нужно, чтобы распарсить vararg-список у функции objc_msgSend?
      • 0
        Но зачем его парсить? objc_msgsend ничего с аргументами делать не должен — только найти реализацию метода и передать управление туда с сохранением состояния регистров и стека перед вызовом objc_msgsend. На уровне асма все вызовы методов превращаются в обычный вызов C функции со стандартным calling convention — несколько аргументов в регистрах, остальное в стеке. Опять же, с переопределением методов для jailbreak твиков можно много чего творить со списком аргументов. Обычное дело — заменить все аргументы на void* или id, если не знаешь их типов или вообще пофиг на них.
        • 0
          Я не очень понял, как передать управление с сохранением состояния стека до вызова, если не знаешь размер списка аргументов.
          • 0
            Ну как. На входе objc_msgsend в стек кладем все регистры, где обычно лежат аргументы. Дальше делаем свои дела, находим функцию. Восстанавливаем все регистры и возвращаем указатель стека на место, где он был на входе в objc_msgsend. Т.е. все регистры остаются как были, а стек указывает на то место, где лежат остальные аргументы. Вызываем наконец метод уже после эпилога objc_msgsend. Все выглядит так, будто не objc_msgsend вызвали, а просто напрямую метод класса. Просто перенаправляем вызов по нужному адресу.
            • 0
              Действительно, чудеса какие :) А это все ваши измышления или оно действительно так работает?
              • 0
                Это мои размышления на основе того, как работает вызов функций вообще — поэтому у меня и вопрос на счет того, зачем type encoding нужен рантайму, если на уровне асма все и без него по идее должно рабоать. Суть в том, чтобы после вызова вернуть все в прежнее состояние. Это и может сделать objc_msgsend с той лишь разницей, что после эпилога он сделает вызов найденного метода и получится, будто мы передали все эти аргументы прямо в метод, а не в objc_msgsend. Главное не трогать аргументы и стек, в котором могут лежать другие аргументы. Сделать это не так сложно — calling conventions действительно творят чудеса
    • +3
      Дело не совсем в этом. Подождите следующей статьи, я всё расскажу. К сожалению, эта статья изначально получилась слишком объемной, поэтому мне пришлось её укоротить и вынести некоторую информацию в следующую часть. В общем, цикл статей не заканчивается на этой.
  • +2
    Огромное спасибо за продолжение, жду следующих статей!
    Некоторые мысли (я не имею практического знания ObjC, но тема очень интересная, какие-то мысли приходят в голову...)
    У каждого объекта класса есть скрытое поле с именем isa, которое указывает на специальный объект, описывающий класс в рантайме. Там, в этом объекте хранится по сути таблица соответствия селекторов и функций. Это похоже на таблицу виртуальных функций? Было бы интересно сравнить. И еще интересно, как оно устроено в ObjectiveC++, где по идее должно как-то совмещаться?
    Существуют ли штатные средства подключения и отключения методов к классам? (по идее это добавление и удаление записей из таблицы соответсивия селекторов функциям)
    Можно ли «переключить» объект на другой «объект класса» (то есть переписать isa)?
    Можно ли делать копии «объектов классов»?
    • +1
      На эти и многие другие вопросы можно найти ответы здесь developer.apple.com/library/mac/documentation/Cocoa/Reference/ObjCRuntimeRef/index.html Рантайм умеет многое. Что касается objc++, то я не слышал, чтобы он чем-то отличался особенным. Это просто расширение для частичной поддержки C++. Основные проблемы конечно, когда мешаешь objc и C++, но так все очень прилично.
    • 0
      Нет, в Obj-C++ никакого совмещения нет, C++ объекты отдельно, Obj-C объекты отдельно.
      • 0
        Отнаследовать класс сразу от NSObject и от C++ класса с виртуальными функциями нельзя?
        • 0
          Нет, объектные системы плюсов и Obj-C разделены.
  • 0
    Неплохо было бы сделать в начале статьи ссылку на первую часть однако.
  • +1
    >будет помещен в некую кеш-таблицу
    Это называется inline caching, см. en.wikipedia.org/wiki/Inline_caching

    И почитайте про устройство рантайма Smalltalk, многое будет намного понятнее. Можно почти сказать, что Obj-C это Smalltalk + C.
    Уверен, тогда не будете считать «умниками» тех, кто говорит, что посылка сообщений — это посылка сообщений.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.