Runtime

Интроспецкия, рефлексия и свизлинг

Этот пост является вольным переводом статьи

Про такой феномен, как рантайм, думаю, все слышали, но не все знают, что он из себя представляет, какие у него есть возможности.

Даже если вы не будете пользоваться его прелестями, то на собесе всё равно получите хотя бы один базовый вопрос от интервьюера.

Daria Korneichuk
iOS Developer
Вам, как правило, не нужно использовать Objective-C runtime библиотеку непосредственно при программировании на Objective-C. Этот API используется в основном для создания моста между Objective-C и другими языками, или для отладки низкого уровня.
Интроспекция (самоанализ)
Это способность предоставлять информацию об объектах / классах во время исполнения программы. Небольшой образец типа информации, предоставленной в рантайме:

1) имена методов класса из объекта класса.
2) информация о аргументах метода
3) имплементация отдельных методов класса
4) информация о переменных экземпляра класса

Этот список довольно длинный. Обратите внимание на раздел "Objective-C Runtime Reference" документации Apple, там найдете исчерпывающую информацию, которую ObjC runtime может предоставить.

Проверим возможности интроспекции рантайма ObjC. Следующий код показывает методы, которые есть у конкретного класса и его суперклассов, включая private методы.

А вот, что в консоли. Для нашего примера я создала класс Cat с некоторыми методами.
Ссылка на github в конце статьи
Рефлексия: Это возможность добавлять новые классы, добавлять/изменять интерфейсы существующих, изменять отношения между классами. ObjC рантайм также позволяет суперклассу класса быть замененным другим классом.

Чтобы продемонстрировать рефлексию в ObjC, давайте добавим метод description в класс. Метод "description" является частью протокола NSObject и вызывается, когда мы используем спецификатор формата строки "%@"(например NSString :: stringWithFormat)

Чтобы протестировать рефлексию, создадим какой-то NSObject класс и добавим несколько переменных экземпляра. Также добавим метод testReflection, как показано ниже. Тестовый код предполагает, что новый класс имеет "ivarString" (NSString) и "ivarNumber" (NSUInteger). Вы можете заменить его на что вы хотите, но убедитесь, что типы поддерживаются myDescriptionIMP. Либо добавьте свои типы, тривиальная задача. Создайте объект нового класса и вызовите его метод testReflection.

- (void)testReflection { 

  ivarString=@"SomeIvarString";
  ivarNumber=100;

  NSLog(@"Before description: %@",self);
  // Default NSObject implementation of «description» gets called

  classAddDescription([self class]);

  NSLog(@"After description: %@",self); 
  // Now our custom implementation of «description» gets called
}
Сам код метода classAddDescription ищете в репо

Примечание: (null) отображается для ivar, если его значение равно нулю и это объект. «Тип не поддерживается» отображается вместо значения ivar, если ivar не поддерживается myDescriptionIMP.
Свизлинг: Термин «Swizzling» в ObjC означает обмену реализаций двух методов (класс или экземпляр) в рантайме.

Термин «implementation(реализация)» обращается к указателю на функцию, который в свою очередь на реализацию метода. ObjC рантайм поддерживает структуру «objc_method» для каждого метода класса. Эта структура имеет имя метода, аргумент, возвращаемый тип и «реализацию» метода. Так swizzling включает в себя обмен значением «реализации» между objc_method данных двумя различными способами.

Примечание: Вызов метода объекта в ObjectiveC включает в себя отправку сообщения на этот объект, чтобы выполнить указанный метод с аргументами. Это дает возможность перенаправить сообщение другому реализации этого метода. Это именно то, что во время выполнения ObjC помогает нам сделать, чтобы реализовать swizzling.

Мы можем получить селекторы с помощью @selector (nameofselector). Так как же мы преобразуем селектор в метод? Понятие метода экземпляра / класса существует только в контексте класса. Таким образом, с помощью объекта класса и селектора, мы можем получить МЕТОД этого селектора, соответствующего этому конкретному классу.

Например, рассмотрим интерфейс класса под названием EZObject

@interface EZObject: NSObject
— (void) method1: (id) argument;
@end 
Следующий код может быть использован для получения селектора method1
SEL selMethod1 = @selector(method1 :);
Следующий код может быть использован для получения метода этого селектора в EZObject.
METHOD methodMethod1=class_getInstanceMethod([EZObject class],selMethod1);
Примечание 1: class_getClassMethod используется для получения METHOD метода класса.
Примечание 2: class_getInstanceMethod ищет метод в указанном классе и всех его супер классах.

Мы уже знаем, что нам нужен объект класса и два селектора, один существующий селектор, а другой представляет собой новый селектор, который будет swizzled с существующей.

Таким образом, возможный прототип может выглядеть следующим образом
void methodSwizzle(Class c, SEL swizzledSelector, SEL swizzlingSelector) ; 
Примечание: На практике, вы можете обменять реализации методов, которые существуют в совершенно разных классах. Но что, если реализация метода относятся к ivar, которые не существуют в новом классе, с которым они связаны? Если это произойдет, ваше приложение либо упадет, либо будет продолжать работать с непредсказуемыми последствиями. Так что не пробуйте swizzling, если вы еще не поняли, все последствия во время выполнения коммутационных реализаций. Но мы обратили внимание на случай, когда оба селекторы происходят из того же самого или супер-класса, так как это является наиболее распространенным сценарием.

Теперь, так как swizzling используется как метод экземпляра и метод класса, то каким образом caller может передать эту информацию? Возьмем за правило, что swizzledSelector трактуется как селектор метода экземпляра класса «с». Если нет такого метода экземпляра, то его обрабатывают как селектор для метода класса.

ОК. Теперь давайте реализуем функцию
Код с примером для того же класса Cat в репо

Где можно использовать swizzling?

Допустим, вы очень заинтересованы в регистрации всех различных типов gesture recognizers используемых фреймворком CocoaTouch в вашем приложении. Не было бы замечательно, если вы можете переопределить UIView :: addGestureRecognizer в своей собственной категории метода myAddGestureRecognizer и регистрировать gesture recognizers там и все равно иметь возможность вызывать реализацию в UIView по этому методу?

Этот трюк работает в том, что вы обмениваете реализацию UIView :: addGestureRecognizer со своим собственным методом UIView категории под названием myAddGestureRecognizer, а затем из реализации myAddGestureRecognizer, вызовите myAddGestureRecognizer! Помните, что swizzling переключает реализацию (указатель на функцию), но не имя метода. Так что, когда вы звоните myAddGestureRecognizer, ваше сообщение направляется к первоначальному реализации UIView: addGestureRecognizer.

Файл реализации категории UIView будет выглядеть следующим образом:
@implementation UIView (Swizzle) 

- (void)myAddGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer { 
    [self myAddGestureRecognizer:gestureRecognizer];
    NSLog(@"%@ added %@\n",[self class],gestureRecognizer);
} 

@end 
В AppDelegate didFinishLaunchingWithOptions метод, swizzle addGestureRecognizer метод следующим образом:
#import «UIView+Swizzle.h» 

— (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 
      methodSwizzle([UIView class],@selector(addGestureRecognizer:),@selector(myAddGestureRecognizer:));
      return YES;
} 
Me:
Where can I find these pieces of code?
Dasha:
You can find source code on my GitHub repo:
https://github.com/dashvlas/ObjC-Runtime
Made on
Tilda