【iOS】OC底层系列十 - runtime-objc_msgSend

OC中的方法调用,其实都是转换为objc_msgSend函数的调用。

objc_msgSend的执行流程可以分为3大阶段:消息发送、动态方法解析、消息转发。

例如,调用DBPerson类的对象方法setRed:,实例对象person就是消息接收者(receiver),发送的消息名称是是setRed:

1
objc_msgSend((id)person, sel_registerName("setRed:");

当向person对象发送消息时(消息发送阶段),会先按照isa指针依次查找方法,找到方法后进行方法调用。如果方法没有找到,会查找开发者动态创建的方法(动态方法解析阶段),如果还是都没有找到合适的方法,会把消息发送给其他对象进行调用(消息转发阶段),最终还是没有找到方法时,运行时会报经典错误:unrecognized selector sent to instance

一、消息发送

如下图,objc_msgSend执行流程的消息发送阶段。

receiver通过isa指针找到receiverClass,receiverClass通过superclass指针找到superClass。

如果是从class_rw_t中查找方法:已经排序的,使用二分查找;没有排序的,进行遍历查找。

二、动态解析

当消息发送阶段最终在自己的类以及父类中都没有找到方法时,会进入到动态解析阶段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface DBPerson : NSObject

- (void)test;

@end

@implementation DBPerson

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
DBPerson *person = [[DBPerson alloc] init];
[person test];
}
return 0;
}

上面的代码在运行过程中会报错:-[DBPerson test]: unrecognized selector sent to instance 0x101806f00。因为在DBPerson类中没有找到test方法的实现(在消息发送阶段没有找到方法),此时会进入动态解析阶段,我们需要自己实现对应的方法。

可以实现以下方法,来动态添加方法实现(和class_addMethod()配合实现):

  • + (BOOL)resolveInstanceMethod:(SEL)sel
  • + (BOOL)resolveClassMethod:(SEL)sel

动态解析过后,会重新走“消息发送”的流程,“从receiverClass的cache中查找方法”这一步开始执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@implementation DBPerson

+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(test)) {
// 第一种方式:调用OC方法
// Method method = class_getInstanceMethod(self, @selector(otherOC));
// class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
// 第二种方式:调用C语言函数
class_addMethod(self, sel, (IMP)otherC, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

- (void)otherOC {
NSLog(@"%s", __func__);
}

void otherC(id self, SEL _cmd) {
NSLog(@"%@-%s-%s", self, sel_getName(_cmd), __func__);
}

@end

class_addMethod方法是把未实现的方法提供一个方法实现。如上,为方法test动态提供方法otherOC的实现。

Method可以理解为等价于struct method_t *

三、消息转发

如果消息发送阶段没有找到方法,并且动态解析阶段也没有实现对应的方法,就会进入消息转发阶段(消息转发给其他消息接收者)。

开发者可以在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
@implementation DBPerson

- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
// 本质就是objc_msgSend([[DBMsgForward alloc] init], aSelector),把消息转发给了DBMsgForward对象
return [[DBMsgForward alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}

@end

@interface DBMsgForward : NSObject

@end

@implementation DBMsgForward

- (void)test {
NSLog(@"%s", __func__);
}

@end

// 输出:-[DBMsgForward test]

如果forwardingTargetForSelector:返回的是nil或没有做任何处理,就会调用methodSignatureForSelector:,这个方法的本质就是返回方法签名(返回值类型、参数类型),如果返回的方法签名不为nil,就会调用forwardInvocation:(只要进入到这个方法,程序运行时就不会报找不到方法的错误,所以可以在这个方法做任何操作),NSInvocation封装了方法调用者,方法名、方法参数和返回值类型等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@implementation DBPerson

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
// v16@0:8就是test方法的types
// return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
// 也可以直接找到对象方法获取方法签名
return [[[DBMsgForward alloc] init] methodSignatureForSelector:@selector(test)];
}
return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (anInvocation.selector == @selector(test)) {
anInvocation.target = [[DBMsgForward alloc] init];
[anInvocation invoke];
// 下面一行代码等价上面两行代码
// [anInvocation invokeWithTarget:[[DBMsgForward alloc] init]];
return;
}
[super forwardInvocation:anInvocation];
}

@end

可以利用消息转发阶段捕获程序”找不到方法“的异常信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@implementation DBPerson

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
// 本来能调用的方法
if ([self respondsToSelector:aSelector]) {
return [super methodSignatureForSelector:aSelector];
}
// 找不到方法时调用
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

// 找不到的方法,都会来到这里
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"找不到%@方法", NSStringFromSelector(anInvocation.selector));
}

