李峰峰博客

Runtime 之消息机制

2020-05-10

1、objc_msgSend 简介

在 OC 中,所有的方法调用底层都会转换成 objc_msgSend 函数进行调用,例如,对于下面方法调用:

1
2
[myObject test1];
[myObject test2:100];

将其转换成 C++ 源码:

1
2
((void (*)(id, SEL))(void *)objc_msgSend)((id)myObject, sel_registerName("test1"));
((void (*)(id, SEL, int))(void *)objc_msgSend)((id)myObject, sel_registerName("test2:"),100);

简化处理后:

1
2
objc_msgSend(myObject, sel_registerName("test1"));
objc_msgSend(myObject, sel_registerName("test2:"),100);

可在源码中找到 objc_msgSend 函数定义:

1
2
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)

objc_msgSend 函数会有一个可能为空的返回值,一般来说需要至少两个参数:消息调用者及方法名,以及不确定个数的方法参数。

objc_msgSend 伪代码如下:

1
2
3
4
5
id objc_msgSend(id self, SEL _cmd, ...) {
Class class = object_getClass(self);
IMP imp = class_getMethodImplementation(class, _cmd);
return imp ? imp(self, _cmd, ...) : 0;
}

其实 objc_msgSend 主要做的就是获取 IMP 并调用,并且在找到对应方法实现 IMP 调用的时候,会默认带入两个参数:self _cmd_cmd 就是 SEL。所以我们可以在方法里使用 self_cmd

实际上,objc_msgSend 是使用汇编来实现的,采用汇编来实现,主要是因为有的方法返回 id,有的返回 int,单独一个方法定义满足不了多种类型返回值,汇编对可变参数处理起来更方便。再加上汇编程序执行效率高,所以 objc_msgSend 这种高频率调用的函数使用汇编实现有助于提高系统运行效率。

2、objc_msgSend 源码分析

(1) 方法快速查找流程(汇编实现)

源码下载地址:https://opensource.apple.com/tarballs/objc4/

objc_msgSend 相关源码主要在 objc-msg-arm64.s 中,从源码可以看出 objc_msgSend 是使用汇编实现的。

objc_msgSend 部分源码如下:

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
    ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// 检查 objc_msgSend 第一个参数(消息接受者)是否为 nil
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
// 如果支持 tagged pointer,如果 p0 <= 0,执行 LNilOrTagged 函数
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check

// tagged
......
cmp x10, x16
b.ne LGetIsaDone

// ext tagged
......
b LGetIsaDone
#endif

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

END_ENTRY _objc_msgSend

首先会对 objc_msgSend 的第一个参数是否为 nil,即消息接收者是否存在:

  • 如果不存在,函数将直接 return,所以给 nil 对象发送消息不会引发 crash。
  • 如果存在,则继续执行,进入缓存查找逻辑 CacheLookup(传入的参数是 NORMAL)。

CacheLookup 源码如下:

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
.macro CacheLookup
// p1 = SEL, p16 = isa(p1 存放着 SEL,p16 存放这当前消息接收者的 isa 指针)
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask
#endif
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

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

2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop

3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)

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

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

2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop

3: // double wrap
JumpMiss $0

.endmacro

.macro 在汇编中用于定义宏,所以 CacheLookup 是被定义成了宏。
CacheLookup 的功能其实就是去当前类的方法缓存中去查找方法:

  • 如果在缓存中找到方法,即命中,会调用 CacheHit,直接调用对应方法或返回方法 imp。
  • 如果没有找到方法,则会调用 CheckMiss

CheckMiss 源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

因为前面 CacheLookup 传入的参数是 NORMAL,所以这里 CheckMiss 调用了 __objc_msgSend_uncached

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
51
52
53
54
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

MethodTableLookup
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached


在 __objc_msgSend_uncached 中,调用了 MethodTableLookup:
.macro MethodTableLookup

// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp

// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]

// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward

// IMP in x0
mov x17, x0

// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]

mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR

.endmacro

可以看到,MethodTableLookup 中又调用了 _lookUpImpOrForward
并且调用 lookUpImpOrForward 函数时传入参数如下:

1
2
3
4
x0 = receiver 
x1 = selector
x2 = class
x3 = LOOKUP_INITIALIZE | LOOKUP_RESOLVER

到这里,快速查找流程结束,进入慢速查找流程。

(2) 方法慢速查找流程(C/C++ 实现)

_lookUpImpOrForward 在 C++ 中对应的函数为 lookUpImpOrForward,位于 objc-runtime-new.m 中:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;

runtimeLock.assertUnlocked();

if (fastpath(behavior & LOOKUP_CACHE)) {
// cache_getImp 为汇编实现,内部实际上就是执行前面快速查找流程
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}

// 加锁
runtimeLock.lock();

// 查询是否为已知类
checkIsKnownClass(cls);

// 类没有实现,尝试实现
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}

// 类如果没有初始化,进行初始化操作
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}

runtimeLock.assertLocked();
curClass = cls;

for (unsigned attempts = unreasonableClassCount();;) {
// 当前类方法列表(如果方法列表已排序采用二分查找算法,否则直接遍历查找),如果找到,则返回,将方法缓存到 cache 中。
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp;
// 跳转到 done
goto done;
}
// curClass = superclass 继续查找,一直到 curClass == nil 的时候让 imp = forward_imp, break,终止循环
if (slowpath((curClass = curClass->superclass) == nil)) {
// 没找到,也没有父类了,使用转发
imp = forward_imp;
break;
}

// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}

// 获取父类的缓存
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// 父类中找到的是forward_imp,则终止查找
break;
}
if (fastpath(imp)) {
// 找到imp,则跳转到 done,进行方法缓存
goto done;
}
}

if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
// 进入动态方法决议
return resolveMethod_locked(inst, sel, cls, behavior);
}

done:
log_and_fill_cache(cls, imp, sel, inst, curClass);
runtimeLock.unlock();
done_nolock:
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}

通过源码可以发现,是通过 getMethodNoSuper_nolock 函数去类的方法列表查找方法:

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
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();

ASSERT(cls->isRealized());
// fixme nil cls?
// fixme nil sel?

auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
// <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
// caller of search_method_list, inlining it turns
// getMethodNoSuper_nolock into a frame-less function and eliminates
// any store from this codepath.
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}

return nil;
}

通过函数中以下源码:

1
2
3
4
5
6
auto const methods = cls->data()->methods();

cls->data() 返回的就是 class_rw_t:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}

可知,查找的方法列表就是 class_rw_t 中的 methods

getMethodNoSuper_nolock 函数中读取了方法列表后,具体 sel 查找是通过 search_method_list_inline 函数:

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
ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);

if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}

#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif

return nil;
}

由以上源码可知,查找方法列表前会对否排好序进行判断,如果方法列表已排序,则会调用 findMethodInSortedMethodList 函数进行查找,findMethodInSortedMethodList 函数实际上进行的是进行二分查找,通过二分查找提高查找效率。否则直接遍历去查询方法列表。

