Objective-C 中的消息发送和转发
Contents
Objective-C 的方法调用被苹果成为”发消息”. objc的消息机制是由运行时实现、非常灵活动态。这篇文章简单记录一下objc运行时对于消息发送和转发的实现. 本文Demo
编译器转换
|
|
意为cat对象调用mew方法, 也就是向cat对象发送mew消息, 实际上编译器会将这行代码转换为
|
|
验证如下:
|
|
结果是输出两次:
|
|
当然我们也可以执行 clang -rewrite-objc main.m
将oc转换为C++代码来查看.
objc_msgSend
文档中这样写:
|
|
将一个消息发送给一个对象,并且返回一个值。
其中,self是消息的接受者,_cmd是selector, …是可变参数列表。
为了了解objc_msgSend方法做了什么,这里需要查看一下objc runtime的源码
首先 runtime定义了如下的数据类型:
|
|
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究竟做了什么呢? 这个方法是由汇编实现得, 用伪代码大概可以表示为:
|
|
另外在objc-msg-arm64.s 中找到如下关键代码
|
|
以及objc-runtime-new.mm :
|
|
以[cat mew];
为例, 总结一下就是:
- 通过cat的isa指针找到它的class, 也就是Cat类
- 在Cat的cache列表中查找mew
- 在Cat的method列表中查找mew
- 在父类的cache和method列表中查找mew
- 在2-4的过程中, 一旦找到就将该方法添加到缓存列表并执行该方法
- 没有找到任何的方法实现, Try method resolver once, 也就是我们说的resolveInstanceMethod方法
- resolveInstanceMethod没实现, 就进入转发机制
另需注意: 如果是类方法的调用, 则是去Cat的元类中查找, 因为类方法都是保存在meta class中, 依次向父元类查找, 注意, 到NSObject meta class, 它的父类是NSObject本类, 如果这个时候有一个同名的实例方法, 也可以调用. 例:
|
|
消息转发
首先我们先来验证一下, 调用一个不存在的方法, 在运行时发了哪些消息
|
|
具体方法如下:
- 断点暂停后执行
call (void)instrumentObjcMessageSends(YES)
- 然后过掉断点, 程序崩溃
|
|
3.在Terminal中输入:
|
|
4.打开查看
|
|
结合 NSObject官方文档, 转发机制可如图表示:
1.调用resolveInstanceMethod:方法,允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回。如果仍没实现,继续下面的动作。
2.调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了nil,继续下面的动作。
3.调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。
4.调用forwardInvocation:方法,将地3步获取到的方法签名包装成Invocation传入,如何处理就在这里面了。
上面这4个方法均是模板方法,开发者可以override,由runtime来调用。最常见的实现消息转发,就是重写方法3和4,吞掉一个消息或者代理给其他对象都是没问题的。
示例: 我们为Dog类添加name属性和实例, 但通过@dynamic使其不自动生成setter和getter
|
|
此时dog对象没有setName: 方法, 会尝试解决, 我们来实现resolveInstanceMethod 方法, 并为name添加setter 和 getter (他们的内部实现可以用指针指向, 也可以用运行时api)
|
|
如果不实现上述方法, 也可以转发给别的对象:
|
|
该消息转发给Cat类的一个对象, 去调用Cat的setName:方法, 当然这没什么实际的意义.
第三次补救:
|
|
首先生成方法签名, 然后转发调用, 如果没有实现forward, 那直接调用doesNotRecognizeSelector, 抛出exception, 也可以不使用doesNotRecognizeSelector, 这样就吞没了这个消息.
__原创文章, 转载请注明出处