
Runtime进行一个简单的介绍。
一:前导介绍
1. Runtime介绍
Objective-C是门动态的语言,那么它需要有编译器,而Objective-C是以Clang作为编译器前端,LLVM作为编译器后端,LLVM 将一些在编译和链接过程中的工作,放到了运行阶段。这就需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。
Runtime就是这个运行时系统,是以 C/C++和汇编编写而成的,这是因为对于编译器来说,C/C++和汇编执行效率更高,可见苹果对动态的效率作出了非常大的努力。
Runtime更多的内容以及API大家可以去
官方文档 看一下。
2. Runtime版本
Runtime其实有两个版本: “modern”和“legacy”。我们现在用的 Objective-C 2.0 采用的是现行 (Modern) 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 之后的 64 位程序中。而 macOS 较老的32位程序仍采用 Objective-C 1 中的(早期)Legacy 版本的 Runtime系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。
3. 三种交互方式
-
- 直接在 OC 层进行交互:比如 @selector
-
- NSObject 的方法:NSSelectorFromName
-
- Runtime 的函数: sel_registerName
4. Runtime应用
-
- 关联对象
-
- 方法交换
-
- 字典和模型的相互转化
-
- 实现
NSCoding的自动归档和自动解档
- 实现
二:方法调用的本质
1.方法调用
@interface WYPerson : NSObject
- (void)eat;
@end
@implementation WYPerson
-(void)eat
{
NSLog(@"吃饭");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
WYPerson *p = [[WYPerson alloc]init];
[p eat];
}
return 0;
}
我们将main.m编译成main.cpp文件看看,底层到底做了什么,如何编译.cpp文件可以看 这篇文章
int main(int argc, const char * argv[]) {
{ __AtAutoreleasePool __autoreleasepool;
WYPerson *p = ((WYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((WYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("WYPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("eat"));
}
return 0;
}
[p eat]
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("eat"));
其中真正发送消息的是objc_msgSend,这个方法有两个参数,一个是消息的接收者者为 id 类型,其实就是我们创建的对象p,第二个是方法编号 sel。
下面我们在[p eat]打上断点,通过汇编来看看底层会不会走objc_msgSend的方法,选择Debug -- Debug Workflow -- Always show Disassembly


objc_msgSend,是在libobjc.A.dylib中实现的,那么我们就需要用到这篇文章的objc源码。
三: objc_msgSend 查找流程
objc_msgSend消息查找分为快速查找和慢速查找,下面会对这两种方式分开解析。
3.1 快速查找
3.1.1 objc_msgSend 汇编入口
使用objc源码,我们快速定位到汇编代码处,文件的标志 s 代表汇编,这里我们主要看arm64下的源码

ENTRY,快速定位到objc_msgSend函数的入口。
注意: 在汇编中ENTRY 是一个伪指令,用于指定汇编程序的入口点(其实我也不懂汇编,其实我也是去度娘结合代码注释去学习)。
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
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
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
前面都是一些判断和异常检查,我们直接往后看:
ldr p13, [x0] // p13 = isa,注释写着p13 = isa,[x0]其实存的就是isa,这一步就是将x0中的值存储到p13。
GetClassFromIsa_p16 p13 // p16 = class,注释写着p16 = class,从函数名也不难看出,意思就是根据p13中的isa获取到对应的class,存入p16。
CacheLookup 进行消息查找,传递的参数是NORMAL。
3.1.2 CacheLookup 消息缓存查找
在文件中搜索CacheLookup,发现注释还比较长。
/********************************************************************
*
* CacheLookup NORMAL|GETIMP|LOOKUP
*
* Locate the implementation for a selector in a class method cache.
*
* Takes:
* x1 = selector
* x16 = class to be searched
*
* Kills:
* x9,x10,x11,x12, x17
*
* On exit: (found) calls or returns IMP
* with x16 = class, x17 = IMP
* (not found) jumps to LCacheMiss
*
********************************************************************/
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
这里就涉及到我们上篇讲的方法cache_t了,意思是这个函数会在类的方法缓存中根据 SEL 查找对应的 IMP,x1 = selector 就是说 x1 就是需要查找的 SEL,x16 是当前类,如果查找到了就调用或返回 IMP,否则跳转到 LCacheMiss。最后三个宏定义,其实NORMAL就是上面传递的参数。
下面看下CacheLookup的具体实现,已加上部分注释
.macro CacheLookup
// p1 = SEL, p16 = isa
// 这一步实际是通过isa偏移16字节,找到cache_t
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask
#endif
// 通过_cmd & mask 获取cache哈希表中的索引
and w12, w1, w11 // x12 = _cmd & mask
// 通过下标获取到对应的bucket
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// 取出bucket中的sel和要查找的方法进行比较
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
// 找到缓存并返回IMP
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
说实在的,看着这段汇编有点蒙圈。但是实际上和上篇讲的查找缓存中cache_find一样,查找对应的IMP,只不过这里是通过汇编来实现的。 下面对命中缓存CacheHit,未命中缓存CheckMiss 进行分析。
3.1.3 CacheHit 找到缓存
// CacheHit: x17 = cached IMP, x12 = address of cached IMP
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x12 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
AuthAndResignAsIMP x0, x12 // authenticate imp and re-sign as IMP
ret // return IMP
.elseif $0 == LOOKUP
AuthAndResignAsIMP x17, x12 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
因为这里传递过来的参数是NORMAL,所以只会走这句TailCallCachedImp x17, x12 // authenticate and call imp
3.1.4 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
因为入参是NORMAL,所以会走__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 重要的地方已注释
.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
......省略
.endmacro
其实MethodTableLookup做的事情很简单,就是先对一些参数进行处理,然后调用__class_lookupMethodAndLoadCache3这个C函数进行下一步的方法查找。
3.1.5 慢速查找小结
-
- 首先从objc_msgSend进来,进行一些异常处理。
-
- 通过
isa获取到对应的class。
- 通过
-
- 通过指针偏移的方式获取到
cache_t。
- 通过指针偏移的方式获取到
-
- 通过对
cache_t中key & mask获取到下标,查找到对应的bucket,获取到其中的IMP.
- 通过对
-
- 如果没有找到,会调用
__objc_msgSend_uncached函数,最终调用__class_lookupMethodAndLoadCache3进入慢速查找流程。
- 如果没有找到,会调用
3.2 慢速查找
3.2.1 _class_lookupMethodAndLoadCache3
接下来我么会研究__class_lookupMethodAndLoadCache3这个函数,但是发现全局搜索是搜索不到这个方法的。




IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
/*
cls:如果是实例方法那就是类,如果是类方法那就是元类
sel:方法名
obj:方法调用者
*/
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
注意:
- 对象方法,查找的是类对象。
- 类方法,查找的是元类对象。
3.2.2 lookUpImpOrForward
lookUpImpOrForward整个方法将分成两部分就行分析,首先我们得明确一点,当汇编快速查找没有找到的时候才会来到这里。
已加注释
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
//如果需要从缓存里面查找,那需要先从缓存里面找
// 第一次进入为 false , 因为汇编快速查找流程没找到进入.
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.lock();
checkIsKnownClass(cls);
/** - 这里是查找钱的一些准备条件为查找方法做准备条件,判断类有没有加载好。
- 如果没有加载好,那就先加载一下类信息,准备好父类、元类
- 只会加载一次.
- 具体实现可以参照realizeClass.
*/
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:
/* retry部分的代码过长,并且是整个方法的核心和分析的重点,下面会进行分析 */
done:
runtimeLock.unlock();
return imp;
}
下面我们来看下方法调用的情况。

上面分析过,要走到这个方法,那再快速查找阶段一定是没有找到缓存的,所以这里的
cache一定是NO。
3.2.3 retry:分析
1. 查找本类
// 查找本类缓存
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;
}
}
- 在本类的方法列表中查找,找到就进行缓存填充(
log_and_fill_cache),并调用done,否则继续下面的操作。- 其实这里还有一个有意思的地方,
log_and_fill_cache,最终这个方法会走到 上篇文章 所讲到的内容,cache_fill_nolock,缓存的入口,有兴趣的可以回过头看一看,串一串。
2.循环查找父类
{
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.
// 找到IMP,把方法缓存
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;
}
}
}
这段代码就是从当前类的父类开始沿着继承链循环查找,直到superClass是nil,先查找父类的cache缓存,再查找父类的 ro 中的方法列表。需要注意是,当在父类中找了IMP,会把IMP存到当前类的缓存中,而不是父类。
3.动态方法解析
如果上面的流程走完,还没有找到,就会到下面这部分
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;
}
- 这里我们发现,执行了
_class_resolveMethod(cls, sel, inst)之后,又是goto retry,重新进行了一次方法查找。>
我们先看下_class_resolveMethod(cls, sel, inst)做了什么
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);
}
}
}
- 先判断类是不是元类,如果类是元类,那方法就是类方法 调用 _class_resolveInstanceMethod。
- 如果不是元类,那么方法就是实例方法,调用 _class_resolveClassMethod。
- 调用 lookUpImpOrNil 如果没找到 , 调用 _class_resolveInstanceMethod
class_resolveInstanceMethod
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
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);
// 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));
}
}
}
// 这个方法NSObject已经实现,返回NO
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
- 先判断SEL_resolveInstanceMethod有没有实现,必然是已经实现的,因为这个方法NSObject已经实现,返回NO。
- 然后向本类发送了
SEL_resolveInstanceMethod消息,能响应吗? 肯定是能的。- 然后完成之后执行
goto retry;操作。
消息转发入口
如果经过上面在本类,和父类循环查找,动态解析,都没有处理的情况下,就来到这里,进行消息转发,也是苹果给我们处理崩溃预留的最后一条途径。
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
3.2.4 慢速查找流程总结
- 调用
_class_lookupMethodAndLoadCache3,继而调用lookUpImpOrForward方法 - 先从当前类的方法列表中查找,找到了返回
- 找不到交给父类,先从父类的缓存中查找,如果找到返回,如果没有,查找方法列表,找到了返回,找不到进行动态方法解析。
- 根据当前是类还是元类来进行对象方法动态解析和类方法动态解析。
- 如果解析成功,则返回,如果失败,进入消息转发流程。
四:全文总结
Objective-C调用一个实例方法[p eat];- 实际会调用函数
objc_msgSend(p, sel_registerName("eat")); objc_msgSend()是一个汇编函数入口objc_msgSend()会先从Person类的cache中查找eat对应的IMP- 如果没有找到缓存,会进入慢速查找流程
- 调用
_class_lookupMethodAndLoadCache3,继而调用lookUpImpOrForward方法 - 先从当前类的方法列表中查找,找到了返回
- 找不到交给父类,先从父类的缓存中查找,如果找到返回,如果没有,查找方法列表,找到了返回,找不到进行动态方法解析。
- 根据当前是类还是元类来进行对象方法动态解析和类方法动态解析。
- 如果解析成功,则返回,如果失败,进入消息转发流程。
本篇我们研究了消息查找的底层实现和流程,下一章将会沿着本章的路线研究消息转发的流程。敬请期待~