lookUpImpOrForward 源码分析:

  • 1、对类做合法性检查及处理,判断是否时已知类、类是否实现、是否已经初始化,如果类没有实现或初始化,进行一下对应的实现及初始化操作。

    • 函数的 behavior 参数 valueLOOKUP_INITIALIZE | LOOKUP_RESOLVER,所以不再去查找缓存(因为之前已经查找过缓存了)。
  • 2、到当前类的方法列表中、查询此方法,如果找到方法,则将方法(Method 中的 IMP)存放到当前类的 cache 中去,并且返回 IMP,方法结束。如果没找到,则执行第 3 步。

    • 此处当前类的方法列表即 class_rw_t 中的 methods
    • 查找方法列表前会对否排好序进行判断,如果方法列表已排序,使用二分查找算法进行查找,提高查找效率。否则直接遍历去查询方法列表。
  • 3、递归向父类中查找,先查找缓存,再查找类方法列表,如果找到方法,执行第 4 步(forward_imp 判断)。如果没有找到,imp = forward_imp,然后执行第 4 步。

  • 4、判断此方法是否是 forward_imp 方法,如果不是,则将此方法存放到当前类的 cache 中,并且返 IMP,方法结束。如果此方法是 forward_imp,直接执行第 5 步。

    • forward_imp 实际上就是:_objc_msgForward_impcache
    • _objc_msgForward_impcache 其实是一个存放在内存中的函数指针,为汇编实现,内部会调用 __objc_msgForward 函数(消息转发的函数)。
  • 6、判断当前是否执行执行过方法解析(动态方法决议),如果没有执行过,进入动态方法解析阶段。如果执行过 1 次动态方法解析,则走到消息转发流程。

(3) 动态方法解析(动态方法决议)

根据前面 lookUpImpOrForward 源码可知,在方法查找时,如果发现方法是 forward_imp,会判断是否执行执行过方法解析,如果没有执行过,进入动态方法解析阶段,进入 resolveMethod_locked 函数。
resolveMethod_locked 函数源码如下:

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
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());

runtimeLock.unlock();

if (! cls->isMetaClass()) {
// 不是元类
// try [cls resolveInstanceMethod:sel]
// 对象方法的动态方法解析
resolveInstanceMethod(inst, sel, cls);
}
else {
// 是元类
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
// 类方法的动态方法解析
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNil(inst, sel, cls)) {
// 对象方法的动态方法解析
resolveInstanceMethod(inst, sel, cls);
}
}

// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

其中调用的 lookUpImpOrNil 函数源码如下:

1
2
3
4
5
static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}

可以看到,lookUpImpOrNil 函数内部就是直接调用了 lookUpImpOrForward 函数,重新进入慢速查找流程,注意这里 behavior 的参数值,进到 lookUpImpOrForward 里后是需要先去缓存中查找方法的(快速查找流程)。

resolveInstanceMethod 函数源码如下:

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
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);

if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
// 如果你没有实现类方法 +(BOOL)resolveInstanceMethod:(SEL)sel
// NSObject 已经实现了,所以一般不会走这里
return;
}

// 调用类方法: +(BOOL)resolveInstanceMethod:(SEL)sel,
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);

// 检测是否有 sel 对应的 IMP。如在 +(BOOL)resolveInstanceMethod:(SEL)sel 中动态添加了 sel 对应方法,此时再次去查找这个 IMP 就能找到,并且在这一步就会将其保存到缓存中,下次调用从缓存中就可以找到了。
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNil(inst, sel, cls);


if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}

resolveInstanceMethod 函数源码解析:

  • 如果实现了 +(BOOL)resolveInstanceMethod:(SEL)sel 方法直接调用该方法。

  • 调用 +(BOOL)resolveInstanceMethod:(SEL)sel 方法后,还会调用一次 lookUpImpOrNil 函数查找一下方法。所以如果在 +(BOOL)resolveInstanceMethod:(SEL)sel 方法中动态添加了 sel 对应方法之后,调用 lookUpImpOrNil 函数就能找到对应 imp,并且会将方法保存到缓存里,下次就能直接从缓存中找到并调用了。

  • 根据源码可发现 +(BOOL)resolveInstanceMethod:(SEL)sel 方法返回值并没什么实际用处,只用来做日志打印而已,所以返回 YES 还是 NO 都无所谓。但是在实际开发的时候还应当按照 OC 规范去做,处理了 sel 就返回 YES,否则返回 NO。

resolveClassMethod 主要逻辑和 resolveInstanceMethod 基本一样,源码如下:

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
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());

if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}

Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNil(inst, sel, cls);

if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}