@end

四、@synthesize和@dynamic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface DBPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;

@end

@implementation DBPerson
@synthesize name = _name, age = _myAge;
@dynamic height;

- (void)setAge:(int)age {
_myAge = age;
}

- (int)age {
return _myAge;
}

@end

@property可以让编译器自动生成属性的setter和getter方法。

@synthesize可以自定义属性的成员变量名称(如上面的属性age,成员变量被自定义成_myAge),在Xcode6.0之前必须手动写@synthesize,否则成员变量会不存在。Xcode6.0之后就不需要写@synthesize了,因为编译器会自动生成下划线开头的成员变量名称。

@dynamic标记属性后,编译器不会自动生成属性的setter和getter方法,并且不会自动生成成员变量,需要开发者手动实现。


面试题1:讲一下 OC 的消息机制?
解答:OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)。objc_msgSend底层有3大阶段:消息发送(当前类、父类中查找)、动态方法解析、消息转发。

面试题2:消息转发直接流程?

解答:参考上面的流程图。

面试题3:什么是Runtime?平时项目中有用过么?
解答:OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行。OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数。平时编写的OC代码,底层都是转换成了Runtime API进行调用。

具体应用:

  1. 利用关联对象(AssociatedObject)给分类添加属性;
  2. 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
  3. 交换方法实现(交换系统的方法)
  4. 利用消息转发机制解决方法找不到的异常问题
    ……

面试题4:下面代码的打印结果分别是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface DBPerson : NSObject

@end

@interface DBStudent : DBPerson

@end

@implementation DBStudent

- (instancetype)init {
if (self = [super init]) {
NSLog(@"[self class] = %@", [self class]);
NSLog(@"[self superclass] = %@", [self superclass]);
NSLog(@"[super class] = %@", [super class]);
NSLog(@"[super superclass] = %@", [super superclass]);
}
return self;
}

@end

输出结果:

1
2
3
4
[self class] = DBStudent
[self superclass] = DBPerson
[super class] = DBStudent
[super superclass] = DBPerson

为什么[super class]打印结果是DBStudent?因为OC调用方法的本质是消息发送。

把上面的代码转为C++:

1
2
3
4
5
6
7
8
9
10
11
struct objc_super {
__unsafe_unretained _Nonnull id receiver; // 消息接收者
__unsafe_unretained _Nonnull Class super_class; // 消息接收者的父类(从父类的实例方法列表中开始查找方法)
};

struct objc_super arg = {
self, // super调用的receiver仍然是DBStudent对象
class_getSuperclass(objc_getClass("DBStudent")) // DBStudent的父类DBPerson
}

objc_msgSendSuper(arg, @selector(class));

class和superclass方法的本质(objc源码中NSObject.mm文件中,这两个方法的返回值取决于方法接收者是什么类):

1
2
3
4
5
6
7
8
9
- (Class)class {
return object_getClass(self);
}

- (Class)superclass {
// return class_getSuperclass(objc_getClass(self));
// 等价
// return [self class]->superclass;
}

[self class]本质就是objc_msgSend(self, @selector(class));,消息接收者是self(DBStudent对象),class返回当前方法调用者所在类(DBStudent类)。

[self superclass]本质就是objc_msgSend(self, @selector(superclass));,消息接收者是self(DBStudent对象),superclass返回当前方法调用者的父类(DBPerson类)。

[super class]本质就是objc_msgSendSuper({self, [DBPerson class]}, @selector(class)),消息接收者是self(DBStudent对象),class返回当前方法调用者所在类(DBStudent类)。super的作用是从父类开始查找方法的实现,所以[super class][self class]的打印结果是一样的,self是从本类开始查找方法,super是从父类开始查找方法,最终消息接收者都是DBStudent对象。

[super superclass]本质就是objc_msgSendSuper({self, [DBPerson class]}, @selector(superclass)),消息接收者是self(DBStudent对象),superclass返回当前方法调用者的父类(DBPerson类)。[self superclass][super superclass]的区别也是因为self是从本类开始查找方法,super是从父类开始查找方法。

objc_msgSendSuper的本质实现是objc_msgSendSuper2,objc_msgSendSuper2里面传2个参数:消息接收者和当前类,在函数内部会自动把当前类转为查找当前类的父类。所以把OC代码转为C++代码后,它的实现逻辑还是有一点差异的,但差别不是很大。

