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. и в заключение — ссылка на документацию
    Метки:
    • +10
    • 16,8k
    • 4
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 4
    • +1
      Расскажите, как это все связано со swift? Ну или поправьте теги
      • 0
        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


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

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.