动态方法解析总结:

  • 如果不是元类(说明当前是对象方法),进行对象方法的动态方法解析。

    • 如果实现了 +(BOOL)resolveInstanceMethod:(SEL)sel 方法并且在方法中动态添加了 sel 对应方法,后面调用 lookUpImpOrNil 函数就能找到对应 imp,并且会将方法保存到缓存里。下次就能直接从缓存中找到 sel 并调用了。
  • 如果是元类(说明当前是类方法),先进行类方法的动态方法解析,如果没在 + (BOOL)resolveClassMethod:(SEL)sel 中动态添加 sel 对应方法,则执行一次对象方法解析。

    • 上面如果 !lookUpImpOrNil(inst, sel, cls) 条件成立,说明执行过类方法的动态方法解析之后仍然没有找到 sel,也就是没在 + (BOOL)resolveClassMethod:(SEL)sel 中动态添加 sel 对应方法。
    • 这里为什么要再执行一次对象方法解析?因为元类的父类是根元类,根元类的父类是根类(NSObject)。如果整个继承链都没找到 +(BOOL)resolveClassMethod:(SEL)sel 方法,最终会找到根类(NSObject),根类是 class 对象,class 对象中保存的是对象方法,所以需要再做一次对象方法动态方法决议。


对象方法、类方法动态方法解析查找路径:

  • 对象方法:类 -> 父类 -> 根类 -> nil
  • 类方法:元类 -> 根元类 -> 根类 -> nil

根据以上查找路径可知,无论是对象方法的动态方法解析还是类方法的动态方法解析,都会查找到根类(NSObject),所以都会走到根类(NSObject)的 +(BOOL)resolveInstanceMethod:(SEL)sel 方法。也就是说,我们可以直接使用 +(BOOL)resolveInstanceMethod:(SEL)sel 方法处理对象方法和类方法的动态方法解析。

动态方法解析 Demo:

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
// 对象方法的动态方法解析
- (void)myInstanceMethod {
NSLog(@"%s",__func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
// 调用了未实现的对象方法 xxxxInstanceMethod
if (sel == @selector(xxxxInstanceMethod)) {
Method resolveMethod = class_getInstanceMethod(self, @selector(myInstanceMethod));
IMP myInstanceIMP = method_getImplementation(resolveMethod);
const char* types = method_getTypeEncoding(resolveMethod);
NSLog(@"%s", types);
return class_addMethod(self, sel, myInstanceIMP, types);
}
return [super resolveInstanceMethod:sel];
}


// 类方法的动态方法解析
+ (void)myClassMethod {
NSLog(@"%s", __func__);
}

+ (BOOL)resolveClassMethod:(SEL)sel {
// 调用了未实现的类方法 xxxxClassMethod
if (sel == @selector(xxxxClassMethod)) {
Method resolveMethod = class_getClassMethod(self, @selector(myClassMethod));
IMP myClassIMP = method_getImplementation(resolveMethod);
const char* types = method_getTypeEncoding(resolveMethod);
NSLog(@"%s", types);
return class_addMethod(object_getClass(self), sel, myClassIMP, types);
}
return [super resolveClassMethod:sel];
}

也可以全都在 + (BOOL)resolveInstanceMethod:(SEL)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
- (void)myInstanceMethod {
NSLog(@"%s",__func__);
}

+ (void)myClassMethod {
NSLog(@"%s", __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(xxxxInstanceMethod)) {
// 调用了未实现的对象方法 xxxxInstanceMethod
Method resolveMethod = class_getInstanceMethod(self, @selector(myInstanceMethod));
IMP myInstanceIMP = method_getImplementation(resolveMethod);
const char* types = method_getTypeEncoding(resolveMethod);
NSLog(@"%s", types);
return class_addMethod(self, sel, myInstanceIMP, types);
} else if (sel == @selector(xxxxClassMethod)) {
// 调用了未实现的类方法 xxxxClassMethod
Method resolveMethod = class_getClassMethod(self, @selector(myClassMethod));
IMP myClassIMP = method_getImplementation(resolveMethod);
const char* types = method_getTypeEncoding(resolveMethod);
NSLog(@"%s", types);
return class_addMethod(object_getClass(self), sel, myClassIMP, types);
}
return NO;
}

(4) 消息转发

根据 lookUpImpOrForward 函数实现可知,如果执行过方法动态解析仍然没有找到对应方法,则走到消息转发流程,lookUpImpOrForward 方法也会把需要消息转发的 _objc_msgForward_impcache 作为 imp 返回。

_objc_msgForward_impcache 在汇编中实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b __objc_msgForward

END_ENTRY __objc_msgForward_impcache



ENTRY __objc_msgForward

adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17

END_ENTRY __objc_msgForward

可以看到 _objc_msgForward_impcache 是一个内部函数指针,最终会拿到 __objc_forward_handler 的地址并调用,其 OC 实现如下:

1
2
3
4
5
6
7
8
9
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

其中 _objc_fatal 作用就是打日志并调用 __builtin_trap() 触发 crash,根据以上源码可知,这里是将 objc_defaultForwardHandler 赋值给 _objc_forward_handler,但是再看下 objc_defaultForwardHandler 实现发现,这里只是 crash 并打印信息,也是那个熟悉的 crash:unrecognized selector sent to instance xxxxxxxx,并没有其他内容。

所以,要想实现消息转发,就需要在某处给 _objc_forward_handler 重新赋值:

1
2
3
4
5
6
7
void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
_objc_forward_handler = fwd;
#if SUPPORT_STRET
_objc_forward_stret_handler = fwd_stret;
#endif
}

