消息机制
1.id
在objc/objc.h
中,id的定义如下:
1 |
|
id
是一个结构体指针类型,它指向OC中的任何对象。
2.类(class)
在objc/runtime.h
中,类的定义如下:
1 |
|
3.Method
Method
即方法,class
结构体的objc_method_list
链表中,保存的正是objc_method
对象:
1 |
|
示例:
1 |
|
结合示例来看三个字段的含义:
#SEL:
表示方法名,运行时中用来代替明文方法名;
#method_types:
表示方法的参数类型和返回值类型,具体到本示例为“@@:iB”:
- 第一个
@
表示方法的返回值为id类型; - 第二个
@
表示方法的调用者; :
表示方法选择器SEL;i
表示参数1为int类型;B
表示参数2为bool类型;
详细编码格式可参考官网#Type Encodings
#IMP:
表示指向该方法的具体实现的函数指针。
4.Selectors
In Objective-C, selector has two meanings. It can be used to refer simply to the name of a method when it’s used in a source-code message to an object. It also, though, refers to the unique identifier that replaces the name when the source code is compiled. Compiled selectors are of type SEL. All methods with the same name have the same selector. You can use a selector to invoke a method on an object—this provides the basis for the implementation of the target-action design pattern in Cocoa.
selector
,方法选择器,分两种情况:
- 编译之前,表示一个对象所调用方法的方法名;
- 编译之后,表示用来替换方法名的唯一标识符(SEL);
相同命名的方法有着相同的selector。
5.SEL
在objc/objc.h
中,SEL的定义如下:
1 |
|
Compiled selectors are assigned to a special type, SEL.
SEL
是方法名经过编译后,在运行时中的表示形式。需要注意的是:
1、一个类中不能同时存在名称和参数个数都相同的两个方法,即使其参数类型和返回值类型不同。
这是因为参数类型和返回值类型信息都保存在Method
结构体的*method_types
字段中,如上面提到的“@@:iB”;而SEL是Method
的method_name
字段,无关参数和返回值的类型。运行时只认SEL,名称相同且参数个数相同的两个方法对应同一个SEL
,一旦相同运行时就不知该选哪个。为防止这种情况,Xcode在编译时会报错;
1 |
|
2、同一个类中,允许存在一对方法名相同的实例方法与类方法。
虽然二者的SEL相同,但实例方法保存在类对象
中,类方法保存在元类对象
中,运行时能分清;
1 |
|
3、不同的类中,可以存在两个名称相同的方法,这对多态机制和动态绑定至关重要;
6.映射关系
For efficiency, full ASCII names are not used as method selectors in compiled code. Instead, the compiler writes each method name into a table, then pairs the name with a unique identifier that represents the method at runtime. The runtime system makes sure each identifier is unique: No two selectors are the same, and all methods with the same name have the same selector.
- 在编译阶段,编译器会将所有方法名写入一张表中;
- 在程序运行阶段,运行时使用SEL代表一个方法(method);
- runtime 会将方法名与SEL进行映射;
- 调用方法时,运行时系统根据SEL从相关类的方法列表(methodLists)中查找对应的方法;
- 找到了方法即可调用其结构体中的IMP;
7.IMP
在objc/objc.h
中,IMP的定义如下:
1 |
|
IMP
是一个函数指针,这个被指向的函数包含一个接收消息的对象id
, 调用方法的选择器SEL
,以及不定个数的方法参数,并返回一个id
。IMP
是消息最终调用的执行代码,是方法真正的实现。
8.消息
在C语言中,函数的调用在编译时就已经决定了。而OC是一种动态语言。对于OC的函数,在编译时并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。所以,你会发现:在编译阶段,只要声明过,OC可以调用任何函数且不会报错,即使这个函数并未实现。相反在C语言中,编译阶段调用未实现的函数则会报错。
OC中,方法调用的本质,就是向对象发送一条消息。
打开objc/message.h
文件,可见如下定义:
1 |
|
9.方法调用的过程
1、调用方法时runtime把方法的调用转化为消息发送
,即objc_msgSend(id self, SEL selector, [参数]...)
;
2、其中SEL
是运行时根据方法名转化而来的,SEL
与调用者一起作为参数传递给objc_msgSend()
,后续就是根据SEL
来查找方法及其IMP
;
2、方法的调用者会通过isa
指针找到其所属的类。在类中有一块最近调用的方法的指针缓存(即cache,参见上面类的定义),出于性能考虑 runtime 会先去cache
中根据SEL查找对应的方法;
3、若cache
中没有找到,则去methodLists
中查找该方法。找到后通过函数指针跳转到方法结构体中,执行结构体中的IMP
,之后将该方法加入到cache
中;
4、若未找到该方法,则通过super_class
往上一级父类查找,重复第2、3步;
5、如果一直到 NSObject 根类都没有找到该方法,在不做特殊处理的情况下(如动态方法决议或消息转发),会报运行时错误:unrecognized selector sent to instance xxx;
10.动态方法决议
为防止上述第5种情况下发生的crash,OC提供了动态方法决议,在运行时动态地为一个 selector
提供实现。
1 |
|
- name参数,表示需要被动态决议的selector;
- Bool返回值,表示动态决议是否成功;
这是NSObject类中的两个类方法,执行动态方法决议时,需重写这两个方法,并在其中为指定的selector提供具体的实现(通过调用运行时函数class_addMethod来添加,下面有示例)。
在不涉及消息转发的情况下:
- 若上述两函数内为指定的selector提供实现,无论返回YES或NO,编译运行都会正常;
- 若上述两函数内并没有为selector提供实现,无论返回YES或NO,编译运行都会crash;
#示例1:
1 |
|
1 |
|
调用示例:
1 |
|
DynamicTool 的头文件并未定义实例方法instanceMethodSelector
和类方法classMethodSelector
。因此通过 performSelector 调用时,runtime会按照上一小结所述流程从类中查找该方法,因为未定义,所以查找失败并走动态方法决议流程,分别通过resolveInstanceMethod
与 resolveClassMethod
查找具体的方法实现。
11.消息转发机制
如果没有实现动态方法决议机制,或者在动态方法决议时并未为selector提供实现,那么就会发生crash。为防止这种闪退,OC还提供了消息转发机制,以便将消息转发给其他对象。
如果同时提供了动态方法决议和消息转发,那么动态方法决议先于消息转发,只有当动态方法决议依然无法正确决议selector的实现,才会尝试进行消息转发。
- 第一次转发机会
1 |
|
Returns the object to which unrecognized messages should first be directed.
返回未识别方法的新接收者。
If an object implements (or inherits) this method, and returns a non-nil (and non-self) result, that returned object is used as the new receiver object and the message dispatch resumes to that new object. (Obviously if you return self from this method, the code would just fall into an infinite loop.)
实现此方法并返回非空和非self对象时,此新对象会被作为原方法的接收者,重新开始方法的派发流程。如果在方法中返回了self对象,则代码会陷入无限循环中~
- 第二次转发机会
1 |
|
When an object is sent a message for which it has no corresponding method, the runtime system gives the receiver an opportunity to delegate the message to another receiver. It delegates the message by creating an NSInvocation object representing the message and sending the receiver a forwardInvocation: message containing this NSInvocation object as the argument. The receiver’s forwardInvocation: method can then choose to forward the message to another object. (If that object can’t respond to the message either, it too will be given a chance to forward it.)
如果消息的接收者不能响应消息,则运行时会再给接收者一次将消息代理给其他对象的机会。运行时会为消息创建一个NSInvocation
对象,随后调用原接收者的forwardInvocation:方法,并传入此NSInvocation
作为参数。forwardInvocation:中将此方法转发给其他对象。
#示例2:
1 |
|
ForwardTool
类的实现如下:
1 |
|
调用示例:
1 |
|
12.消息转发的过程
1、动态方法决议进入resolvexxxMethod
方法时,指定是否动态添加方法。若指定了实现函数,则通过class_addMethod
函数动态地添加方法,并正常执行作为替代的C函数,如上面示例中的dynamicResolution
方法;否则,进入第2步;
2、如果resolvexxxMethod
方法中未指定实现函数,不论返回YES或NO,都会进入消息转发流程,调用forwardingTargetForSelector
方法,在这里指定由哪个对象响应这个selector。若返回某个对象,则会调用该对象的方法;若返回nil,进入第3步;
3、如果上一步forwardingTargetForSelector
中返回nil或者返回的转发对象也不能响应此方法,则runtime会给我们第二次转发机会,通过methodSignatureForSelector
创建一个方法签名。返回nil表示不处理,程序会crash;返回方法签名,则进入第4步。
4、methodSignatureForSelector
返回方法签名时,会调用forwardInvocation
方法。到这个方法中后即使不做任何处理程序也不会闪退,当然也可以通过anInvocation
再次将消息转发给多个对象,或者修改实现方法,修改响应对象等。
13.NSInvocation
OC中 直接调用类的方法有两种途径:
- 通过 NSObject 分类中定义的
-performSelector:withObject:withObject:
方法; - 通过
NSInvocation
;
第一种适合处理参数较少的方法调用;当有多个参数时,就需要使用第二种方式。
#示例3:
1 |
|
相关参考: