В данном посте я хочу обратиться к теме, о которой многие начинающие iPhone-разработчики часто имеют смутное представление: Objective-C Runtime. Многие знают, что он существует, но каковы его возможности и как его использовать на практике?
Попробуем разобраться в базовых функциях этой библиотеки. Материал основан на лекциях, которые мы в 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 разработки
Автор: nsemchenkov