文章目录
  1. 1. 消息发送、转发流程图
  2. 2. objc_msgSend
    1. 2.1. 声明原型
      1. 2.1.1. 如何继续使用objc_msgSend
  3. 3. 消息发送
    1. 3.1. arm64源码分析
      1. 3.1.1. 1. GetIsa
      2. 3.1.2. CacheLookup
      3. 3.1.3. __objc_msgSend_uncached
    2. 3.2. C/C++源码分析
      1. 3.2.1. _class_lookupMethodAndLoadCache3
      2. 3.2.2. lookUpImpOrForward
        1. 3.2.2.1. 注册类
        2. 3.2.2.2. 初始化类
        3. 3.2.2.3. cache_getImp
        4. 3.2.2.4. getMethodNoSuper_nolock
        5. 3.2.2.5. 循环遍历父类的cache_getImp跟getMethodNoSuper_nolock
        6. 3.2.2.6. resolveMethod(方法解析)
        7. 3.2.2.7. _objc_msgForward_impcache
      3. 3.2.3. 消息转发阶段
  4. 4. 参考

分析OC方法调用过程的博客多如牛毛,为什么我还来炒剩饭,原因:

  1. 我自己虽然之前也分析过方法调用,但是没有成体系做过笔记,这次相当于自己做一个笔记,便于以后查看。
  2. 网上有详细分析,但是都是基于x86汇编分析的(因为runtime开源的代码可以在macOS上运行起来,更方便分析吧),我只对arm64汇编熟悉,我想应该也有部分同学跟我一样,所以我基于arm64汇编分析一波~
  3. 我这个是基于最新的runtime源码版本(版本号objc4-756.2,苹果官网的源码),网上分析的大多都是几年前的版本,虽然说整个逻辑基本一致,但是还是有些许不同。

消息发送、转发流程图

objc_msgSend

声明原型

以前是:

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

Xcode11改成了:

1
void objc_msgSend(void);

修改原型是为了解决:接收方(被调用者)会从调用方传递参数的相同位置和格式中检索参数。也就是说,被调用者一定知道调用者把参数放在什么寄存器/内存,这样就不会取错参数。避免了调用者把参数放在a寄存器,但是被调用者去b寄存器取参数的错误行为。

例如:

1
2
3
- (void)log: (float)x {
printf("%f\n", x);
}

因为以前是不定参数,所以objc_msgSend(obj, @selector(log:), (float)M_PI);不会报错,但是
在intel ABI上面,会出错(函数里取得的浮点数是错误的浮点数)。(因为intel ABI中,float跟double在不同的寄存器里,传一个double,但是函数参数是float,函数从float取值)。这个就是调用者把参数放在a寄存器,被调用者去b寄存器取参数。

如何继续使用objc_msgSend

显然,苹果不建议我们直接使用objc_msgSend,但是我们依然想使用,可以用下面两种方法:

  1. 强制转换:
    1
    ((void (*)(id, SEL, float))objc_msgSend)(obj, @selector(log:), M_PI);

会强制将double转换成float,然后放入float对应的寄存器,被调用者也是去float对应的寄存器取参数。

  1. 声明函数指针来调用:
    1
    2
    void (*PILog)(id, SEL, float) = (void (*)(id, SEL, float))objc_msgSend;
    PILog(obj, @selector(log:), M_PI);

虽然上面两种方法都是强制转换objc_msgSend,让我们可以直接使用objc_msgSend,但是还是不建议强制转换objc_msgSend。对于某些类型的参数,它在运行时仍可能失败,这就是为什么存在一些变体(为了适配不同cpu架构,比如arm64就不用为返回值是结构体,而专门有objc_msgSend_stret,但是其它cpu架构需要有),例如objc_msgSend_stret,objc_msgSend_fpret,objc_msgSend_fp2ret……
只要使用基本类型,就应该没问题,但是当开始使用结构体时,或使用long double和复杂类型,就得注意了。