上面 _objc_forward_stret_handler 是用在非 arm64 下的,这里不再深究。

objc_setForwardHandler 的调用,以及之后的消息转发相关方法调用栈,是在 CoreFoundation 中的,但是 Apple 故意在相关开源的代码中删除了在 CFRuntime.c 文件 __CFInitialize() 中调用 objc_setForwardHandler 的代码(CoreFoundation 源码地址)。
借助逆向可分析下:

根据逆向结果可以发现,__CFInitialize() 中是以 __CF_forwarding_prep_0___forwarding_prep_1___ 作为参数调用的。

接下来制造一个简单 crash 查看方法调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
MessageCrashTest[7942:944693] -[TestObject test]: unrecognized selector sent to instance 0x100508d80
MessageCrashTest[7942:944693] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[TestObject test]: unrecognized selector sent to instance 0x100508d80'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff34801b57 __exceptionPreprocess + 178
1 libobjc.A.dylib 0x00007fff6d6765bf objc_exception_throw + 48
2 CoreFoundation 0x00007fff34880be7 -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
3 CoreFoundation 0x00007fff347663bb ___forwarding___ + 1009
4 CoreFoundation 0x00007fff34765d98 _CF_forwarding_prep_0 + 120
5 MessageCrashTest 0x0000000100000f26 main + 79
6 libdyld.dylib 0x00007fff6e81ecc9 start + 1
)

可以看到 _CF_forwarding_prep_0 函数内部调用了 ___forwarding___ 函数,然后调用 doesNotRecognizeSelector 方法,最后抛出异常。

___forwarding___ 也是不开源的,但是国外大神通过逆向分析后写了伪代码( 链接 ):

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
void __forwarding__(BOOL isStret, void *frameStackPointer, ...) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 4);

Class receiverClass = object_getClass(receiver);

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

const char *className = class_getName(object_getClass(receiver));
const char *zombiePrefix = "_NSZombie_";
size_t prefixLen = strlen(zombiePrefix);
if (strncmp(className, zombiePrefix, prefixLen) == 0) {
CFLog(kCFLogLevelError,
@"-[%s %s]: message sent to deallocated instance %p",
className + prefixLen,
sel_getName(sel),
receiver);
<breakpoint-interrupt>
}

if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature) {
BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
if (signatureIsStret != isStret) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.",
sel_getName(sel),
signatureIsStret ? "" : not,
isStret ? "" : not);
}
if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature
frame:frameStackPointer];
[receiver forwardInvocation:invocation];

