Objective-C Runtime. Теория и практическое применение

от автора

В данном посте я хочу обратиться к теме, о которой многие начинающие 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 и чем он может быть полезен разработчику на практике. Для желающих узнать возможности библиотеки глубже, могу посоветовать следующие дополнительные ресурсы:

ссылка на оригинал статьи http://habrahabr.ru/post/177421/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *