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

objc_msgSend
声明原型
以前是:
id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...);
Xcode11改成了:
void objc_msgSend(void);
修改原型是为了解决:接收方(被调用者)会从调用方传递参数的相同位置和格式中检索参数。也就是说,被调用者一定知道调用者把参数放在什么寄存器/内存,这样就不会取错参数。避免了调用者把参数放在a寄存器,但是被调用者去b寄存器取参数的错误行为。
例如:
- (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,但是我们依然想使用,可以用下面两种方法:
- 强制转换:
((void (*)(id, SEL, float))objc_msgSend)(obj, @selector(log:), M_PI);
会强制将double转换成float,然后放入float对应的寄存器,被调用者也是去float对应的寄存器取参数。
- 声明函数指针来调用:
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
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
//缓存的数据结构
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。(注意15%3 = 0,但是15&3 = 3。但是对这个求hash无影响,可以不考虑)
讲人话,就是当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
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。下面详细分析这几个过程:
注册类
//注册类
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父类跟元类,然后设置类的父类跟元类;添加类的方法、属性、协议;添加分类的方法、属性、协议。返回这个类的结构体
初始化类
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
retry:
runtimeLock.assertLocked();
// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;
因为多线程,此时cache可能改变了,所以需要重新来次CacheLookup。
getMethodNoSuper_nolock
// 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

// 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(方法解析)
// 如果上面都没有找到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。
// 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___函数。

- 如果类实现了forwardingTargetForSelector,调用,返回对象target跟self不同,重新调用objc_msgSend(target,sel...) 然后ret。
- 如果实现了methodSignatureForSelector,调用,返回sig,则调用forwardInvocation,然后返回结果;否则调用doesNotRecognizeSelector
// 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);
}