面试题5:下面代码的打印结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BOOL res1 = [[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [[DBPerson class] isKindOfClass:[DBPerson class]];
BOOL res4 = [[DBPerson class] isMemberOfClass:[DBPerson class]];
BOOL res5 = [[DBPerson class] isMemberOfClass:[NSObject class]];
BOOL res6 = [[DBPerson class] isKindOfClass:[NSObject class]];
NSLog(@"%d - %d - %d - %d - %d - %d", res1, res2, res3, res4, res5, res6);
// 输出:1 - 0 - 0 - 0 - 0 - 1

BOOL res7 = [DBPerson isMemberOfClass:[DBPerson class]];
BOOL res8 = [DBPerson isMemberOfClass:object_getClass([DBPerson class])];
BOOL res9 = [DBPerson isKindOfClass:object_getClass([NSObject class])];
NSLog(@"%d - %d - %d", res7, res8, res9);
// 输出:0 - 1 - 1

BOOL res10 = [DBPerson isKindOfClass:[DBPerson class]];
BOOL res11 = [DBPerson isKindOfClass:[NSObject class]];
NSLog(@"%d - %d", res10, res11); // 输出:0 - 1

isKindOfClass:isMemberOfClass的实现(objc源码中NSObject.mm文件中):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}

通过源码可以看到,isMemberOfClass:用来判断当前对象是否属于传入类的类型。isKindOfClass用来判断当前对象是否属于传入类或子类类型。需要注意的是,实例方法判断的是类类型,类方法判断的是元类类型,基类元类对象的superclass指针指向的是类对象,所以上面代码中res10res11的结果是不一样的,res6和res11是等价的。

如果isKindOfClass:右边传入的是[NSObject class],无论方法调用者是哪个类型(只要是NSObject体系下)都会成立。因为基类元类对象的superclass最终会指向类对象。

isKindOfClass:isMemberOfClass正确使用方法:对象方法需要传入类对象,实例方法需要传入元类对象。

面试题6:以下代码能不能执行成功?如果可以,打印结果是什么?

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
@interface DBPerson : NSObject

@property (nonatomic, copy) NSString *name;
- (void)print;

@end

@implementation DBPerson

- (void)print {
NSLog(@"my name is %@", self.name);
}

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

id cls = [DBPerson class];
void *obj = &cls;
[(__bridge id)obj print];
}

@end

输出结果:my name is <ViewController: 0x7fb92fc07630>

如果在cls前面加上一句其它代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

NSString *test = @"123";

id cls = [DBPerson class];
void *obj = &cls;
[(__bridge id)obj print];
}

@end

输出结果:my name is 123

如果在上面的代码基础上,DBPerson增加一个属性:

1
2
3
4
5
6
7
8
@interface DBPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

- (void)print;

@end

输出结果:my name is <ViewController: 0x7f889d707ee0>

疑问:print方法为什么可以执行(从代码层面开obj不是DBPerson类型对象)?为什么self.name变成了ViewController?为什么DBPerson只有一个属性并且在cls变量前面加一个变量时,self.name变成了新增变量?为什么DBPerson有多个属性时,self.name又变成了ViewController

正常情况下,调用print方法的代码是这样的:

1
2
DBPerson *person = [[DBPerson alloc] init];
[person print];

person指针存储的是person实例对象地址(前8个字节),而实例对象地址的前8个字节存储的就是isa指针地址,isa指针存储的是DBPerson类对象地址,最终到类对象中查找print方法。

我们看一下示例代码中的指针指向流程关系图:

cls是一个指针(占用8个字节),里面存储的是DBPerson类对象,obj指针存储的是cls的内存地址(前8个字节)。发现和上面通过person调用print方法的流程都是一样的,所以可以通过obj调用print方法。

为什么加上NSString *test = @"123";打印结果是my name is 123

很重要的一个基础知识点:局部变量都是存储在栈里面的,计算机是从高地址开始分配内存空间的,而且局部变量分配的内存地址是连续的

假设test的内存地址是0x7ffee1184108,那么cls的内存地址就是0x7ffee46e4100obj的内存地址是0x7ffee46e40f8print方法中的self就是方法调用者objself.name的本质就是取出obj偏移8个字节后的内存地址,由于obj里面只存储了cls,所以偏移8个字节后是test的内存地址,所以_name的打印信息是123

如果没有加test变量,打印结果是my name is <ViewController: 0x7f889d707ee0>,主要原因就是super viewDidLoad]。因为super的本质是objc_msgSendSuper(arg, @selector(viewDidLoad))arg是创建的一个临时结构体变量struct arg {self, [UIViewController class]}cls后偏移8个字节是self,因此打印结果是ViewController。

这个面试题考察的范围非常广:isa指针指向,栈空间内存分配(小端模式),super的本质(objc_msgSendSuper()),访问成员变量的本质(地址偏移找结构体成员)。