文章目录
  1. 1. Objective—C消息发送与转发
    1. 1.1. 消息
    2. 1.2. 编译转化
    3. 1.3. objc_msgSend
    4. 1.4. 动态方法解析
    5. 1.5. 重定向
    6. 1.6. 消息转发
    7. 1.7. 总结
    8. 1.8. 优化

Objective—C消息发送与转发

我是LastDays一个热爱分享交流的,一个喜欢白菜的90后,LastDays

消息

上一篇文章Objective-C类和对象的实现原理对Objective-C类和对象的实现原理的实现原理进行了总结。我在最开始学习的时候,一直认为[receiver message]仅仅就是一个方法调用,但是后来深入学习以后才明白这是”发送消息”

编译转化

在[receiver message]的执行过程当中,[receiver message]是会被动态编译的,Objc是动态语言,因此它会想尽办法将编译连接推迟到运行时来做。runtime这个时实运行系统就是来执行编译后的代码,编译成什么样runtime才能认识呢?

这里有段代码

1
#import <Foundation/Foundation.h>
#import "Receiver.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
            
        Receiver *receiver = [[Receiver alloc] init];
        [receiver message];
    }
    return 0;
}

我们使用clang将main.m转换为c代码来看下结构

我们终端下输入:

1
clang -rewrite-objc main.m

在转换后代码查找到这个:

1
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 


        Receiver *receiver = ((Receiver *(*)(id, SEL))(void *)objc_msgSend)((id)((Receiver *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Receiver"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)receiver, sel_registerName("message"));
    }
    return 0;
}

来看下这段代码:

1
((void (*)(id, SEL))(void *)objc_msgSend)((id)receiver, sel_registerName("message"));

所以说objc发消息后大多数都是objc_msgSend函数的调用

根据苹果文档对Runtime的解释

1
id objc_msgSend(id self, SEL _cmd, ...)

将一个消息发送给一个对象,并且返回一个值。

  • self是消息的接受者,
  • _cmd是selector,
  • …是可变参数列表。

对象发送消息,调用objc_msgSend,super发送消息时,调用的是objc_msgSendSupe,如果返回值是一个结构体,则会调用objc_msgSend_stret或objc_msgSendSuper_stret。

objc_msgSend

经过了上面的铺垫,分析下objc_msgSend的发送过程:

objc_msgSend的动作其实是比较清晰的,上面我们提到过,runtime这个实时运行系统就是用来运行编译后的代码。

运行时的数据结构在Objective-C类和对象的实现原理中提到过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct objc_class *Class;
typedef struct objc_object *id;
struct objc_object {
Class isa;
};
struct objc_class {
Class isa;
}

/// 不透明结构体, selector
typedef struct objc_selector *SEL;

/// 函数指针, 用于表示对象方法的实现
typedef id (*IMP)(id, SEL, ...);
  • id:指代objc中的对象,我们不能也定它的结构,但是首地址指向isa指针。通过isa指针,运行时就能拿到objc_class。
  • objc_class: 表示对象的Class,它的结构是确定的,由编译器生成。
  • SEL:表示选择器,这是一个不透明结构体。但是实际上,通常可以把它理解为一个字符串。例如printf(“%s”,@selector(isEqual:))会打印出”isEqual:”。运行时维护着一张SEL的表,将相同字符串的方法名映射到唯一一个SEL。 通过sel_registerName(char *name)方法,可以查找到这张表中方法名对应的SEL。苹果提供了一个语法糖@selector用来方便地调用该函数。
  • IMP:是一个函数指针。objc中的方法最终会被转换成纯C的函数,IMP就是为了表示这些函数的地址。

objc_msgSend的执行过程:

(1)检查接收的对象是否为nil,如果是,调用nil处理方案

(2)在objc_object结构体中含有cache,首先会在Class的cache中查找IMP(如果没有缓存则会初始化缓存),如果找到就会跳转到对应的函数上执行。

(3)如果没有找到就像父类的Class查找,如果还没有没找到就继续向上查找,知道找到根类。

(4)如果找到根类还是没有实现方案,这个时候就需要使用_objc_msgForward函数指针替代imp,最后来执行这个imp(动态方法实现)。

也就是顺着这个结构进行查找。

其实编译器会根据情况在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。排列组合正好四个方法。

动态方法解析

在进入消息转发之前,动态方法解析会在转发机制之前执行。

调用resolveInstanceMethod(或者resolveClassMethod):如果runtime在cache或者方法列表中没有找到目标实现方法,这个时候重载resolveInstanceMethod(或者resolveClassMethod)返回YES,可以动态添加方法。我们使用class_addMethod就可以向特定的类添加特定方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end

当然如果想进入转发机制,可以让resolveInstanceMethod返回NO。

重定向

在进入消息转发之前,runtime总是仁慈的提供给我们很多机会

调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。

也就是说在进入消息转发机制之前我们可以通过重载forwardingTargetForSelector方法替换消息的接受者为其他对象,切记不能返回self,这样会进入死循环状态。如果返回了nil,继续下面的动作。

1
2
3
4
5
6
7
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}

消息转发

(1)调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常

(2)调用forwardInvocation:方法,将地1步获取到的方法签名包装成Invocation传入,如何处理就在这里面了。

这就是消息转发,当调用forwardInvocation的时候,我们同样可以重载forwardInvocation,来定义我们的转发逻辑:

1
2
3
4
- (void)forwardInvocation:(NSInvocation *)anInvocation
{

}

总结

resolveInstanceMethodforwardingTargetForSelectormethodSignatureForSelectorforwardInvocation,这四个方法在开发的过程当中都可以进行override,使用runtime来调用,经常使用的可能是实现消息转发,就是重写方法methodSignatureForSelectorforwardInvocation,吞掉一个消息或者代理给其他对象都是没问题的。

整个过程也可以用这个图表示:

优化

其实一个objc程序在启动的时候,需要一个准备时间,这个准备时间就是对cache的初始化,之后所有的消息发送都是先访问cache,这样的设计可以提高效率。

文章目录
  1. 1. Objective—C消息发送与转发
    1. 1.1. 消息
    2. 1.2. 编译转化
    3. 1.3. objc_msgSend
    4. 1.4. 动态方法解析
    5. 1.5. 重定向
    6. 1.6. 消息转发
    7. 1.7. 总结
    8. 1.8. 优化