探索底层原理,积累从点滴做起。大家好,我是Mars。
往期回顾
iOS底层原理探索—OC对象的本质
iOS底层原理探索—class的本质
iOS底层原理探索—KVO的本质
iOS底层原理探索— KVC的本质
iOS底层原理探索— Category的本质(一)
iOS底层原理探索— Category的本质(二)
iOS底层原理探索— 关联对象的本质
iOS底层原理探索— block的本质(一)
iOS底层原理探索— block的本质(二)
iOS底层原理探索— Runtime之isa的本质
iOS底层原理探索— Runtime之class的本质
今天继续带领大家探索iOS之Runtime
的本质。
前言
OC是一门动态性比较强的编程语言,它的动态性是基于Runtime
的API
。Runtime
在我们的实际开发中占据着重要的地位,在面试过程中也经常遇到Runtime
相关的面试题,我们在之前几期的探索分析时也经常会到Runtime
的底层源码中查看相关实现。Runtime
对于iOS
开发者的重要性不言而喻,想要学习和掌握Runtime
的相关技术,就要从Runtime
底层的一些常用数据结构入手。掌握了它的底层结构,我们学习起来也能达到事半功倍的效果。今天研究OC
的消息机制
。
消息机制
OC
语言中方法调用通过消息机制
来实现,方法调用其实都是转换为 objc_msgSend
函数调用。
OC
的消息机制
可以分为一下三个阶段:
1、消息发送阶段:从类及父类的方法缓存列表及方法列表查找方法;
2、动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责动态的添加方法实现;
3、消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理;
如果消息转发也没有实现,就会报出经典的错误:unrecognzied selector sent to instance
,方法找不到的错误,无法识别消息。
接下来我们通过源码分析消息机制
的三个阶段分别是如何实现的。
1、消息发送
在项目中方法调用的频率很高,所以为了能够提升效率,在底层代码中objc_msgSend
函数的实现是通过汇编语言编写的,我们在源码中找到objc-msg-arm64.s
汇编文件,来具体分析一下objc_msgSend
函数的实现。
objc_msgSend
函数中首先判断消息接收者receiver
是否为空。如果传入的消息接受者为nil
则会执行LNilOrTagged
,LNilOrTagged
内部会执行LReturnZero
,而LReturnZero
内部则直接return 0
。
如果传入的消息接收者receiver
不为空则通过消息接收者receiver
的isa
指针找到消息接收者的class
,执行CacheLookup
从方法缓存中取查找。如果在方法缓存列表找到则执行CacheHit
,调用方法或者返回函数地址;如果找到就执行CheckMiss
。CheckMiss
内调用__objc_msgSend_uncached
,方法没有被缓存。
__objc_msgSend_uncached
内会执行MethodTableLookup
,去方法列表中查找。MethodTableLookup
内部的核心代码__class_lookupMethodAndLoadCache3
也就是c
语言函数_class_lookupMethodAndLoadCache3
(双下划线开头变成单下划线)。
以上分析我们用简单的流程图来总结:
接下来我们进入
_class_lookupMethodAndLoadCache3
函数,分析是如何从方法列表中查找方法。
_class_lookupMethodAndLoadCache3函数
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
函数内部调用lookUpImpOrForward
方法,传入三个BOOL
类型的参数。
lookUpImpOrForward 函数
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
//接收传入的参数, initialize = YES , cache = NO , resolver = YES
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// 缓存查找, 因为cache传入的为NO, 这里不会进行缓存查找, 因为在汇编语言中CacheLookup已经查找过
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.read();
if (!cls->isRealized()) {
runtimeLock.unlockRead();
runtimeLock.write();
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
}
retry:
runtimeLock.assertReading();
// 防止动态添加方法,缓存会变化,再次查找缓存。
imp = cache_getImp(cls, sel);
// 如果找到imp方法地址, 直接调用done, 返回方法地址
if (imp) goto done;
// 查找方法列表, 传入类对象和方法名
{
// 根据sel去类对象里面查找方法
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
// 如果方法存在,则缓存方法,
// 内部调用的就是 cache_fill。
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
// 方法缓存之后, 取出函数地址imp并返回
imp = meth->imp;
goto done;
}
}
// 如果类方法列表中没有找到, 则去父类的缓存中或方法列表中查找方法
{
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.");
}
// 查找父类的缓存
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// 在父类中找到方法, 在本类中缓存方法, 注意这里传入的是cls, 将方法缓存在本类缓存列表中, 而非父类中
log_and_fill_cache(cls, imp, sel, inst, curClass);
// 执行done, 返回imp
goto done;
}
else {
// 跳出循环, 停止搜索
break;
}
}
// 查找父类的方法列表
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
// 同样拿到方法, 在本类进行缓存
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
// 执行done, 返回imp
goto done;
}
}
}
// ---------------- 消息发送阶段完成,没有找到方法实现,进入动态解析阶段 ---------------------
//首先检查是否已经被标记为动态方法解析,如果没有才会进入动态方法解析
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
//将triedResolver标记为YES,下次就不会再进入动态方法解析
triedResolver = YES;
goto retry;
}
// ---------------- 动态解析阶段完成,进入消息转发阶段 ---------------------
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
// 返回方法地址
return imp;
}
getMethodNoSuper_nolock 函数
方法列表中查找方法
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// cls->data() 得到的是 class_rw_t
// class_rw_t->methods 得到的是methods二维数组
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
// mlists 为 method_list_t
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
getMethodNoSuper_nolock
函数中通过遍历方法列表拿到method_list_t
最终通过search_method_list
函数查找方法
search_method_list
函数
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
// 如果方法列表已经排序好了,则通过二分查找法查找方法,以节省时间
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// 如果方法列表没有排序好就遍历查找
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
return nil;
}
findMethodInSortedMethodList
函数内二分查找实现原理
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
assert(list);
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
// >>1 表示将变量n的各个二进制位顺序右移1位,最高位补二进制0。
// count >>= 1 如果count为偶数则值变为(count / 2)。如果count为奇数则值变为(count-1) / 2
for (count = list->count; count != 0; count >>= 1) {
// probe 指向数组中间的值
probe = base + (count >> 1);
// 取出中间method_t的name,也就是SEL
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
// 取出 probe
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
// 返回方法
return (method_t *)probe;
}
// 如果keyValue > probeValue 则折半向后查询
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
通过以上分析,我们了解了消息机制
中的第一阶段消息发送阶段,下面我们用一张图来总结一下整体流程:
2、动态方法解析
当在类和父类的方法缓存列表、方法列表中都找不到方法时,就会进入动态方法解析阶段。我们在消息发送阶段源码中看到,进入动态方法解析阶段是通过函数_class_resolveMethod
。
_class_resolveMethod函数
函数内部会根据是元类还是类,并且类方法和对象方法的动态方法解析是调用不同的函数:
动态解析对象方法时,会调用
+(BOOL)resolveInstanceMethod:(SEL)sel
方法。动态解析类方法时,会调用
+(BOOL)resolveClassMethod:(SEL)sel
方法。
动态解析方法之后,会将triedResolver = YES;
那么下次就不会在进行动态解析阶段了,之后会回到消息发送阶段,重新执行retry
,重新对方法查找一遍。
我们可以利用动态方法解析来动态的添加方法。我们将
MPerson
类中的test
方法实现注释掉,用other
方法的实现来替代test
方法实现:从图中的可以看到,我们注释掉
test
方法实现后系统已经报出了警告,下面我们测试一下代码:当调用
MPerson
的test
方法时,打印了[MPerson other]
。动态添加方法成功。
这里需要注意class_addMethod
函数用来向具有给定名称和实现的类添加新方法,class_addMethod
将添加一个方法实现的覆盖,但是不会替换已有的实现。也就是说如果上述代码中已经实现了-(void)test
方法,则不会再动态添加方法。
3、消息转发阶段
如果上面两个阶段都失败的话,就会来到第三阶段:消息转发阶段。
由于OC
中消息机制并不是开源的,这里就直接将消息转发的原理告诉给大家了。
进入消息转发阶段后,就会判断是否指定了其它对象来执行方法。具体查看当前类是否实现了forwardingTargetForSelector
函数,如果返回值不为空,那么说明指定了转发目标,那么就会让转发目标处理消息。
如果forwardingTargetForSelector
函数返回为nil
,没有指定转发目标,就会调用methodSignatureForSelector
方法,用来返回一个方法签名,这也是跳转方法的最后机会。
如果methodSignatureForSelector
方法返回正确的方法签名就会调用forwardInvocation
方法,forwardInvocation
方法内提供一个NSInvocation
类型的参数,NSInvocation
封装了一个方法的调用,包括方法的调用者,方法名,以及方法的参数。在forwardInvocation
函数内修改方法调用对象即可。
如果methodSignatureForSelector
返回的为nil
,就会来到doseNotRecognizeSelector:
方法内部,程序crash
报出经典的错误unrecognized selector sent to instance
。
至此,OC
的消息机制
的分析就告一段落,OC
中的方法调用其实都是转成了objc_msgSend
函数的调用,给方法调用者(receiver)发送一条消息(selector方法名)。方法调用过程包括三个阶段:消息发送、动态方法解析、消息转发。
更多技术知识请关注公众号
iOS进阶