Runtime初探(三)

在 Objective-C 语言中,实例对象执行方法,而执行方法的过程也可以称为给实例对象发送消息。
当你随便写下一段函数调用的代码后,

[receiver message];

都会被编译器转化为

id objc_msgSend(id self, SEL op, ...);

本文将分析objc_msgSendobjc4-756.2objc-msg-arm64.s文件的汇编实现

objc_msgSend

objc_msgSend虽然开源但是是用汇编实现的,好在有详细对的注释。对于不熟悉汇编的人通过注释也能够看得懂

1
2
3
4
5
6
7
8
9
    ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame

cmp p0, #0 // nil check and tagged pointer check
b.le LNilOrTagged // (MSB tagged pointer looks negative)
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or

这段代码主要做了两个事情

  1. 校验tagged是否为空。如果为空则return
  2. 如果不为空则跳转到CacheLookup处。参数为NORMAL

CacheLookup

.macro CacheLookup
    // p1 = SEL, p16 = isa
    ldp    p10, p11, [x16, #CACHE]    // p10 = buckets, p11 = occupied|mask
    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

代码逻辑主要为从当前类对象缓存中寻找函数。
1.找到则调用。
2.未找到则跳转到CheckMiss

CheckMiss

.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的入参$0NORMAL,所以此处CheckMiss的入参也是NORMAL。所以回跳转到__objc_msgSend_uncached

__objc_msgSend_uncached

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search

MethodTableLookup
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached

主要逻辑是调用MethodTableLookup寻找函数地址并将其存放在x17寄存器中调用。

MethodTableLookup

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
.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)]

// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3

// 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

前后一堆操作地址可以忽略。主要是看__class_lookupMethodAndLoadCache3这个调用,这个函数式使用C语言实现的,可以不用看汇编了。

_class_lookupMethodAndLoadCache3

1
2
3
4
5
6
7
8
9
10
11
/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

_class_lookupMethodAndLoadCache3 主要的逻辑是调用lookUpImpOrForward函数,并且通过传参可以看出cache是为NO,因为之前在汇编代码CacheLookup中已经使用过缓查找。接下来主要分析lookUpImpOrForward的实现。

lookUpImpOrForward

这个函数主要做了有三件事情

  1. 查找函数地址
  2. 如果未找到函数地址,进入方法决议流程。
  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
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134

/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup.
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known.
* If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use
* must be converted to _objc_msgForward or _objc_msgForward_stret.
* If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;

runtimeLock.assertUnlocked();

// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}

// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.

// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.

runtimeLock.lock();
checkIsKnownClass(cls);

if (!cls->isRealized()) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}

if (initialize && !cls->isInitialized()) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
// runtimeLock may have been dropped but is now locked again

// If sel == initialize, class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}


retry:
runtimeLock.assertLocked();

// Try this class's cache.

imp = cache_getImp(cls, sel);
if (imp) goto done;

// Try this class's method lists.
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}

// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}

// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}

// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}

// No implementation found. Try method resolver once.

if (resolver && !triedResolver) {
runtimeLock.unlock();
resolveMethod(cls, sel, inst);
runtimeLock.lock();
// 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;
}

// No implementation found, and method resolver didn't help.
// Use forwarding.

imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);

done:
runtimeLock.unlock();

return imp;
}

由于代码很长,就不逐行分析了,之写下大致流程。

  1. 查找函数地址
    1.1 如果类对象第一次接受到消息,会调用initialize函数
    1.2 从当前的类对象缓存中查找函数(如果是类方法则从当前的元类对象缓存查找,以下同理)
    1.3 上面未找到,则从当前类对象的函数列表中查找,如果找到,将其放入当前类对象的缓存中。
    1.4 上面未找到,则逐级从父类的缓存和方法列表中查找,如果找到,如果找到,将其放入当前类对象的缓存中。
    1.5 上面未找到,跳入重定向流程
  2. 方法决议
    2.1 如果当前类是类对象,同时有实现+ resolveInstanceMethod。则调用resolveInstanceMethod,然后跳转到1.1
    2.2 如果当前类是元类类对象,同时有实现+ resolveClassMethod。则调用resolveClassMethod,然后跳转到1.1
    2.3 如果经过上面步骤还是无法查找到函数地址则进入消息转发阶段。
  3. 消息转发
    3.1 由于消息转发的实现_objc_msgForward_impcache是闭源的,目前无法查看,但是大致做的事情有很多博客有说明。
    3.2 调用[+ -] forwardingTargetForSelector尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作
    3.3 调用[+ -] methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给[+ -]forwardInvocation:
    3.4 调用[+ -] forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,接下来如何操作由程序员自行处理。
    3.5 如果上面都符合的话就会抛出doesNotRecognizeSelector异常。