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类型的方法签名,当调用不能识别的方法的时候打印简单的日志。当然在程序中最好不要这么做,特别是开发的时候,大部分时候我们更希望能够尽早的发现这种调用错误。

posted @ 2014-04-14 16:14  needly  阅读(186)  评论(0编辑  收藏  举报