Objective-C что такое на самом деле метод и self? + runtime

от автора

Как self и _cmd оказываются в методе? Как работает dispatch table и категории? Что такое мета-класс? Сколько на самом деле методов у ваших классов в ARC и в MRC? Как работает swizzling?
Интересно? Добро пожаловать под кат!

ВНИМАНИЕ!
Эта статья не рассчитана на начинающих разработчиков… Приношу свои извинения за то, что не рассматриваю многие моменты, которые должен знать Objective-C разработчик.

Есть методы класса, есть методы экземпляров класса. Давайте временно забудем, что класс имеет методы, позже мы обязательно к этому вернемся — так будет меньше путаницы при чтении статьи.
Не будем уделять дополнительное внимание тому, как происходит поиск метода в Objective-C, для это есть подходящие статьи, достаточно даже википедии.

И так, мы начинаем.

Поиск метода происходит по dispatch table у isa, уходя вниз. Именно поэтому все методы в Objective-C являются виртуальными, включая private.
И поэтому же мы можем обратиться в метод, зная его селектор.
Ключем в dispatch table является SEL (селектор, подробный разбор), а значением IMP (реализация, самая обычная C функция)

Метод — это функция? Об этом позже.

По рисунку, таблица дочернего класса не включает в себя таблицу родительского класса, но использует композицию. Проверим это на практике:

Получение dispatch table класса

... typedef struct objc_method *Method; ... struct objc_method {     SEL method_name                                          OBJC2_UNAVAILABLE;     char *method_types                                       OBJC2_UNAVAILABLE;     IMP method_imp                                           OBJC2_UNAVAILABLE; } 

Human.h

#import <Foundation/Foundation.h>   @interface Human : NSObject  @property (copy, nonatomic) NSString *name;  @end 

Human.m

#import "Human.h"   @implementation Human @end 

main.m

#import <Foundation/Foundation.h> #import <objc/runtime.h> #import "Human.h"   void printAllMethodClass(Class clazz) {     unsigned int count;     Method *methods = class_copyMethodList(clazz, &count);     for (int i = 0; i < count; i++) {         Method method = methods[i];         SEL sel = method_getName(method);         NSLog(@"%@", NSStringFromSelector(sel));     } }  int main(int argc, const char * argv[]) {     @autoreleasepool {         printAllMethodClass([Human class]);     }     return 0; } 

Вывод

2015-11-14 22:02:03.744 TestingRuntime[71448:6200105] .cxx_destruct
2015-11-14 22:02:03.746 TestingRuntime[71448:6200105] setName:
2015-11-14 22:02:03.746 TestingRuntime[71448:6200105] name

Замечание

методы setName и name сгенерированны, поскольку мы объявили property name

Отлично, мы смогли получить таблицу методов класса Human и убедились, что родительская таблица используется композицией. Правда среди наших методов обнаружился .cxx_destruct (добавляется ARC при наличии полей, именно здесь происходит их release), но это не является темой данной статьи.
Разбираемся дальше в dispatch table. Как работают категории? Они расширяют таблицу класса. А как это происходит? Когда мы используем include/import? Нет, это не так.

Влияние категории на dispatch table

Human+FooMethod.h

#import "Human.h"   @interface Human (FooMethod) @end 

Human+FooMethod.m

#import "Human+FooMethod.h"   @implementation Human (FooMethod)  - (void)fooMethod {     NSLog(@"i send msg fooMethod"); }  @end 

main.m

#import <Foundation/Foundation.h> #import "Human.h"   int main(int argc, const char * argv[]) {     @autoreleasepool {         Human *human = [[Human alloc] init];         [human performSelector:@selector(fooMethod)];     }     return 0; } 

Вывод

2015-11-14 22:24:20.862 TestingRuntime[71509:6208985] i send msg fooMethod

Почему наша программа не упала, а метод был вызван? Потому что на этот момент метод «fooMethod» уже присутствует в dispatch table. Замечу, что в коде нигде не используются включения файла «Human+FooMethod.h». Значит категория срабатывает на всем проекте, а не только в файлах, где мы ее включили, используя include/import. А что будет, если в таблице произойдет коллизия? Неопределенное поведение, и не важно, как мы используем категории в коде.

Теперь расширим таблицу руками. Да, добавим метод в рантайме и преобразуем обычную функцию в метод.

Сделаем функцию методом

Human.h

#import <Foundation/Foundation.h>   @interface Human : NSObject  @property (copy, nonatomic) NSString *name;  @end 

main.m

