Попробуем разобраться в базовых функциях этой библиотеки. Материал основан на лекциях, которые мы в Coalla используем для обучения сотрудников.
Что такое Runtime?
Objective-C задумывался как надстройка над языком C, добавляющая к нему поддержку объектно-ориентированной парадигмы. Фактически, с точки зрения синтаксиса, Objective-C — это достаточно небольшой набор ключевых слов и управляющих конструкций над обычным C. Именно Runtime, библиотека времени выполнения, предоставляет тот набор функций, которые вдыхают в язык жизнь, реализуя его динамические возможности и обеспечивая функционирование ООП.
Базовые структуры данных
Функции и структуры Runtime-библиотеки определены в нескольких заголовочных файлах: objc.h
, runtime.h
и message.h
. Сначала обратимся к файлу objc.h
и посмотрим, что представляет из себя объект с точки зрения Runtime:
typedef struct objc_class *Class; typedef struct objc_object { Class isa; } *id;
Мы видим, что объект в процессе работы программы представлен обычной C-структурой. Каждый Objective-C объект имеет ссылку на свой класс — так называемый isa-указатель. Думаю, все видели его при просмотре структуры объектов во время отладки приложений. В свою очередь, класс также представляет из себя аналогичную структуру:
struct objc_class { Class isa; };
Класс в Objective-C — это полноценный объект и у него тоже присутствует isa-указатель на «класс класса», так называемый метакласс в терминах Objective-C. Аналогично, С-структуры определены и для других сущностей языка:
typedef struct objc_selector *SEL; typedef struct objc_method *Method; typedef struct objc_ivar *Ivar; typedef struct objc_category *Category; typedef struct objc_property *objc_property_t;
Функции Runtime-библиотеки
Помимо определения основных структур языка, библиотека включает в себя набор функций, работающих с этими структурами. Их можно условно разделить на несколько групп (назначение функций, как правило, очевидно из их названия):
- Манипулирование классами:
class_addMethod
,class_addIvar
,class_replaceMethod
- Создание новых классов:
class_allocateClassPair
,class_registerClassPair
- Интроспекция:
class_getName
,class_getSuperclass
,class_getInstanceVariable
,class_getProperty
,class_copyMethodList
,class_copyIvarList
,class_copyPropertyList
- Манипулирование объектами:
objc_msgSend
,objc_getClass
,object_copy
- Работа с ассоциативными ссылками
Пример 1. Интроспекция объекта
Рассмотрим пример использования Runtime библиотеки. В одном из наших проектов модель данных представляет собой plain old Objective-C объекты с некоторым набором свойств:
@interface COConcreteObject : COBaseObject @property(nonatomic, strong) NSString *name; @property(nonatomic, strong) NSString *title; @property(nonatomic, strong) NSNumber *quantity; @end
Для удобства отладки хотелось бы, чтобы при выводе в лог печаталась информация о состоянии свойств объекта, а не нечто вроде <COConcreteObject: 0x71d6860>
. Поскольку модель данных достаточно разветвленная, с большим количеством различных подклассов, нежелательно писать для каждого класса отдельный метод description
, в котором вручную собирать значения его свойств. На помощь приходит Objective-C Runtime:
@implementation COBaseObject - (NSString *)description { NSMutableDictionary *propertyValues = [NSMutableDictionary dictionary]; unsigned int propertyCount; objc_property_t *properties = class_copyPropertyList([self class], &propertyCount); for (unsigned int i = 0; i < propertyCount; i++) { char const *propertyName = property_getName(properties[i]); const char *attr = property_getAttributes(properties[i]); if (attr[1] == '@') { NSString *selector = [NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding]; SEL sel = sel_registerName([selector UTF8String]); NSObject * propertyValue = objc_msgSend(self, sel); propertyValues[selector] = propertyValue.description; } } free(properties); return [NSString stringWithFormat:@"%@: %@", self.class, propertyValues]; } @end
Метод, определенный в общем суперклассе объектов модели, получает список всех свойств объекта с помощью функции class_copyPropertyList
. Затем значения свойств собираются в NSDictionary
, который и используется при построении строкового представления объекта. Данный алгоритм раработает только со свойствами, которые являются Objective-C объектами. Проверка типа осуществляется с использованием функции property_getAttributes
. Результат работы метода выглядит примерно так:
2013-05-04 15:54:01.992 Test[40675:11303] COConcreteObject: {
name = Foo;
quantity = 10;
title = bar;
}
Сообщения
Система вызова методов в Objective-C реализована через посылку сообщений объекту. Каждый вызов метода транслируется в соответствующий вызов функции objc_msgSend
:
// Вызов метода [array insertObject:foo atIndex:1]; // Соответствующий ему вызов Runtime-функции objc_msgSend(array, @selector(insertObject:atIndex:), foo, 1);
Вызов objc_msgSent
инициирует процесс поиска реализации метода, соответствующего селектору, переданному в функцию. Реализация метода ищется в так называемой таблице диспетчеризации класса. Поскольку этот процесс может быть достаточно продолжительным, с каждым классом ассоциирован кеш методов. После первого вызова любого метода, результат поиска его реализации будет закеширован в классе. Если реализация метода не найдена в самом классе, дальше поиск продолжается вверх по иерархии наследования — в суперклассах данного класса. Если же и при поиске по иерархии результат не достигнут, в дело вступает механизм динамического поиска — вызывается один из специальных методов: resolveInstanceMethod
или resolveClassMethod
. Переопределение этих методов — одна из последних возможностей повлиять на Runtime:
+ (BOOL)resolveInstanceMethod:(SEL)aSelector { if (aSelector == @selector(myDynamicMethod)) { class_addMethod(self, aSelector, (IMP)myDynamicIMP, "v@:"); return YES; } return [super resolveInstanceMethod:aSelector]; }
Здесь вы можете динамически указать свою реализацию вызываемого метода. Если же этот механизм по каким-то причнам вас не устраивает — вы можете использовать форвардинг сообщений.
Пример 2. Method Swizzling
Одна из особенностей категорий в Objective-C — метод, определенный в категории, полностью перекрывает метод базового класса. Иногда нам требуется не переопределить, а расширить функционал имеющегося метода. Пусть, например, по каким-то причинам нам хочется залогировать все добавления элементов в массив NSMutableArray
. Стандартными средствами языка этого сделать не получится. Но мы можем использовать прием под названием method swizzling:
@implementation NSMutableArray (CO) + (void)load { Method addObject = class_getInstanceMethod(self, @selector(addObject:)); Method logAddObject = class_getInstanceMethod(self, @selector(logAddObject:)); method_exchangeImplementations(addObject, logAddObject); } - (void)logAddObject:(id)aObject { [self logAddObject:aObject]; NSLog(@"Добавлен объект %@ в массив %@", aObject, self); } @end
Мы перегружаем метод load
— это специальный callback, который, если он определен в классе, будет вызван во время инициализации этого класса — до вызова любого из других его методов. Здесь мы меняем местами реализацию базового метода addObject:
и нашего метода logAddObject:
. Обратите внимание на «рекурсивный» вызов в logAddObject:
— это и есть обращение к перегруженной реализации основного метода.
Пример 3. Ассоциативные ссылки
Еще одним известным ограничением категорий является невозможность создания в них новых переменных экземпляра. Пусть, например, вам требуется добавить новое свойство к библиотечному классу UITableView
— ссылку на «заглушку», которая будет показываться, когда таблица пуста:
@interface UITableView (Additions) @property(nonatomic, strong) UIView *placeholderView; @end
«Из коробки» этот код работать не будет, вы получите исключение во время выполнения программы. Эту проблему можно обойти, используя функционал ассоциативных ссылок:
static char key; @implementation UITableView (Additions) -(void)setPlaceholderView:(UIView *)placeholderView { objc_setAssociatedObject(self, &key, placeholderView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -(UIView *) placeholderView { return objc_getAssociatedObject(self, &key); } @end
Любой объект вы можете использовать как ассоциативный массив, связывая с ним другие объекты с помощью функции objc_setAssociatedObject
. Для ее работы требуется ключ, по которому вы потом сможете извлечь нужный вам объект назад, используя вызов objc_getAssociatedObject
.
Заключение
Теперь вы располагаете базовым представлением о том, что такое Objective-C Runtime и чем он может быть полезен разработчику на практике. Для желающих узнать возможности библиотеки глубже, могу посоветовать следующие дополнительные ресурсы:
- Objective-C Runtime Programming Guide — документация от Apple
- Блог Mike Ash — статьи гуру Objective-C разработки
ссылка на оригинал статьи http://habrahabr.ru/post/177421/
Добавить комментарий