学习MJ的视频课程,整理总结知识点–objc_msgSend

本文主要从Class结构体出发,讲解class_rw_t的结构及class_rw_t中的methods,及Class中的cache_t cache

objc_msgSend简介

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

1
2
3
4
5
6
7
8
9
MJPerson *person = [MJPerson alloc] init];

[person personTest];

// 编译后对应的源码
MJPerson *person = objc_msgSend(objc_msgSend(objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init"));

// 等价于 objc_msgSend(person, @selector(personTest));
objc_msgSend(person, sel_registerName("personTest"));

可以看到,OC的函数调用转成消息发送
我们可以测试sel_registerName("personTest")@selector(personTest)两个函数的地址是相等的,其实它们两个确实是等价的,只不过是在不同环境的不同表现形式。

所以我们可以认为
OC的方法调用:消息机制,给方法调用者发消息

objc_msgSend的执行流程可以分为3大阶段

  1. 消息发送

  2. 动态方法解析
    允许动态创建一个方法出来

  3. 消息转发
    消息转发给另外的对象调用

objc_msgSend消息发送

关于消息发送,我们从objc_msgSend的源码开始解读,这一部分是汇编代码,可能是因为objc_msgSend的调用频率比较高,为了性能的考虑,使用了更底层的汇编来实现。在objc-msg-arm64.s

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
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START

cmp x0, #0 // nil check and tagged pointer check
b.le LNilOrTagged // (MSB tagged pointer looks negative) nil的话跳转到LNilOrTagged
ldr x13, [x0] // x13 = isa
and x16, x13, #ISA_MASK // x16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached 缓存查找 参数 为NORMAL

LNilOrTagged:
b.eq LReturnZero // nil check nil的话直接return

// tagged
mov x10, #0xf000000000000000
cmp x0, x10
b.hs LExtTag
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone

LExtTag:
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone

LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
MESSENGER_END_NIL
ret

END_ENTRY _objc_msgSend

ENTRY是入口的意思,_objc_msgSend先判断对象是否为空,为空时,直接return,不为空继续往下走。先难道isa去对应位置的缓存中找CacheLookup NORMAL

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
.macro CacheLookup
// x1 = SEL, x16 = isa
ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4)

ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp

2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop

3: // wrap: x12 = first bucket, w11 = mask
add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)

// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.

ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp

2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop

3: // double wrap
JumpMiss $0

.endmacro

CacheLookup的汇编代码看不懂,但是它的注释写的挺详细,以及一些关键词,我们呢也能分析出大致的流程。
拿着isa在对应的class里面找cache及里面的buckets
根据哈希计算(_cmd & mask)的结果去buckets里面找
找到了调用CacheHit,返回imp
没找到的话调CheckMiss,用loop循环找
最终还没找到的话JumpMiss
因为参数是NORMALJumpMiss的处理流程会调用__objc_msgSend_uncached
因为是未缓存的方法,__objc_msgSend_uncached流程内会调用MethodTableLookup,也就是方法列表查找
方法列表查找内部调用__class_lookupMethodAndLoadCache3
我们搜索__class_lookupMethodAndLoadCache3发现找不到匹配的关键字,因为在OC中,函数转到汇编函数时会多一个_,我们去掉下划线,搜索_class_lookupMethodAndLoadCache3,就来到了runtime的api

1
2
3
4
5
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
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
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
bool initialize, bool cache, bool resolver)
{
// Optimistic cache lookup
// 需要从缓存中取
if (cache) {
// 从缓存中取
imp = cache_getImp(cls, sel);
// 取到了直接返回
if (imp) return imp;
}

// cls未初始化要先初始化

// 从当前cls取,取到了直接返回

// 说明缓存中没取到,去当前cls的方法列表找`getMethodNoSuper_nolock`
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
// 找到了把此方法填入当前cls的缓存中
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}

// 当前cls没找到,回通过superclass指针去父类里面找,流程很类似,也是先从父类的缓存中找,没找到会去父类的方法列表中找

// 还是没找到,尝试动态解析
// No implementation found. Try method resolver once.
_class_resolveMethod(cls, sel, inst);
// goto retry;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);

if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
// 有序的话就用二分查找
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
// 无序的话就用线性遍历查找
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
}

核心点就是先去当前类的缓存中找,找不到去当前类的方法列表中找,还找不到去父类缓存中找,找不到去父类的方法列表中找,还找不到尝试走动态解析。
思路很清晰,没什么难点,可以通过一个图来表示这个过程

消息方法
objc_msgSend-消息发送

objc_msgSend动态方法解析(resolve)

lookUpImpOrForward函数内先走消息查找逻辑,找不到时会走到动态方法解析阶段,这部分代码如下

1
2
3
4
5
6
7
8
9
10
11
// No implementation found. Try method resolver once.

if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}

方法找不到时,动态方法解析阶段内部会调用resolveInstanceMethodresolveClassMethod方法,可以在这两个方法内部做动态添加函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}

假如我们调用一个未实现的对象方法[person test],程序运行时,调用test方法时会走动态方法解析,我们实现resolveInstanceMethod方法,并在此方法内部动态添加test函数的实现,即可让程序继续正常执行。

当走到动态方法方法解析阶段时,lookUpImpOrForward函数接续往下走,会goto retry,也就是重新走消息发送流程。如果程序正确的动态添加了方法,那么消息发送流程会继续正确的执行,如果没有,会走到消息转发流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)other {
NSLog(@"%s", __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(test)) {
// 获取其他方法
Method method = class_getInstanceMethod(self, @selector(other));

// 动态添加test方法的实现
// class_addMethod会把函数添加到Class里面的class_rw_t里面的methods
class_addMethod(self, sel,
method_getImplementation(method),
method_getTypeEncoding(method));

// 返回YES代表有动态添加方法
return YES;
}
return [super resolveInstanceMethod:sel];
}

此时调用test函数,实际上内部调用的是other函数
如果是调用为实现的类方法,即给类发送消息,也会走同样的流程,不过在动态解析的时候,要记得是给类的元类动态添加方法。

动态方法解析
objc_msgSend-动态方法解析

objc_msgSend消息转发(forward)

如果动态方法解析阶段没有找到方法的实现,会走到消息转发阶段

1
2
3
4
// No implementation found, and method resolver didn't help. 
// Use forwarding.

imp = (IMP)_objc_msgForward_impcache;

_objc_msgForward具体流程不开源,我们在objc-msg-arm64.s文件中搜索到如下汇编代码

1
2
3
4
5
6
7
ENTRY __objc_msgForward

adrp x17, __objc_forward_handler@PAGE
ldr x17, [x17, __objc_forward_handler@PAGEOFF]
br x17

END_ENTRY __objc_msgForward

但是在_objc_forward_handler没有找到消息转发的相关实现。
同时,如果我们调用一个未实现的方法时,运行时会报错,类似如下

1
3   CoreFoundation 0x00007fff3077c8ef ___forwarding___ + 1485

关于___forwarding___,也就是调用消息转发流程的关键,有人根据汇编写出C语言的伪代码。forwarding.c

___forwarding___.c简化的伪代码如下

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
int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);

// 调用 forwardingTargetForSelector:
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget && forwardingTarget != receiver) {
return objc_msgSend(forwardingTarget, sel, ...);
}
}

// 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

[receiver forwardInvocation:invocation];

void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
}
}

if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}

// The point of no return.
kill(getpid(), 9);
}

我们可以根据C的伪代码分析调用流程。

先判断是否实现forwardingTargetForSelector,如果实现了此方法,且返回一个对象,那么会同时把这个消息转发到这个对象上去。

1
2
3
4
5
6
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
return [[MJCat alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}

这个Demo中,是吧test方法这个消息,转发到MJCat的实例对象,也就是去调用MJCat对象的test方法。这就是消息转发的由来。

如果没有实现forwardingTargetForSelector方法,或者这个方法返回的对象为空,消息转发流程会继续走到methodSignatureForSelector,拿方法的签名,拿到对象的方法签名后,会调用forwardInvocation方法,返回值为空时,此时才调用doesNotRecognizeSelector方法,抛出错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
NSMethodSignature *methodSign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return methodSign;
}
return [super methodSignatureForSelector:aSelector];
}

// NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
// anInvocation.target 方法调用者
// anInvocation.selector 方法名
// [anInvocation getArgument:NULL atIndex:0]
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 这个函数内部可以根据业务场景实现,没有具体要求
// anInvocation.target = [[MJCat alloc] init];
// [anInvocation invoke];
// [anInvocation invokeWithTarget:[[MJCat alloc] init]];
}

不仅实例方法支持动态方法解析、消息转发阶段,类方法也是支持的,因为消息发送的本质是要求接受者、方法名。类对象也是属于对象,当然也是支持这个流程的。前面也提到过,实例方法、类方法其实没有本质区别,只不过存储的位置不一样而已。

消息转发
objc_msgSend-消息转发

这一部分可以参考消息转发-Demo进行试验。

总结

简介OC的消息机制:
OC中的方法调用其实都转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
objc_msgSend有3大阶段
消息发送(去类、父类中查找)、动态方法解析、消息转发

消息转发机制流程
先判断是否实现了forwardingTargetForSelector:方法,如果实现了此方法且此方法的返回值不为nil,那么就会给返回值对象发送对应的消息SE

如果forwardingTargetForSelector返回值为nil,那么会调用methodSignatureForSelector:方法,如果此方法返回值不为nil,那么会调用forwardInvocation:方法,否则调用doesNotRecognizeSelector:方法。

参考和源码

objc4

源码:
forwarding.c
消息转发-Demo

评论