iOS开发笔记(三):消息传递与转发机制

1.前言

在iOS开发中经常会遇到unrecognized selector sent to instance 0x100111df0的问题,这是为什么呢,从字面上理解来说是无法识别的selector子发送给对象,其实调用一个不存在的方法就会遇到这个问题。

严格来说iOS中不存在方法调用的说法,应该说是消息的传递。消息传递和函数调用的区别就是,你可以在任意的时候对一个对象发送任何消息,而不需要在编译的时候声明。但是函数调用就不行。

先理解C语言的函数调用方式。C语言使用静态绑定,也就是说,在编译期就能决定程序运行时所应该调用的函数,所以在C语言中,如果某个函数没有实现,编译时是不能通过的。

在Objective-C中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得Objecttive-C成为一门真正的动态语言。

2.消息传递机制

2.1 objc_msgSend()

消息有名称或选择子,可以接受参数,而且可能还有返回值。 给对象发送消息可以这样写:

id returnValue = [someObject messageName:parameter];
复制代码

someObject:接收者

messageName:选择子(选择器)

选择子与参数合起来称为“消息”

编译器看到此消息后,将其转换成一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend,其“原型”如下:

void objc_msgSend(id self, SEL cmd, ...)

这是个“参数个数可变的函数,能接受两个或两个以上的参数。第一个参数代表接收者,第二个参数代表选择子(SEL是选择子的类型),后续参数就是消息中的那些参数,其顺序不变。选择子指的就是方法的名字。“选择子”与“方法”这两个词经常交替使用。编译器会把刚才那个例子中的消息转换为如下函数:

id returnValue =  objc_msgSend(someObject, @selector(messageName:), parameter);
复制代码

2.2 消息传递流程

objc_msgSend()函数会一句接受者(调用方法的对象)的类型和选择子(方法名)来调用适当的方法。

  • 接收者会根据isa指针找到接收者自己所属的类,然后在所属类的“方法列表(method list)”中从上向下遍历。如果能找到与选择子名称相符的方法,就根据IMP指针跳转到方法的实现代码,调用这个方法的实现。
  • 如果找不到与选择子名称相符的方法,接收者会根据所属类的superClass指针,沿着类的继承体系继续向上查找(向父类查找),如果能找到与名称相符的方法,就根据IMP指针跳转到方法的实现代码,调用这个方法的实现。
  • 如果在继承体系中还是找不到与选择子相符的方法,此时就会执行“消息转发(message forwarding)”操作。

SEL:类成员方法的指针,但和C的函数指针还不一样,函数指针直接保存了方法的地址,但是SEL只是方法编号。

isa指针:每个对象都有一个标识对象类的isa实例变量。运行时使用此指针来确定对象需要时的实际类。

IMP:函数指针,保存了方法地址。

2.3 快速映射表

我们发现调用一个方法并不像我们想的那么简单,更不像我们写的那么简单,一个方法的执行其实底层需要很多步骤。正因如此,objc_msgSend()会将调用过且匹配到的方法缓存在“快速映射表(fast map)”中,快速映射表就是方法的缓存表。每个类都有这样一个缓存。所以,即便子类实例从父类的方法列表中取过了某个对象方法,那么子类的方法缓存表中也会缓存父类的这个方法,下次调用这个方法,会优先去当前类(对象所属的类)的方法缓存表中查找这个方法,这样的好处是显而易见的,减少了漫长的方法查找过程,使得方法的调用更快。同样,如果父类实例对象调用了同样的方法,也会在父类的方法缓存表中缓存这个方法。

同理,如果用一个子类对象调用某个类方法,也会在子类的metaclass里缓存一份。而当用一个父类对象去调用那个类方法的时候,也会在父类的metaclass里缓存一份。

3.消息转发机制

3.1 动态方法解析

动态方法解析的意思就是,征询消息接受者所属的类,看其是否能动态添加方法,以处理当前“这个未知的选择子(unknown selector)”。实例对象在接受到无法解读的消息后,首先会调用其所属类的下列类方法:

+ (BOOL)resolveInstanceMethod:(SEL)selector
复制代码

类对象在接受到无法解读的消息后,那么运行期系统就会调用另外的一个方法,如下:

+ (BOOL)resolveClassMethod:(SEL)selector
复制代码

如果运行期系统已经执行完了动态方法解析,那么消息接受者自己就无法再以动态新增方法的形式来响应包含该未知选择子的消息了,此时就进入了第二阶段——完整的消息转发。运行期系统会请求消息接受者以其他手段来处理与消息相关的方法调用。

