runtime
Objective-c是一门动态语言,动态两个字主要就体现在我们调用方法的时候,运行时回动态的查找方法,然后调用相应的函数地址。运行时是整个Objective-c程序的基石,有了它我们的程序才能正常运行起来。
NSObject是Cocoa中绝大部分类的基类,它主要是提供了序列话,拷贝对象,以及支持运行时动态识别的框架。
在Objective-c中每一个类对象最开始的位置都会有一个isa指针,该指针指向一块内存区域,该部分主要包含两部分信息:
1、指向父类的指针。
2、自身的方法分发表。
有了这两部分,Objective-c的方法的调用流程就可以跑起来了。当我们调用一个对象的某一个方法的时候,首先会在当前类的分发表中寻找该方法,如果找不到对应的方法,然后再去其父类中寻找该方法,依次类推直到找到对应的方法为止,流程图如下:
你可能会想到,如果一个类有很深的继承层次,每次去调用根类的某个函数,岂不是都要做很多次查找。理论上是这个样子的,不过runtime也并非那么傻,它会为每一个类(不是对象)维护一个经常调用的方法的列表,只要调用过就会缓存起来(官方没有明确说明缓存机制),这样当程序运行稳定以后整个方法调用的过程就会更加高效。
通过学习官方文档Objective-C Runtime Programming Guide,可以发现其实所有的selector调用最后都会转化为C类型的函数调用。举个例子我们创建了一个A类型的对象aSample,然后调用其test方法([aSample test]),编译的时候,编译器就会将该调用转化为objc_send(aSample, selector)的形式,runtime会调用test方法实现所对应的函数地址。该函数的参数包含了两个隐含的参数self以及_cmd,其中self指向调用该方法的对象,_cmd则代表要调用的方法。
前面提到了NSObject提供了很多遍历的方法可以和运行时进行交互,其中有个方法methodForSelector,通过它我们可以直接获取到指定的方法对应的函数指针。通常我们直接使用Objective-c方式的方法调用就可以了,但有时程序中可能会频繁的调用某一个方法,为了提高效率。我们可以直接获取到方法对应的函数地址,然后直接调用该函数,这样就少了动态识别的时间。
下面举个例子:
// 父类中定义该方法 - (void)testMethod { //NSLog(@"the implementation of BaseSample!!!"); int a = 5 / 2.0f; a = ~a; } // 测试方法,分别使用两种方法调用1亿次 - (void)test { void (*methodAddress)(id,SEL); methodAddress = (void(*)(id,SEL))[self methodForSelector:@selector(testMethod)]; NSLog(@"Invoke with Method Address start!!!"); for (int i = 0; i < 100000000; ++i) { methodAddress(self, @selector(testMethod)); } NSLog(@"Invoke with Method Address finish!!!"); NSLog(@"Invoke with direct selector start!!!"); for (int i = 0; i < 100000000; ++i) { [self testMethod]; } NSLog(@"Invoke with direct selector finish!!!"); }
运行结果如下图:
可以看出调用时间:使用函数地址调用共花费0.151s,直接调用方法花费0.734s。时间是有一点儿差距,但是已经微乎其微了,这也从侧面说明了runtime的缓存机制还是很给力的。
当我们调用某一个不存在的方法的时候,程序会crush,在命令行提示“unrecognized selector sent to instance 0xxxxxxx”,并抛出“NSInvalidArgumentException”的异常。当调用一个对象不能识别的方法时,runtime会一直沿着类的继承关系往基类方向寻找,直到NSObject类,如果还是识别不了该方法的话,再抛出异常之前runtime还给我们了最后一次“补救”的机会。它会先调用forwardInvocation方法,如果我们想把这个方法异常调用捕获并传递到其他地方的话,可以在类中重写该方法。NSObject对于forwardInvocation方法的默认实现是调用doesNotRecognizeSelector方法,而doesNotRecognizeSelector则是直接抛出异常。
当调用forwardInvocation的时候会传入一个NSInvocation的参数,该参数标识了调用的方法的对象以及调用的方法,并对该方法的调用结果进行封装。我们重写forwardInvocation方法的时候,还必须同时重写methodSignatureForSelector方法,该方法返回表示一个方法的字符串,具体如何构建请看Type Encodings。
下面举一个简单的重写forwardInvocation的例子:
#pragma mark- #pragma mark 重写 ForwardInvocation - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if ([self respondsToSelector:aSelector]) { return [super methodSignatureForSelector:aSelector]; } else { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } } - (void)forwardInvocation:(NSInvocation *)anInvocation { NSLog(@"Hello unreconginized selector!"); } // 在init中调用一个不存在的方法hello - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // Initialization code [self hello]; } return self; }
上面的例子,截获了不能识别的方法调用,创建了一个返回void类型的方法签名,当调用不能识别的方法的时候打印简单的日志。当然在程序中最好不要这么做,特别是开发的时候,大部分时候我们更希望能够尽早的发现这种调用错误。