Как self и _cmd оказываются в методе? Как работает dispatch table и категории? Что такое мета-класс? Сколько на самом деле методов у ваших классов в ARC и в MRC? Как работает swizzling?
Интересно? Добро пожаловать под кат!
ВНИМАНИЕ!
Эта статья не рассчитана на начинающих разработчиков… Приношу свои извинения за то, что не рассматриваю многие моменты, которые должен знать Objective-C разработчик.
Есть методы класса, есть методы экземпляров класса. Давайте временно забудем, что класс имеет методы, позже мы обязательно к этому вернемся — так будет меньше путаницы при чтении статьи.
Не будем уделять дополнительное внимание тому, как происходит поиск метода в Objective-C, для это есть подходящие статьи, достаточно даже википедии.
Итак, мы начинаем.
Поиск метода происходит по dispatch table у isa, уходя вниз. Именно поэтому все методы в Objective-C являются виртуальными, включая private.
И поэтому же мы можем обратиться в метод, зная его селектор.
Ключом в dispatch table является SEL (селектор, подробный разбор), а значением IMP (реализация, самая обычная C функция)
Метод — это функция? Об этом позже.
По рисунку, таблица дочернего класса не включает в себя таблицу родительского класса, но использует композицию. Проверим это на практике:
Отлично, мы смогли получить таблицу методов класса Human и убедились, что родительская таблица используется композицией. Правда среди наших методов обнаружился .cxx_destruct (добавляется ARC при наличии полей, именно здесь происходит их release), но это не является темой данной статьи.
Разбираемся дальше в dispatch table. Как работают категории? Они расширяют таблицу класса. А как это происходит? Когда мы используем include/import? Нет, это не так.
Почему наша программа не упала, а метод был вызван? Потому что на этот момент метод «fooMethod» уже присутствует в dispatch table. Замечу, что в коде нигде не используются включения файла «Human+FooMethod.h». Значит категория срабатывает на всем проекте, а не только в файлах, где мы ее включили, используя include/import. А что будет, если в таблице произойдет коллизия? Неопределенное поведение, и не важно, как мы используем категории в коде.
Теперь расширим таблицу руками. Да, добавим метод в рантайме и преобразуем обычную функцию в метод.
Минимальное ограничение, в метод необходимо передать объект и селектор, что символизирует self и _cmd(что это?)
Значит метод — это функция, в которую передается объект и селектор. Несет ли это какую-то практическую значимость?
Теперь мы знаем, что self — это переменная и что блок захватывает self как обычную внешнюю переменную (тема отдельной статьи). И по этому же мы можем создать в блоке переменную с именем self (что порой приходится делать при использовании макросов, где внутри используется self).
Возникает закономерный вопрос: «Можем ли мы подделать self и _cmd при вызове?» Да, можем. Как видно в коде выше, IMP — это простая функция, которую можно привести к любому необходимому виду и передать в нее все, что захотим.
Что мы еще можем делать в рантайме? Использовать приватные ivar, добавлять классы, проперти, методы, удалять, получать все методы класса и другие вещи. Но статья не о том, как использовать рантайм, а о методах.
Мы подошли к понятию swizzling, что является подменой.
Мы также можем добавить свои методы в таблицу и сделать необходимые действия. Никакой магии теперь для нас нет, когда разобрались, что такое методы.
А как же isa? Ведь все проходит здесь, можем ли мы изменить класс объекта? Можем.
В начале статьи, я попросил забыть о том, что у класса есть методы и что в Objective-C это объект. Так вот, отмените.
Действительно, класс — это объект мета-класса. У него есть свои методы, своя собственная dispatch table, свой isa. Также он обладает своей точкой входа (+initializer).
Мы точно так же можем добавить классу метод, как и делали это ранее. За исключением одного момента, что нужно получить мета-класс.
Осталось для закрепления материала, получить адрес метода экземпляра класса и вызвать его как обычную функцию с приведением к нужному типу.
Статья получилась не маленькой, надеюсь, я смог объяснить, что такое на самом деле методы в языке Objective-C.
p.s. и в заключение — ссылка на документацию
Интересно? Добро пожаловать под кат!
ВНИМАНИЕ!
Эта статья не рассчитана на начинающих разработчиков… Приношу свои извинения за то, что не рассматриваю многие моменты, которые должен знать 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
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
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
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
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
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. и в заключение — ссылка на документацию
VitaliyNSK
Расскажите, как это все связано со swift? Ну или поправьте теги
ajjnix
Swift я вписал в теги больше по причине след возможности
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
shergin
Автор, а вот теперь попробуй тоже самое сделать на Swift применительно к Objective-C runtime (селекторы, вызов функции по адресу и прочее), раз уж в тегах есть Swift. Это должно получиться действительно интересно.
i_user
Там не особо интересно и ничего особо нового, хотя есть ряд восхитительных грабель, когда пытаешься подменить имплементацию метода на блок.