#import <Foundation/Foundation.h> #import <objc/runtime.h> #import "Human.h"  void methodInRuntime(Human *self, SEL _cmd) {     NSLog(@"name self %@", self.name); }  int main(int argc, const char * argv[]) {     @autoreleasepool {         class_addMethod([Human class], @selector(methodInRuntime), (IMP)methodInRuntime, "@@:");                  Human *human = [[Human alloc] init];         human.name = @"ajjnix";         [human performSelector:@selector(methodInRuntime)];     }     return 0; } 

Типы

Минимальное ограничение, в метод необходимо передать объект и селектор, что символизирует self и _cmd(что это?)

Значит метод — это функция, в которую передается объект и селектор. Несет ли это какую-то практическую значимость?
Теперь мы знаем, что self — это переменная и что блок захватывает self как обычную внешнюю переменную (тема отдельной статьи). И по этому же мы можем создать в блоке переменную с именем self (что порой приходится делать при использовании макросов, где внутри используется self).
Возникает закономерный вопрос: «Можем ли мы подделать self и _cmd при вызове?» Да, можем. Как видно в коде выше, IMP — это простая функция, которую можно привести к любому необходимому виду и передать в нее все, что захотим.

Что мы еще можем делать в рантайме? Использовать приватные ivar, добавлять классы, проперти, методы, удалять, получать все методы класса и другие вещи. Но статья не о том, как использовать рантайм, а о методах.

Мы подошли к понятию swizzling, что является подменой.

swizzling

Human.h

#import <Foundation/Foundation.h>   @interface Human : NSObject  - (NSString *)fname; - (NSString *)lname;  - (void)swizzling;  @end 

Human.m

#import "Human.h" #import <objc/runtime.h>   @implementation Human  - (NSString *)fname {     return @"first name"; }  - (NSString *)lname {     return  @"last name"; }  - (void)swizzling {     Method mfname = class_getInstanceMethod([self class], @selector(fname));     Method mlname = class_getInstanceMethod([self class], @selector(lname));     method_exchangeImplementations(mfname, mlname); }  @end 

main.m

#import <Foundation/Foundation.h> #import "Human.h"   int main(int argc, const char * argv[]) {     @autoreleasepool {         Human *human = [[Human alloc] init];         NSLog(@"my fname:%@", [human fname]);         NSLog(@"my lname:%@", [human lname]);          [human swizzling];                  NSLog(@"my fname:%@", [human fname]);         NSLog(@"my lname:%@", [human lname]);     }     return 0; } 

Вывод

2015-11-15 19:53:28.307 TestingRuntime[72180:6349571] my fname:first name
2015-11-15 19:53:28.309 TestingRuntime[72180:6349571] my lname:last name
2015-11-15 19:53:28.309 TestingRuntime[72180:6349571] my fname:last name
2015-11-15 19:53:28.309 TestingRuntime[72180:6349571] my lname:first name

Мы так же можем добавить свои методы в таблицу и сделать необходимые действия. Никакой магии теперь для нас нет, когда разобрались, что такое методы.

А как же isa? Ведь все проходит здесь, можем ли мы изменить класс объекта? Можем.

Смена класса в runtime, не изменяя адрес объекта

Human.h

#import <Foundation/Foundation.h>   @interface Human : NSObject  - (void)humanMethod;  @end   @interface Human1 : Human  - (void)humanMethod1;  @end   @interface NoHuman : NSObject  @property (copy, nonatomic) NSString *foo;  - (void)noHumanMethod;  @end  

Human.m

#import "Human.h"   @implementation Human  - (void)humanMethod {     NSLog(@"humanMethod"); }  @end   @implementation Human1  - (void)humanMethod1 {     NSLog(@"humanMethod1"); }  @end   @implementation NoHuman  - (void)noHumanMethod {     NSLog(@"noHumanMethod with property foo:%@", self.foo); }  @end 

main.m

#import <Foundation/Foundation.h> #import "Human.h" #import <objc/runtime.h>   int main(int argc, const char * argv[]) {     @autoreleasepool {         Human *human = [[Human alloc] init];         NSLog(@"ptr %p, isa = %@", human, NSStringFromClass([human class]));         [human performSelector:@selector(humanMethod)];                  object_setClass(human, [Human1 class]);         NSLog(@"ptr %p, isa = %@", human, NSStringFromClass([human class]));         [human performSelector:@selector(humanMethod)];         [human performSelector:@selector(humanMethod1)];                  object_setClass(human, [NoHuman class]);         NSLog(@"ptr %p, isa = %@", human, NSStringFromClass([human class]));         NoHuman *noHuman = (NoHuman *)human;         noHuman.foo = @"f o o";         [noHuman noHumanMethod];     }     return 0; } 

Вывод