3.2 完整的消息转发

完整的消息转发又分为两个阶段,第一阶段称为备援接受者,第二阶段才是启动完整的消息转发机制。

  • 备援接收者

    当前接受者如果不能处理这条消息,运行期系统会请求当前接受者让其他接受者处理这条消息,与之对应的方法是:

      - (id)forwardingTargetForSelector:(SEL)selector
    复制代码

    方法参数代表未知的选择子,返回值为备援接受者,若当前接受者能找到备援接受者,就直接返回,这个未知的选择子将会交由备援接受者处理。如果找不到备援接受者,就返回nil,此时就会启用“完整的消息转发机制”。

  • 完整的消息转发

    如果转发算法已经来到了这一步,那么代表之前的所有转发尝试都失败了,此时只能启用完整的消息转发机制。完整的消息转发机制是这样的:首先创建NSInvocation对象,把尚未处理的那条消息有关的全部细节封装于这个NSInvocation对象中。此对象中包含选择子(selector)、目标(target)及参数。在触发NSInvocation对象时,“消息派发系统(message-dispatch system)”将亲自触发,把消息派发给目标对象。此步骤中会调用下面这个方法来转发消息:

      - (void)forwardInvocation:(NSInvocation *)invocation
    复制代码

    消息派发系统触发消息前,会以某种方式改变消息内容,包括但不限于额外追加一个参数、改变选择子等。实现此方法时,如果发现调用操作不应该由本类处理,则需要沿着继承体系,调用父类的同名方法,这样一来,继承体系中的每个类都有机会处理这个调用请求,直至rootClass,也就是NSObject类。如果最后调用了NSObject的类方法,那么该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,此异常表明选择子最终也未能得到处理。消息转发到此结束。

消息转发流程图

3.3 举例

  • main.m

      #import <Foundation/Foundation.h>
      #import "Test.h"
    
      int main(int argc, const char * argv[]) {
      	Test *test = [[Test alloc] init];
      	[test instanceMethod];
      	return 0;
      }
    复制代码
  • Test.m

      #import <Foundation/Foundation.h>
    
      @interface Test : NSObject
    
      - (void)instanceMethod;
    
      @end
    
    
      #import "Test.h"
      #import "Test2.h"
      #import <objc/runtime.h>
    
      @implementation Test
      /*
      * 被动态添加的实例方法实现
      */
      void instanceMethod(id self, SEL _cmd) {
      	NSLog(@"收到消息会执行此处的函数实现...");
      }
    
      /*
      * 1.第一道防线:动态方法解析
      */
      + (BOOL)resolveInstanceMethod:(SEL)sel {
      	// return NO;
      	if (sel == @selector(instanceMethod)) {
      		class_addMethod(self, sel, (IMP)instanceMethod, "v@:");
      		return YES;
      	}
      	return [super resolveInstanceMethod:sel];
      }
    
      /*
      * 2.第二道防线:备援接收者
      */
      - (id)forwardingTargetForSelector:(SEL)aSelector {
      	// return nil;
      	/* 返回转发的对象实例 */
      	if (aSelector == @selector(instanceMethod)) {
      		return [[Test2 alloc] init];
      	}
      	return nil;
      }
    
      /*
      * 3.第三道防线:完整的消息转发
      */
      - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
      	/* 为指定的方法手动生成签名 */
      	NSString *selName = NSStringFromSelector(aSelector);
      	if ([selName isEqualToString:@"instanceMethod"]) {
      		return [NSMethodSignature signatureWithObjCTypes:"v@:"];
      	}
      	return [super methodSignatureForSelector:aSelector];
      }
      - (void)forwardInvocation:(NSInvocation *)anInvocation {
      	/* 如果另一个对象可以相应该消息,则将消息转发给他 */
      	SEL sel = [anInvocation selector];
      	Test2 *test2 = [[Test2 alloc] init];
      	if ([test2 respondsToSelector:sel]) {
      		[anInvocation invokeWithTarget:test2];
      	}
      }
    
      @end
    复制代码
  • Test2.m

      #import <Foundation/Foundation.h>
    
      @interface Test2 : NSObject
    
      - (void)instanceMethod;
    
      @end
    
    
      #import "Test2.h"
    
      @implementation Test2
    
      - (void)instanceMethod {
      	NSLog(@"消息转发到这...");
      }
    
      @end
    复制代码

4.参考