经过前几章的探索,已经了解了对象和类的底层实现,对属性、成员变量和方法的存储也有了一定的了解,明白了方法的缓存机制,那么方法到底是如何进行调用的,它的整个流程是什么样的,怎么进行转发的,我们本章来探究一下
RunTime简介
我们都知道OC是一门动态语言,分为编译时和运行时,而Runtime是OC进行运行时支持API
- Objective-C是一门动态性比较强的编程语言,根C、C++等语言有很大不同
- Objective-C的动态性是由Runtime API来支撑的
- Runtime API提供的接口基本都是C语言的,源码由C/C++/汇编语言编写
主要代码
1、Objective-C code:例如@selector()
2、NSObject的方法:例如NSSelectorFromString()
3、Runtime Api:例如sel_registerName
运行时
将代码装载在内存在需要的是进行调用就叫做运行时
编译时
Xcode中command+B就是编译时操作,将语法翻译成机器能识别的语言,编译成的可执行文件,运行的时候就是将这个可执行文件加载到内存中
方法的本质
通过运行clang命令,查看方法sayNB
在编译后是如何运行的
LGPerson *person = [LGPerson alloc];
[person sayNB];
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
通过上面的例子我们知道,方法再底层会变成objc_msgSend
方法,即通过objc_msgSend
来发送消息,其实obj
表示消息接受者,sel_registerName
是Runtime的API,表示根据一个方法名
,返回一个SEL
objc_msgSend(obj, sel_registerName("sayNB"));
SEL sel_registerName(const char *name) {
return __sel_registerName(name, 1, 1); // YES lock, YES copy
}
消息的发送
既然知道了方法的本质即为objc_msgSend
的消息发送,那么他是怎么实现的呢,通过objc源码,我们可以知道objc_msgSend
方法再底层是由汇编来实现的,我们主要研究iOS设备的arm64结构的汇编实现
使用汇编的主要原因:
- 对于一些调用频率太高的函数或操作,使用汇编来实现能够提高效率和性能,容易被机器来识别。
- 一些未知参数的识别,用C或者C++来实现比较困难
objc_msgSend
相关汇编实现如下
GetClassFromIsa_p16
方法即代表通过对象的isa & mask
即可得到类,这个和之前章节讲过的方式是一样的,只不过这里是汇编实现
快速查找流程
快速流程即通过底层汇编代码快速找到方法的调用IMP,通过上面的代码分析,我们可以查看CacheLookup NORMAL
相关代码
通过注释我们可知,该方法有3中参数NORMAL
,GETIMP
,LOOKUP
,我们目前使用的是NORMAL
参数
CacheLookup NORMAL
主要查找流程如下,基本就是通过汇编来实现上一章节中,对类的cache_find
的操作,从而找到对应缓存的IMP
CacheHit
即为把响应的IMP
返回给接受者
CheckMiss
说明类的缓存中没有响应的IMP,会调用__objc_msgSend_uncached
进行下一步查找
可以发现__objc_msgSend_uncached
的实现中,主要进行了MethodTableLookup
操作
MethodTableLookup
中主要对未知的参数进行了一系列的处理,然后,调用__class_lookupMethodAndLoadCache3
进行慢速查找,这是一个C和C++函数,所以调用起来比汇编要慢
流程小结
1.对接受者进行判空处理
2.进行taggedPoin
t等异常处理
3.获取到接受者isa
,对isa & mask
获取到class
4.通过对class的isa进行指针偏移,获取到cache_t
5.通过对cache_t中key & mask
获取到下标,查找到对应的bucket
,获取到其中的IMP
6.如果上述没有找到IMP,走到__objc_msgSend_uncached
中的MethodTableLookup
开始慢速查找
慢速查找流程
通过上述的汇编快速查找,如果没有方法的缓存,则会进入这个慢速查找的流程,那么慢速查找流程的起点是什么,我们可以通过打断点,看汇编代码查看
首先断点在需要跟踪的方法,打开Xcode中的Always Show Disassembly
即可跟踪到相对应的汇编实现
__objc_msgSend_uncached
继续跟踪,我们可以发现调用了方法_class_lookupMethodAndLoadCache3
,自此开始了慢速查找的流程
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();
❗️// 如果父类缓存列表及方法列表均找不到方法,则去父类的父类去查找。一层层进行递归查找,直到找到NSObject类
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_t缓存
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方法
就是一个简单的一个遍历方法列表
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;
}
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 {
// Linear search of unsorted method list
💡//如果方法列表没有排序好就遍历查找
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
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` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
💡// 继续向前二分查询
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
// 取出 probe
return (method_t *)probe;
}
💡// 如果keyValue > probeValue 则折半向后查询
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
_objc_msgForward_impcache
简述
在通过消息查找和动态解析失败后,最后会走到_objc_msgForward_impcache
方法,调用__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
搜索__objc_forward_handler
发现这是一个C++方法,最终实现如下,这就是最终找不到方法时,LLDB打印输出的内容
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);
}
流程小结
1.首先对需要的变量进行初始化操作和加锁操作
2.其次如果没有进行初始化,则初始化类,父类、元类等,保证方法的查找
3.在receive的方法列表中进行二分查找,如果找到,则返回,并写入缓存
4.如果没找到,则一层层递归receive的父类的缓存和方法列表,直到NSObject,找到即返回,并写入receive的缓存
5.如果没找到,则进入动态方法解析
流程,进行动态方法的解析,有则执行
6.如果没有动态方法解析,则进入消息转发
流程
7.如果上述都没有实现和处理,则最终无法找到方法,会崩溃
总结
OC的消息机制可以分为一下三个阶段:
- 消息发送阶段:从类及父类的方法缓存列表及方法列表查找方法;
- 动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责动态的添加方法实现;
- 消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理;
通过本章,我们基本了解了方法的本质和方法的查找流程,但是对于消息动态解析和消息转发的流程并没有深入了解,下一章节会着重讲解这两个部分