void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
receiver,
className);
return 0;
}
}
}

const char *selName = sel_getName(sel);
SEL *registeredSel = sel_getUid(selName);

if (sel != registeredSel) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
sel,
selName,
registeredSel);
} else if (class_respondsToSelector(receiverClass, @selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
receiver,
className);
}

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

其逻辑大致如下:

  • 1、调用 forwardingTargetForSelector,会返回一个新的 target,使用这个 target 作为新的 receiver 执行 selector,如果返回内容为 nil 或者和旧 receiver 一样,进行第 2 步。
  • 2、调用 methodSignatureForSelector 获取方法签名后,判断返回类型信息是否正确,然后调用 forwardInvocation 执行 NSInvocation 对象,并将结果返回。如果对象没实现 methodSignatureForSelector 方法,进行第 3 步。
  • 3、调用 doesNotRecognizeSelector 方法。

也就是说,调用了未实现的方法,如果在动态方法解析阶段未处理,我们还可以在消息转发阶段通过以下处理方式避免 crash,例如:

假设调用了当前类(AObject)中未实现的如下方法,且没有在动态方法解析阶段动态处理该方法:

1
- (int)test:(int)arg;

我们现在将其转发到 BObject 中的如下相同方法:

1
- (int)test:(int)arg;

方式 1:在 AObject 类中实现如下方法转发到 BObject:

1
2
3
4
5
6
7
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(test:)) {
// 返回一个可以处理方法的对象,需保证对象里有相同方法
return [[BObject alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}

如果 AObject 类没有实现该方法,还可以使用下面方式转发消息。

方式 2:在 AObject 类中实现如下方法执行 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
25
26
27
28
29
30
31
// 首先,调用如下方法返回对应方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(test:)) {
// 根据 Types 获取方法签名
// return [NSMethodSignature signatureWithObjCTypes:"v20@0:8i16"];
// return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
// 如果 BObject 中实现了相同方法,可以使用下面方法获取方法签名
return [[[BObject alloc] init] methodSignatureForSelector:aSelector];
}
return [super methodSignatureForSelector:aSelector];
}

// 然后在下面方法中做转发处理
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (anInvocation.selector == @selector(test:)) {
// 方法 1:调用 BObject 中同名方法
// [anInvocation invokeWithTarget:[[BObject alloc] init]];

// 方法 2:修改 anInvocation,改为调用 BObject 中其他方法
// anInvocation.target == [[BObject alloc] init]
// anInvocation.selector == xxxxTest:
// [anInvocation invoke];


// 方法 3:直接使用 BObject 的对象调用 test 方法
// [[[BObject alloc] init] test:xxxxx]

// 方法 4:什么都不做也可以,也不会 crash 了

}
}

对于类方法的处理,也是一样的,只不过改调用对应类方法而已:

1
2
3
4
5
6
7
8
9
// 转发方式 1
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(test:)) {
// 注意,由于是类方法,所以这里应当返回 class,当然,如果想转发后调用 BObject 中的对象方法,这里就返回对象(例如:return [[BObject alloc] init];)即可
return [BObject class];
}

return [super forwardingTargetForSelector:aSelector];
}

同理,如果 AObject 类没有实现上面方法,还可以使用下面方式转发消息:

1
2
3
4
5
6
7
8
9
10
11
12
// 转发方式 2
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
}

return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation {
// 转发逻辑,或不做任何处理
}

NSInvocation 其他两个常用方法:

  • 获取参数
    1
    2
    3
    int arg;
    // 如果只有一个参数 arg,那么这个参数的 index 是 2,即第三个参数,因为前两个参数是 self 和 _cmd
    [anInvocation getArgument:&arg atIndex:2];
  • 获取返回值
    1
    2
    int ret;
    [anInvocation getReturnValue:&ret];

3、消息发送完整流程图

附上一张本人绘制的消息发送完整流程图: