Как 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. и в заключение — ссылка на документацию

Комментарии (4)


  1. VitaliyNSK
    19.11.2015 05:42
    +1

    Расскажите, как это все связано со swift? Ну или поправьте теги


    1. ajjnix
      19.11.2015 09:58

      Swift я вписал в теги больше по причине след возможности

      swift
      class FooClass {

      func someMethodWithInt(let value: Int) -> String {
      return String(value * 2)
      }
      }

      let foo = FooClass()

      print(foo.someMethodWithInt(15))

      let f = FooClass.someMethodWithInt(foo)
      print(f(40))

      Вывод:
      30
      80


  1. shergin
    19.11.2015 09:22

    Автор, а вот теперь попробуй тоже самое сделать на Swift применительно к Objective-C runtime (селекторы, вызов функции по адресу и прочее), раз уж в тегах есть Swift. Это должно получиться действительно интересно.


    1. i_user
      19.11.2015 09:41

      Там не особо интересно и ничего особо нового, хотя есть ряд восхитительных грабель, когда пытаешься подменить имплементацию метода на блок.