如果我们使用[obj log:M_PI]来调用,不过什么平台的ABI,都不会出错,Xcode都会帮我们准确的翻译好的。所以没有特殊需要,不要直接使用objc_msgSend。

消息发送

arm64源码分析

arm64汇编做3件事:

1. GetIsa

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
struct objc_object {
private:
isa_t isa;
...
}

struct objc_class : objc_object {
// isa_t isa;
Class superclass;
cache_t cache;
class_data_bits_t bits;
...
}

union isa_t {
Class cls;
uintptr_t bits;
struct {
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls (4-36bits,共33bits,存放类地址): 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
};
...
};

因为对象的方法存放在对象对应的类里,所以需要获得类的地址。类的地址存放在isa的4-36 bits上,所以需要先获得isa;而对象(地址)放在X0上,对象就是objc_object结构体,所以X0里的地址就是objc_object结构体地址,结构体第一个就是isa,那么X0地址就可以看做是isa地址。所以X0&ISA_MASK(0x0000000ffffffff8ULL)就是类地址(因为类的指针要按照字节(8 bits)内存对齐,其指针后三位必定是0,33(33个1与运算)+3(填充3个0),一共36位表示)。

当X0小于0时候,说明X0是tagged pointer,通过类索引来获取类地址。

CacheLookup

1
2
3
4
5
6
7
8
9
10
11
12
13
//缓存的数据结构
typedef uint32_t mask_t;
struct cache_t {
struct bucket_t *_buckets; //哈希表地址
mask_t _mask; //哈希表的大小,值为2^n-1
mask_t _occupied; //哈希表中元素个数
}

typedef uintptr_t cache_key_t;
struct bucket_t {
cache_key_t _key; //SEL
IMP _imp; //函数指针
}

先讨论一个数学问题:
a%b=a-(a/b)*b,这个很明显吧;那么当b=2^n - 1,比如b=3、7、15等等,a%b=a&b。比如13%3=1,13&3也是等于1。

讲人话,就是当b=2^n - 1,可以用与运算(a&b)来替代模运算(a%b),但是避免了模操作的昂贵开销。汇编里,用sel&mask来代替sel%mask。

sel%mask(哈希表大小)+buckets,结果就是sel函数在缓存里的地址;如果cache里为0,说明没有缓存,调用objc_msgSend_uncached;如果发生哈希冲突,那么从后往前遍历,如果SEL跟X1匹配上了,则缓存命中;如果遍历到bucket_t的SEL为0,则调用objc_msgSend_uncached。
X12第一次遍历到buckets(哈希表表头)时,将X12置为哈希表尾,重新从后往前遍历。整个遍历过程如果遇到SEL为0,则调用objc_msgSend_uncached,X12第二次遍历到buckets时,也调用objc_msgSend_uncached,遍历过程如果缓存命中,则调用imp,直接ret。

__objc_msgSend_uncached

objc_msgSend_uncached就是调用前保存X0-X8/q0-q7寄存器,然后调用class_lookupMethodAndLoadCache3函数,返回函数imp放在x17,恢复寄存器,然后调用imp。

C/C++源码分析

_class_lookupMethodAndLoadCache3

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*/);
}

_class_lookupMethodAndLoadCache3就是调用了lookUpImpOrForward函数而已。

lookUpImpOrForward

lookUpImpOrForward函数主要干:1.类没有注册,就注册类;2.类没有初始化,就初始化类;3.分别从缓存(cache_getImp)和类方法列表(getMethodNoSuper_nolock)里遍历,寻找sel函数;4.循环从父类的缓存和方法列表遍历,直到父类为nil;5.如果还没有找到,则进行方法解析(resolveMethod);6.如果最后依然没有找到方法,就把imp赋值为_objc_msgForward_impcache,返回imp。下面详细分析这几个过程:

注册类

1
2
3
4
5
//注册类
if (!cls->isRealized()) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}

