Как self и _cmd оказываются в методе? Как работает dispatch table и категории? Что такое мета-класс? Сколько на самом деле методов у ваших классов в ARC и в MRC? Как работает swizzling?
Интересно? Добро пожаловать под кат!
ВНИМАНИЕ!
Эта статья не рассчитана на начинающих разработчиков… Приношу свои извинения за то, что не рассматриваю многие моменты, которые должен знать Objective-C разработчик.
Есть методы класса, есть методы экземпляров класса. Давайте временно забудем, что класс имеет методы, позже мы обязательно к этому вернемся — так будет меньше путаницы при чтении статьи.
Не будем уделять дополнительное внимание тому, как происходит поиск метода в Objective-C, для это есть подходящие статьи, достаточно даже википедии.
И так, мы начинаем.
Поиск метода происходит по dispatch table у isa, уходя вниз. Именно поэтому все методы в Objective-C являются виртуальными, включая private.
И поэтому же мы можем обратиться в метод, зная его селектор.
Ключем в dispatch table является SEL (селектор, подробный разбор), а значением IMP (реализация, самая обычная C функция)
Метод — это функция? Об этом позже.
По рисунку, таблица дочернего класса не включает в себя таблицу родительского класса, но использует композицию. Проверим это на практике:
... typedef struct objc_method *Method; ... struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; }
#import <Foundation/Foundation.h> @interface Human : NSObject @property (copy, nonatomic) NSString *name; @end
#import "Human.h" @implementation Human @end
#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.746 TestingRuntime[71448:6200105] setName:
2015-11-14 22:02:03.746 TestingRuntime[71448:6200105] name
Отлично, мы смогли получить таблицу методов класса Human и убедились, что родительская таблица используется композицией. Правда среди наших методов обнаружился .cxx_destruct (добавляется ARC при наличии полей, именно здесь происходит их release), но это не является темой данной статьи.
Разбираемся дальше в dispatch table. Как работают категории? Они расширяют таблицу класса. А как это происходит? Когда мы используем include/import? Нет, это не так.
#import "Human.h" @interface Human (FooMethod) @end
#import "Human+FooMethod.h" @implementation Human (FooMethod) - (void)fooMethod { NSLog(@"i send msg fooMethod"); } @end
#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; }
Почему наша программа не упала, а метод был вызван? Потому что на этот момент метод «fooMethod» уже присутствует в dispatch table. Замечу, что в коде нигде не используются включения файла «Human+FooMethod.h». Значит категория срабатывает на всем проекте, а не только в файлах, где мы ее включили, используя include/import. А что будет, если в таблице произойдет коллизия? Неопределенное поведение, и не важно, как мы используем категории в коде.
Теперь расширим таблицу руками. Да, добавим метод в рантайме и преобразуем обычную функцию в метод.
#import <Foundation/Foundation.h> @interface Human : NSObject @property (copy, nonatomic) NSString *name; @end
#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, что является подменой.
#import <Foundation/Foundation.h> @interface Human : NSObject - (NSString *)fname; - (NSString *)lname; - (void)swizzling; @end
#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
#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.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? Ведь все проходит здесь, можем ли мы изменить класс объекта? Можем.
#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
#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
#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.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).
Мы точно так же можем добавить классу метод, как и делали это ранее. За исключением одного момента, что нужно получить мета-класс.
#import <Foundation/Foundation.h> @interface Human : NSObject + (void)humanClassMethod; - (void)humanMethod; @end
#import "Human.h" @implementation Human - (void)humanMethod { NSLog(@"humanMethod"); } + (void)humanClassMethod { NSLog(@"humanClassMethod"); } @end
#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.129 TestingRuntime[72303:6360119]
2015-11-15 20:20:33.129 TestingRuntime[72303:6360119] humanClassMethod
Осталось для закрепления материала, получить адрес метода экземпляра класса и вызвать его как обычную функцию с приведением к нужному типу.
#import <Foundation/Foundation.h> @interface Human : NSObject @property (copy, nonatomic) NSString *name; - (NSString *)fooMethodWithArg1:(NSString *)arg1 arg2:(NSString *)arg2; @end
#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
#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; }
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/
Добавить комментарий