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. Наприклад, ми не розібралися з поверненням результату викликаються методів. Але про це читайте в наступних частинах :).

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.