一般情况下,类在App启动加载macho文件时候,就已经注册了。但是也有特例,比如weaklink的类,可能运行到这里,还没有初始化。为什么有weaklink,就是App最低版本支持iOS9,但是却使用了iOS11 SDK的新功能,如果没有weaklink,程序里肯定是不能使用新版本的功能的。更详细介绍,请见官网

注册类(realizeClassWithoutSwift)
这个过程会申请class_rw_t空间,递归realize父类跟元类,然后设置类的父类跟元类;添加类的方法、属性、协议;添加分类的方法、属性、协议。返回这个类的结构体

初始化类

1
2
3
4
5
6
7
8
9
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
}

如果类没有初始化,先递归初始化父类,然后给这个类发送objc_msgSend(cls, SEL_initialize)方法。所以initialize不需要显示调用父类,并且子类没有实现initialize,会调用父类的initialize方法(这个方法没啥特别的,也是通过objc_msgSend来调用的)。

cache_getImp

1
2
3
4
5
6
retry:    
runtimeLock.assertLocked();

// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;

因为多线程,此时cache可能改变了,所以需要重新来次CacheLookup。

getMethodNoSuper_nolock

1
2
3
4
5
6
7
8
9
// 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;
}
}

getMethodNoSuper_nolock内部会调用search_method_list函数,search_method_list函数就是遍历类的方法列表,只不过当方法列表是排序的,就二分法查找,否则就是依次遍历。

循环遍历父类的cache_getImp跟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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 // Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
从上图可以看出,不管是类还是元类,都是一直遍历到RootClass(NSObject)。
整个过程,不过是cache中,还是methodlist中找到sel的imp,都调用log_and_fill_cache,将sel和imp放入cache中
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.");
}

// 父类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 {
// 如果imp为_objc_msgForward_impcache,说明这个sel之前寻找过,没有找到。所以退出循环
// 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;
}
}
}

resolveMethod(方法解析)

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
// 如果上面都没有找到sel的imp,就不会执行goto done;进而走到这里来,这里会调用方法解析,方法解析后,然后goto retry,
又回到上面的cache_getImp--> getMethodNoSuper_nolock -->
循环遍历父类的cache_getImp跟getMethodNoSuper_nolock -->
再次到此处,但是再次到此处时候,不会进入if里面了,因为triedResolver已经设置为YES了。
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;
}


static void resolveMethod(Class cls, SEL sel, id inst)
{
runtimeLock.assertUnlocked();
assert(cls->isRealized());
//如果类不是元类,调用resolveInstanceMethod,
//resolveInstanceMethod函数会调用objc_msgSend(cls, SEL_resolveInstanceMethod, sel);
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(cls, sel, inst);
}
else {
//如果类是元类,就调用resolveClassMethod
//resolveClassMethod函数会调用objc_msgSend(nonmeta, SEL_resolveClassMethod, sel);
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
resolveInstanceMethod(cls, sel, inst);
}
}
}

//只给出resolveInstanceMethod函数,resolveClassMethod类似。
static void resolveInstanceMethod(Class cls, SEL sel, id inst)
{
runtimeLock.assertUnlocked();
assert(cls->isRealized());

if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}

BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
//从这里可以看出执行完SEL_resolveInstanceMethod,返回的bool值,跟会不会进行消息转发无关,仅仅跟打印系统日志有关。
// 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(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

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函数跟resolveClassMethod函数,一般用来add method,他们返回的bool值,跟是否会进入消息转发无关,网上文章绝大部分都说返回YES就表示消息解析已经处理了这个消息,不会进行消息转发,而返回NO,就进入消息转发。其实是错误的,读者可以自己写demo验证。

根据上面的流程图,我们可以清楚知道,消息解析后,会重新进行类cache_getImp–> 类getMethodNoSuper_nolock –>
循环遍历父类的cache_getImp跟getMethodNoSuper_nolock,如果找到了,填充cache,然后到done,ret。如果没有找到,imp赋值为_objc_msgForward_impcache,而执行_objc_msgForward_impcache才会进入消息转发,跟resolveInstanceMethod返回的bool值确实没有关系。

_objc_msgForward_impcache