2015-11-15 22:40:45.960 TestingRuntime[72469:7427905] ptr 0x10020b8b0, isa = Human
2015-11-15 22:40:45.961 TestingRuntime[72469:7427905] humanMethod
2015-11-15 22:40:45.962 TestingRuntime[72469:7427905] ptr 0x10020b8b0, isa = Human1
2015-11-15 22:40:45.962 TestingRuntime[72469:7427905] humanMethod
2015-11-15 22:40:45.962 TestingRuntime[72469:7427905] humanMethod1
2015-11-15 22:40:45.962 TestingRuntime[72469:7427905] ptr 0x10020b8b0, isa = NoHuman
2015-11-15 22:40:45.962 TestingRuntime[72469:7427905] noHumanMethod with property foo:f o o

В начале статьи, я попросил забыть о том, что у класса есть методы и что в Objective-C это объект. Так вот, отмените.

Действительно, класс — это объект мета-класса. У него есть свои методы, своя собственная dispatch table, свой isa. Также он обладает своей точкой входа (+initializer).
Мы точно так же можем добавить классу метод, как и делали это ранее. За исключением одного момента, что нужно получить мета-класс.

Демонстрация, различных dispatch table, использование мета-класса

Human.h

#import <Foundation/Foundation.h>   @interface Human : NSObject  + (void)humanClassMethod; - (void)humanMethod;  @end 

Human.m

#import "Human.h"   @implementation Human  - (void)humanMethod {     NSLog(@"humanMethod"); }  + (void)humanClassMethod {     NSLog(@"humanClassMethod"); }  @end 

main.m

#import <Foundation/Foundation.h> #import "Human.h" #import <objc/runtime.h>   void printAllMethodClass(Class clazz) {     unsigned int count;     Method *methods = class_copyMethodList(clazz, &count);     for (int i = 0; i < count; i++) {         Method method = methods[i];         SEL sel = method_getName(method);         NSLog(@"%@", NSStringFromSelector(sel));     } }  int main(int argc, const char * argv[]) {     @autoreleasepool {         printAllMethodClass([Human class]);          NSLog(@"\n\n\n");                  Class metaClass = object_getClass([Human class]);         printAllMethodClass(metaClass);     }     return 0; } 

Вывод

2015-11-15 20:20:33.128 TestingRuntime[72303:6360119] humanMethod
2015-11-15 20:20:33.129 TestingRuntime[72303:6360119]

2015-11-15 20:20:33.129 TestingRuntime[72303:6360119] humanClassMethod

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

Вызов метода как функцию

Human.h

#import <Foundation/Foundation.h>   @interface Human : NSObject  @property (copy, nonatomic) NSString *name;  - (NSString *)fooMethodWithArg1:(NSString *)arg1 arg2:(NSString *)arg2;  @end 

Human.m

#import "Human.h"   @implementation Human  - (NSString *)fooMethodWithArg1:(NSString *)arg1 arg2:(NSString *)arg2 {     return [NSString stringWithFormat:@"\nname:%@ \n_cmd:%@ \narg1:%@ \narg2:%@", self.name, NSStringFromSelector(_cmd), arg1, arg2]; }  @end  

main.m

#import <Foundation/Foundation.h> #import "Human.h" #import <objc/runtime.h>   int main(int argc, const char * argv[]) {     @autoreleasepool {                  SEL sel = @selector(fooMethodWithArg1:arg2:);         Method method = class_getInstanceMethod([Human class], sel);         IMP imp = method_getImplementation(method);          #define funcWithArg1AndArg2(imp) ((NSString * (*)())imp)         Human *human = [[Human alloc] init];         human.name = @"ajjnix";                  NSString *result = funcWithArg1AndArg2(imp)(human, sel, @"Hello ", @"world");         NSLog(@"%@", result);                  NSLog(@"\n\n\n");                  NSString *result1 = funcWithArg1AndArg2(imp)(human, @selector(fake_selector), @"Hello ", @"world");         NSLog(@"%@", result1); #undef funcWithArg1AndArg2     }     return 0; } 

Вывод

2015-11-17 12:28:29.821 TestingRuntime[73269:8918838]
name:ajjnix
_cmd:fooMethodWithArg1:arg2:
arg1:Hello
arg2:world
2015-11-17 12:28:29.823 TestingRuntime[73269:8918838]

2015-11-17 12:28:29.823 TestingRuntime[73269:8918838]
name:ajjnix
_cmd:fake_selector
arg1:Hello
arg2:world

Статья получилась не маленькой, надеюсь я смог объяснить что такое на самом деле методы в языке Objective-C.
p.s. и в заключении, ссылка на документацию

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


Комментарии

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

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