Runtime — 消息发送
一、Runtime
1. Runtime介绍
Objective-C 是一门动态语言,而承载整个 OC 动态特性的就是 Runtime。
Runtime:是一套由C、C++、汇编混合编写的为OC提供运行时功能的 api。
关于 Runtime 更多内容可进入官网文档查看。
2. 我们与 Runtime 打交道有三种方式:
- 在 OC 层的交互:@selector
- NSObject 的方法:NSSelectorFromName
- Runtime 的函数: sel_registerName
二、方法的底层源码探索
1. 准备OC代码
@interface DZPerson : NSObject
- (void)sayHello;
@end
@implementation DZPerson
- (void)sayHello {
NSLog(@"%s",__func__);
}
@end
#import <Foundation/Foundation.h>
#import "DZPerson.h"
#import <objc/runtime.h>
void run(){
NSLog(@"%s",__func__);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
DZPerson *person = [DZPerson alloc];
[person sayHello]; // OC方法
run(); // C函数方法
}
return 0;
}
2. 使用clang命令查看编译期底层源码
clang -rewrite-objc main.m -o main.cpp
在 main.cpp 文件最下方,我们会看到代码被编译如下:
void run(){
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kx_p_4mn02529l53n53r5mggdl00000gn_T_main_63361f_mi_0,__func__);
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
DZPerson *person = ((DZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("DZPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
run();
}
return 0;
}
- 从上述编译期代码分析,C函数方法
run()
在编译期就确定了函数调用及实现,通过函数名(函数指针)直接找到函数。 - 而
OC
方法则需要调用objc_msgSend
函数,通过SEL
找到函数imp
,然后找到真正的函数实现,这就是我们在Runtime
经常会提到的 消息发送机制。
三、OC方法的本质
1. OC方法的本质
OC方法的本质:其实就是使用 objc_msgSend
来发送消息。
参数
id: 消息接收者
SEL: 方法编号
2. 方法对照解释
DZPerson *person = [DZPerson alloc];
[person fly];
DZPerson *person = ((DZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("DZPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("fly"));
((DZPerson *(*)(id, SEL))(void *)
是类型强转(id)objc_getClass("DZPerson")
获取FXPerson类对象sel_registerName("alloc")
等同于@selector()
- 可以通俗理解为
((类型强转)objc_msgSend)(对象, 方法调用)
3. 方法发送的几种情况
- 向对象
s
发送sayCode
消息(对象方法)
DZStudent *s = [DZStudent new];
[s sayCode];
// 消息 : 消息接受者 消息编号
objc_msgSend(s, sel_registerName("sayCode"));
- 向类
DZStudent
发送sayNB
消息(类方法)
objc_msgSend(objc_getClass("DZStudent"), sel_registerName("sayNB"));
- 向父类(元类)
DZPerson
发送sayHello
消息(父类 对象方法)
struct objc_super dzSuper;
dzSuper.receiver = s;
dzSuper.super_class = [DZPerson class];
objc_msgSendSuper(&dzSuper, @selector(sayHello));
- 向父类(元类)
DZPerson
发送sayNB
消息(父类 类方法)
struct objc_super myClassSuper;
myClassSuper.receiver = [s class];
myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 元类
objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));
四、探索消息查找机制 - 快速流程 objc_msgSend
提示:使用
objc_msgSend
函数要把校验关闭,否则编译就报错了
![]()
1. 打开Xcode汇编分析
-
准备代码
-
打开汇编,找到objc_msgSend位置
Debug -> Debug Workflow -> Always Show Disassembly
我们会看到断点停留在 17
行,下一步就是我们为之关心的 objc_msgSend
:

20
行加断点,往下运行,control+下箭头(Step into)
,进入objc_msgSend
:


我们可以看到,objc_msgSend是在 libobjc.A.dylib 源码中,下来我们进入libobjc.A.dylib 源码继续探索。
2. objc_msgSend 汇编源码分析
objc_msgSend 函数是通过 汇编 来实现的,并且不同架构下对应不同的版本,这里我们关注 arm64版本
的实现就可以了。
用汇编的原因:
1. C语言中不可能通过写一个函数来保留未知的参数并跳转到一个任意的函数指针,汇编可以
2. objc_msgSend用汇编可以足够快
-
查找objc_msgSend
既然我们知道 objc_msgSend 是关键,那就在源码中搜索它的位置。

汇编补充
如何找到汇编方法的起止:
ENTRY _objc_msgSend:ENTRY+函数名是汇编方法开始入口
END_ENTRY _objc_msgSend:END_ENTRY+函数名是汇编方法的结束
- _objc_msgSend 分析
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// 判断0号寄存器是否为空,也就是第一个参数(id:接收者) 是否为空
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
// 读取 x0 赋值到 p13,因为第一个参数是isa所以x0其实就是 isa
// isa 是在类或者元类中查找方法缓存或方法列表所必需的
ldr p13, [x0] // p13 = isa
// 通过isa 获取class 存入p16
GetClassFromIsa_p16 p13 // p16 = class
// 查找isa结束
LGetIsaDone:
// CacheLookup 先查找类中方法缓存
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
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]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// 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
// SUPPORT_TAGGED_POINTERS
#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
主要就是通过 isa
获取 class
存入 p16
,然后调用 CacheLookup
方法,查找方法缓存。
3. CacheLookup
-
CacheLookup入参说明
其中NORMAL
是CacheLookup
方法的入参,NORMAL
代表快速流程,LOOKUP
是慢速流程:
CacheLookup NORMAL|GETIMP|LOOKUP
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
-
CacheLookup 分析
CacheLookup NORMAL
是查找方法的快速流程,即:从类方法缓存中获取buckets
,然后判断bucket->sel
是否等于传入的SEL _cmd
,如果等于直接return imp
;如果没找到,就跳转LCacheMiss
。
.macro CacheLookup
// p1 = SEL, p16 = isa
// [x16, #CACHE]意思是平移16个字节得到cache结构体,cache里包含p10(buckets) 和 p11(occupied|mask)
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
// 这段其实就是_cmd & mask操作拿到散列表的下标key,然后获取buckets
#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))
/* 这里获取到buckets里的 imp 和 sel,
然后进行判断bucket->sel是否等于传入的SEL _cmd,
b.ne是b.noEqual,如果不等于,2f 跳转 "2:" 流程
如果等于,即在缓存里边找到了,直接返回imp
*/
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
4. CheckMiss
然后判断 bucket->sel 是否等于
因为在流程 "1:" 中,如果传入的 SEL _cmd
和 cache
缓存中的 bucket->sel
没有匹配到,也就是没匹配到方法,那么就跳转 "2:",然后我们就看到了 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
的参数是 NORMAL
,那么很明显,我们进入 CheckMiss
后的下一步是 __objc_msgSend_uncached
。
5. __objc_msgSend_uncached
这个方法里边的核心逻辑是 MethodTableLookup
,也就是查找方法列表。
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
6. 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)]
// 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
-
MethodTableLookup
里面巴拉巴拉的都是一些配置信息,我们重点看__class_lookupMethodAndLoadCache3
,这里其实就是查找方法的慢速流程,这个函数在当前的汇编代码里面是搜索不到具体实现的。 -
此处需要说明一下的是:
__class_lookupMethodAndLoadCache3
开头的双下划线是苹果中汇编的写法,在objc源码
中搜索,我们需要去掉一个下划线,也就是_class_lookupMethodAndLoadCache3
(objc-runtime-new.mm中)。 -
我们会发现,代码已经从汇编跳转到 C函数了,下来我们在
objc源码
中继续追踪。
五、探索消息查找机制 - 慢速流程 方法查找
在缓存中通过 objc_msgSend
进行快速查找,如果没有找到就会走 __class_lookupMethodAndLoadCache3
函数,也就是慢速查找,具体源码如下,我们来进行具体分析。
1. _class_lookupMethodAndLoadCache3
/***********************************************************************
* _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*/);
}
// initialize = YES , cache = NO , resolver = YES
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// 缓存查找,cache为NO直接跳过
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.
// lock是为了防止多线程操作; 类是否被编译
runtimeLock.lock();
checkIsKnownClass(cls);
/* 为查找方法做准备条件,如果类没有初始化时,初始化类和父类、元类等,
保证当对象方法或者类方法没有找到,能够在父类和元类中继续递归查找
*/
if (!cls->isRealized()) {
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
// 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.
// 从缓存里面查找一遍,若有直接goto done
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
// 当前类的方法列表
// 形成局部作用域,避免局部变量命名重复
{
// 在类的方法列表中查找方法,若有直接cache_fill
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.
// 在父类缓存中查找,若有直接cache_fill
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.
// getMethodNoSuper_nolock在父类的方法列表中查找方法,若有直接log_and_fill_cache (内部是cache_fill)
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();
_class_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;
}
慢速流程:
- 首先会初始化类和父类、元类,保证当对象方法或者类方法没有找到,能够在父类和元类中继续递归查找
- 在缓存中查找方法
- 在类的方法列表中查找方法,若有,
log_and_fill_cache -> cache_fill -> cache_fill_nolock
- 在父类的方法列表中查找方法,若有,
log_and_fill_cache -> cache_fill -> cache_fill_nolock
- 都没有找到,则进行动态方法解析
_class_resolveMethod
,系统会提供一次容错的机会可以针对sel
来操作一下- 如果没做任何处理,就会进入消息的转发
_objc_msgForward_impcache
,此时又进入到了汇编中__objc_msgForward
下面针对关键部分函数进行详细解析:
2. getMethodNoSuper_nolock
对 methods列表 进行遍历,找到当前 sel 对应的 method,找不到返回 nil。
search_method_list
内部是一个二分查找。
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
3. log_and_fill_cache
如果找到方法,会进入 log_and_fill_cache
,调用 cache_fill
进入缓存流程,具体参考 方法缓存cache_t
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (objcMsgLogEnabled) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cache_fill (cls, sel, imp, receiver);
}
4. _class_resolveMethod
动态方法解析 _class_resolveMethod
,找不到 imp
时 Runtime
会给一次容错机会:cls 是元类的话调用类方法的_class_resolveInstanceMethod
,实例方法调用_class_resolveInstanceMethod
。
两个方法逻辑相似,主要是 objc_msgSend
函数发送 SEL_resolveInstanceMethod
消息,再查找一下 sel
方法
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);
}
}
}
5. _objc_msgForward_impcache
如果没有任何处理,我们就会进入 消息的转发,其内部调用了 __objc_msgForward
方法,这里我们看到 __objc_msgForward
又调用了 __objc_forward_handler
方法。
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
6. __objc_forward_handler
通过对 __objc_forward_handler
的探索,我们看到如果没有找到方法,这里有一段经典的报错信息:unrecognized selector sent to instance xxx
。
// 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;