调用_objc_msgForward_impcache:(接口宏,定义在arm64里)
在arm64汇编里,最后调用了_objc_forward_handler函数。
_objc_msgForward–>_objc_forward_handler。

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
// Default forward handler halts the process.
__attribute__((noreturn)) 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;

#if SUPPORT_STRET
struct stret { int i[100]; };
__attribute__((noreturn)) struct stret
objc_defaultForwardStretHandler(id self, SEL sel)
{
objc_defaultForwardHandler(self, sel);
}
void *_objc_forward_stret_handler = (void*)objc_defaultForwardStretHandler;
#endif

void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
_objc_forward_handler = fwd;
#if SUPPORT_STRET
_objc_forward_stret_handler = fwd_stret;
#endif
}

// Define SUPPORT_STRET on architectures that need separate struct-return ABI.
#if defined(__arm64__)
# define SUPPORT_STRET 0
#else
# define SUPPORT_STRET 1
#endif

因为arm64中(不用为返回值是结构体,而需要支持objc_msgSend_stret(这也是为啥其它文章里面有许多objc_msgSend变体,而本文没有)等。),SUPPORT_STRET为0。
上面代码在arm64中,可以简洁为:

// Default forward handler halts the process.
__attribute__((noreturn)) 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;

void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
_objc_forward_handler = fwd;
}

可以看到_objc_forward_handler的默认实现是objc_defaultForwardHandler(打印系统日志,杀掉进程),
但是App在启动时候,会调用objc_setForwardHandler,重新给_objc_forward_handler赋值新的函数指针。赋值成什么函数呢?在Core Foundation
中。

消息转发阶段

Core Foundation 里面没找到objc_setForwardHandler的调用,但是打符号断点,发现App启动时候,通过_CFInitialize调用了objc_setForwardHandler函数,说明_objc_forward_handler被重新赋值了。

通过消息转发调用堆栈,发现_objc_forward_handler被替换成了_CF_forwarding_prep_0函数,_CF_forwarding_prep_0调用forwarding函数。

forwarding 函数(打符号断点看到有336行汇编)
大概做了:

  1. 如果类实现了forwardingTargetForSelector,调用,返回对象target跟self不同,重新调用objc_msgSend(target,sel…) 然后ret。
  2. 如果实现了methodSignatureForSelector,调用,返回sig,则调用forwardInvocation,然后返回结果;否则调用doesNotRecognizeSelector
1
2
3
4
5
6
7
8
9
10
11
12
13

// Replaced by CF (throws an NSException)这里说了,
也是被Core Foundation替换,其实也是打日志,抛异常。
+ (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("+[%s %s]: unrecognized selector sent to instance %p",
class_getName(self), sel_getName(sel), self);
}

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
object_getClassName(self), sel_getName(sel), self);
}

参考

  1. https://www.mikeash.com/pyblog/objc_msgsends-new-prototype.html
  2. http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/
文章目录
  1. 1. 消息发送、转发流程图
  2. 2. objc_msgSend
    1. 2.1. 声明原型
      1. 2.1.1. 如何继续使用objc_msgSend
  3. 3. 消息发送
    1. 3.1. arm64源码分析
      1. 3.1.1. 1. GetIsa
      2. 3.1.2. CacheLookup
      3. 3.1.3. __objc_msgSend_uncached
    2. 3.2. C/C++源码分析
      1. 3.2.1. _class_lookupMethodAndLoadCache3
      2. 3.2.2. lookUpImpOrForward
        1. 3.2.2.1. 注册类
        2. 3.2.2.2. 初始化类
        3. 3.2.2.3. cache_getImp
        4. 3.2.2.4. getMethodNoSuper_nolock
        5. 3.2.2.5. 循环遍历父类的cache_getImp跟getMethodNoSuper_nolock
        6. 3.2.2.6. resolveMethod(方法解析)
        7. 3.2.2.7. _objc_msgForward_impcache
      3. 3.2.3. 消息转发阶段
  4. 4. 参考