Objective-C Runtime (二):方法与消息转发

359 阅读6分钟

Objective-C Runtime (二):方法与消息转发

方法基础数据类型

SEL(objc_selector)

Objc.h
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

SEL是什么了? 以下是官方说明:

A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

SEL其实是Objective-C在编译时,根据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。如下代码所示:

SEL sel = @selector(age);
NSLog(@"%p", sel);

输出为:

2018-05-25 13:15:30.816955+0800 OC_Object_Analysis[18514:3240643] 0x7fffb3819130

当我们多次运行,打印的结果永远是0x7fffb3819130。 从上面我们可以分析出:

  1. 只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个SEL。所以,在Objective-C的用一个类中,不能存在2个同名的方法,即使参数类型不同也不行。(不能像C,C++, C#那样的函数重载,就是函数名相同,参数不同)
  2. 不同的类可以拥有相同的selector,这个没有问题。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。

IMP

IMP实际上是一个函数指针,指向方法实现的首地址,其定义如下:

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

当的得到IMP后,我们就获得了执行这个方法代码的入口点,可以跳过Runtime的消息传递机制,像调用普通的C语言函数一样来使用这个函数指针,这样省去了Runtime消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些。

方法调用

在Objective-C中,消息是直到运行时才绑定到方法实现上。不像C语言,调用一个方法其实就是跳到内存中的某一点并开始执行一段代码。没有任何动态的特性,因为这在编译时就决定好了。

在Objective-C中,比如:[object foo]语法并不会立即执行 foo 这个方法的代码。它是在运行时给 object 发送一条叫 foo 的消息。这个消息,也许会由 object 来处理,也许会被转发给另一个对象,或者不予理睬假装没收到这个消息。多条不同的消息也可以对应同一个方法实现。这些都是在程序运行的时候决定的。

在编译期间, Objective-C 中函数调用的语法都会被翻译成一个 C 的函数调用 :objc_msgSend

NSMutableArray *mArr = [NSMutableArray array];
//以下两个方法是等效的
[mArr addObject:@"1"];
((void (*)(id, SEL, id))(void *) objc_msgSend)(mArr, @selector(addObject:),@"2");

那么objc_msgSend做了什么?我们以objc_msgSend(obj, foo)为例来说明:

  1. 首先,通过 obj 的 isa 指针找到它的 class ;
  2. 在class的cache里找到foo,如果找到就去执行它的实现IMP;
  3. 如果在cache里没有找到找到foo,去class的method list 找 foo;
  4. 如果 class 中没找到 foo,继续往它的 superclass 中找 ;
  5. 一旦找到 foo 这个函数,把 foo 的 method_name 作为 key ,method_imp 作为 value 给存起来,在去执行它的实现IMP。

动态方法解析和转发

当一个对象能够接收对象时,就会走正常的调用流程。但如果一个对象无法接收指定消息时,又会发生什么事呢?比如:在上面的例子中,如果 foo 没有找到会发生什么?通常情况下,程序会在运行时挂掉并抛出 unrecognized selector sent to … 的异常。但在异常抛出前,Objective-C 的运行时会给你三次拯救程序的机会:

  1. Method resolution(动态方法解析)
  2. Fast forwarding(备用接收者)
  3. Normal forwarding(完整消息转发)

Method Resolution

首先,Objective-C 运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。还是以 foo 为例,你可以这么实现:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo)) {
        class_addMethod([self class], sel, (IMP)foo, "v@:");
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

void foo(id obj, SEL _cmd) {
    NSLog(@"foo Method ");
}

如果 resolve 方法返回 NO ,运行时就会移到下一步:Fast forwarding。 @dynamic属性的实现就是这种方案。

Fast forwarding

如果目标对象实现了 -forwardingTargetForSelector: ,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。

使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。如下代码所示:

#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"Doing foo");//Person的foo函数
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //执行foo函数
    [self performSelector:@selector(foo)];
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        return [Person new];//返回Person对象,让Person对象接收这个消息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

@end

Normal forwarding

如果在上一步还不能处理未知消息,运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。

首先它会发送**-methodSignatureForSelector:**消息获得函数的参数和返回值类型。如果 -methodSignatureForSelector: 返回nil,Runtime 则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime 就会创建一个 NSInvocation 对象并发送 -forwardInvocation: 消息给目标对象。

NSInvocation 实际上就是对一个消息的描述,包括selector 以及参数等信息。所以你可以在 -forwardInvocation: 里修改传进来的 NSInvocation 对象,然后发送 -invokeWithTarget: 消息给它,传进去一个新的目标。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];

    if (!signature) {
        signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    
    Person *p = [Person new];
    if([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    }
    else {
        [self doesNotRecognizeSelector:sel];
    }
    
}

NSObject的**forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:**方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。

从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。这取决于具体的实现。

Cocoa 里很多地方都利用到了消息传递机制来对语言进行扩展,如Responder Chain。,而 Responder Chain 保证一个消息转发给合适的响应者。

参考:

  1. blog.csdn.net/fengsh998/a…
  2. Message forwarding
  3. The faster objc_msgSend
  4. Understanding objective-c runtime