消息机制

1.id

objc/objc.h中,id的定义如下:

1
2
3
4
5
6
7
8
/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif

id是一个结构体指针类型,它指向OC中的任何对象。

2.类(class)

objc/runtime.h中,类的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //指针

#if !__OBJC2__
Class super_class //父类;
const char *name //类名;
long version //类的版本信息,默认为0;
long info //位标识;
long instance_size //类的实例变量大小;
struct objc_ivar_list *ivars //类的成员变量链表;
struct objc_method_list **methodLists //方法定义的链表;
struct objc_cache *cache //方法缓存;
struct objc_protocol_list *protocols //协议链表;
#endif

} OBJC2_UNAVAILABLE;

3.Method

Method即方法,class结构体的objc_method_list链表中,保存的正是objc_method 对象:

1
2
3
4
5
struct objc_method {
SEL _Nonnull method_name //方法名称
char * _Nullable method_types //方法的参数类型和返回值类型
IMP _Nonnull method_imp //指向该方法的具体实现的函数指针
}

示例:

1
2
3
4
5
6
- (NSString *)methodNameWithParam1:(int)intValue param2:(BOOL)boolvalue
{
NSString *result = [NSString stringWithFormat:@"param1:%d, param2:%d",intValue,boolvalue];
NSLog(@"+++result:%@",result);
return result;
}

结合示例来看三个字段的含义:

#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
2
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

Compiled selectors are assigned to a special type, SEL.

SEL是方法名经过编译后,在运行时中的表示形式。需要注意的是:

1、一个类中不能同时存在名称和参数个数都相同的两个方法,即使其参数类型和返回值类型不同。

这是因为参数类型和返回值类型信息都保存在Method结构体的*method_types字段中,如上面提到的“@@:iB”;而SEL是Methodmethod_name字段,无关参数和返回值的类型。运行时只认SEL,名称相同且参数个数相同的两个方法对应同一个SEL,一旦相同运行时就不知该选哪个。为防止这种情况,Xcode在编译时会报错;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)methodName{ //实例方法
NSLog(@"+++");
}

- (void)sameName{ //原方法
}

- (void)sameName:(int)p1{ //正常(方法、返回值类型名相同,参数个数不同)
}

- (int)sameName:(int)p1{ //报错(方法名、参数类型和个数相同,返回值不同)
return 0;
}

- (int)sameName:(int)p1 param2:(int)p2{
}

- (int)sameName:(int)p1 param2:(NSString*)p3{ // 报错(方法名、返回值类型、参数个数相同,只是参数类型不同)
}

2、同一个类中,允许存在一对方法名相同的实例方法与类方法。

虽然二者的SEL相同,但实例方法保存在类对象中,类方法保存在元类对象中,运行时能分清;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@interface AppDelegate : UIResponder <UIApplicationDelegate>

// 类方法和实例方法名相同
+ (void)methodName;
- (void)methodName;

@end


@implementation AppDelegate

//MARK: 允许类方法和实例方法同名
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
SEL aSEL = @selector(methodName);

// 调用相同的SEL
[AppDelegate performSelector:aSEL withObject:nil]; // 调用类方法
[self performSelector:aSEL withObject:nil]; // 调用实例方法
return YES;
}

+ (void)methodName{ //类方法
NSLog(@"+++");
}
@end

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
2
3
4
5
6
/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif

IMP是一个函数指针,这个被指向的函数包含一个接收消息的对象id, 调用方法的选择器SEL,以及不定个数的方法参数,并返回一个idIMP是消息最终调用的执行代码,是方法真正的实现。

8.消息

在C语言中,函数的调用在编译时就已经决定了。而OC是一种动态语言。对于OC的函数,在编译时并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。所以,你会发现:在编译阶段,只要声明过,OC可以调用任何函数且不会报错,即使这个函数并未实现。相反在C语言中,编译阶段调用未实现的函数则会报错。

OC中,方法调用的本质,就是向对象发送一条消息。

打开objc/message.h文件,可见如下定义:

1
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )

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
2
+ (BOOL)resolveClassMethod:(SEL)name;    //类方法
+ (BOOL)resolveInstanceMethod:(SEL)name; //实例方法
  • name参数,表示需要被动态决议的selector;
  • Bool返回值,表示动态决议是否成功;

这是NSObject类中的两个类方法,执行动态方法决议时,需重写这两个方法,并在其中为指定的selector提供具体的实现(通过调用运行时函数class_addMethod来添加,下面有示例)。

在不涉及消息转发的情况下:

  • 若上述两函数内为指定的selector提供实现,无论返回YES或NO,编译运行都会正常;
  • 若上述两函数内并没有为selector提供实现,无论返回YES或NO,编译运行都会crash;

#示例1:

1
2
3
4
5
6
7
//DynamicTool头文件如下:

#import <Foundation/Foundation.h>

@interface DynamicTool : NSObject

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//DynamicTool实现文件如下:

#import "DynamicTool.h"
#import <objc/runtime.h>

void instanceMethod(id self,SEL sel)
{
NSLog(@"instance method");
}

void classMethod(id self,SEL sel,NSString* str1,NSString* str2)
{
NSLog(@"class method param1:%@,param2:%@",str1,str2);
}

@implementation DynamicTool

#pragma mark -动态决议
//实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(instanceMethodSelector)) {
class_addMethod([self class], sel, (IMP)instanceMethod, "v@:");
/*每个方法都有 self 和_cmd 两个默认的隐藏参数,
self 即接收消息的对象本身,_cmd 即是 selector 选择器;
v表示void返回值;@表示一个对象,这里指self;":"表示SEL,即_cmd。
其他符号的意义可参考文章底部链接
*/
}
return NO;
}
//类方法
+(BOOL)resolveClassMethod:(SEL)sel
{
if (sel == @selector(classMethodSelector)) {
class_addMethod(objc_getMetaClass("DynamicTool"), sel,
(IMP)classMethod, "s#:@@");
}
return YES;
}
@end

调用示例:

1
2
3
4
5
6
7
DynamicTool *insObj = [DynamicTool new];
Class classObj = NSClassFromString(@"DynamicTool");

[insObj performSelector:@selector(instanceMethodSelector)];

[classObj performSelector:@selector(classMethodSelector)
withObject:@"A" withObject:@"B"];

DynamicTool 的头文件并未定义实例方法instanceMethodSelector 和类方法classMethodSelector。因此通过 performSelector 调用时,runtime会按照上一小结所述流程从类中查找该方法,因为未定义,所以查找失败并走动态方法决议流程,分别通过resolveInstanceMethodresolveClassMethod查找具体的方法实现。

11.消息转发机制

如果没有实现动态方法决议机制,或者在动态方法决议时并未为selector提供实现,那么就会发生crash。为防止这种闪退,OC还提供了消息转发机制,以便将消息转发给其他对象。

如果同时提供了动态方法决议和消息转发,那么动态方法决议先于消息转发,只有当动态方法决议依然无法正确决议selector的实现,才会尝试进行消息转发。

  • 第一次转发机会
1
- (id)forwardingTargetForSelector:(SEL)sel;

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
2
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;
- (void)forwardInvocation:(NSInvocation *)invo;

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//DynamicTool实现文件如下:

#import "DynamicTool.h"
#import <objc/runtime.h>
#import "ForwardTool.h"

@implementation DynamicTool

#pragma mark -动态决议
//实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// 注意:为了展示消息转发的实现,这里没有给SEL提供具体的实现
return NO;
}
//类方法
+(BOOL)resolveClassMethod:(SEL)sel
{
// 注意:为了展示消息转发的实现,这里没有给SEL提供具体的实现
return YES;
}

#pragma mark -消息转发
- (id)forwardingTargetForSelector:(SEL)sel
{
//if (sel == @selector(unknownSelector)) {
// return [ForwardTool new];
//}
return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
if (!methodSignature) {
methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
}
return methodSignature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// 创建一个或多个对象 将消息转发给它们
ForwardTool *forwardTool = [ForwardTool new];
if ([forwardTool respondsToSelector:@selector(unknownSelector)]) {
//这里可以转发给多个对象,最终的返回值以最后一个调用的返回值为准
[anInvocation invokeWithTarget:forwardTool];
}
}
@end

ForwardTool类的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <Foundation/Foundation.h>

@interface ForwardTool : NSObject
- (void)unknownSelector;
@end

//ForwardTool实现文件如下:

#import "ForwardTool.h"

@implementation ForwardTool

- (void)unknownSelector
{
NSLog(@"消息转发");
}
@end

调用示例:

1
2
3
DynamicTool *insObj = [DynamicTool new];
Class classObj = NSClassFromString(@"DynamicTool");
[insObj performSelector:@selector(unknownSelector)];

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"@@:@B"];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = self;
invocation.selector = @selector(onSendMessageWithParam1:param2:);//方法必须和签名中的方法一致

NSString*str1 = @"THIS IS A STR~";
BOOL boolValue = YES;

//第一个参数为参数对象的指针;
//第二个参数为参数的索引,注意不能从0开始,因为0已经被self占用,1已经被_cmd占用
[invocation setArgument:&str1 atIndex:2];
[invocation setArgument:&boolValue atIndex:3];

[invocation invoke];//执行方法
id retLoc = nil;
[invocation getReturnValue:&retLoc];//获取返回值

return YES;
}

- (NSString *)onSendMessageWithParam1:(NSString*)str1 param2:(BOOL)boolvalue
{
NSLog(@"+++%@++++%d",str1,boolvalue);
return str1;
}

相关参考:

#©Apple-Selectors

#©Apple-Type Encodings


消息机制
https://davidlii.cn/2017/08/23/runtime-message.html
作者
Davidli
发布于
2017年8月23日
许可协议