1.方法的本质
1.1 探索
- 在之前的文章中,我们探索了对象、类、以及isa等的本质,那么今天我们一起来分析一下方法的本质,看看OC所调用的方法究竟是什么
- 首先进入target目录下,使用clang -rewrite-objc main.m,编译生成main.cpp文件,对照二者的main函数
// oc
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [[LGPerson alloc] init];;
[person sayNB];
}
return 0;
}
// c++
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc")), sel_registerName("init"));;
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
}
return 0;
}
- 对比分析可以发现,我们调用的[person sayNB]方法,在C++中则被编译成为了objc_msgSend((id)person, sel_registerName("sayNB")),由此可以分析出,方法在底层的本质其实就是objc_msgSend,调用方法其实就是调用objc_msgSend向特定的对象发送特定的消息
- objc_msgSend(对象,方法名)
- 我们接着定义一个C语言函数,然后继续调用,并且同样使用Clang编译成C++,可以看到run函数直接就调用了
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc")), sel_registerName("init"));;
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
// run是函数,直接调用了
run();
}
return 0;
}
1.2 分析
- 所以,oc中方法调用的过程,就是利用objc_msgSend向对象发送消息,然后根据发送的SEL找到具体的函数实现地址imp,最后调用
- 实例方法:objc_msgSend(对象,sel)
- 类方法:objc_msgSend(类,sel)
- 父类:则是使用objc_msgSendSuper
- 注意,super和self指向一个对象,但是self是类隐藏参数,而super只是预编译指令符号,作用就是不经过本类的方法列表,直接通过superClass的方法列表去查找,然后利用本身(objc_super->receiver)去调用
2. objc_msgSend分析
- 首先,打开工程,在方法调用那一行,标记断点,并且在Debug-Debug WorkFlow-Always Show Disassembly勾选上,让我们开始跟踪一下当调用方法的时候,到底进行了哪些步骤
2.1 初探
- 首先,通关一连串看不懂的汇编代码中,找到了我们的init方法,sayNB方法,以及objc_msgSend方法
-
control+in进入 进入objc_msgSend方法,找到其所在为libobjc.A.dylib,那么接下来,我们可以通过源码搜索对其进行探索
2.2 探究 objc_msgSend
- 打开objc源码,全局搜索objc_msgSend方法,在objc-msg-arm.s文件中找到objc_msgSend入口
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
// person - isa - 类
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
- 简单分析一下,首先进行正常的判空以及TAGGED_POINTERS操作
- 其次通过GetClassFromIsa_p16获取当前的类
- 通过CacheLookup先到缓存中进行查找,大概就是通过汇编语言,对当前的类进行地地址偏移16个字节(isa和superClass各占据8个字节),找到cache的结构体所在,然后找到buckets进行缓存方法的查找
- 如果没有查找到,则进行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
- 在CheckMiss的汇编方法中,我们看到了,根据传递参数的不同,返回不同的结果,我们刚才传递的类型为NORMAL,所以接下来会调用__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
- 那么看一下之前的步骤,先通过isa找到类,在找到类的cache以及buckets,当缓存之中全都没有找到之后,那么接下来就去类原来的存储空间,也就是class-bits-rw-ro-methodList中去找方法的定义
- 在MethodTableLookup方法中,先是做了一系列准备工作,之后调用了__class_lookupMethodAndLoadCache3方法,进行方法查找和加载缓存,到此汇编阶段结束,准备进入C/C++阶段
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
- 这里就是最后从汇编进入到C++阶段的方法,不过这里需要把汇编中的方法去掉一个下划线,才能在源码中搜索到
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
2.4 补充
刚刚探索到最后从汇编到C++的那个步骤,也就是__class_lookupMethodAndLoadCache3,但是从这里想要进入到C++,必须要有一定的上帝视角才行,下面换个断点方法直接找到我们需要找的C++方法
依照第一段中的项目,control+in 进入objc_msgSend方法之后,在一系列看不懂的汇编中找到我们刚才寻找的__objc_msgSend_uncached方法
-
继续往下走,进入__objc_msgSend_uncached内部,在断点处,发现其跳转了一个位于objc-runtime-new.mm文件的C++方法
我们根据方法定义找到其方法定义,到此,方法查找成功从汇编过渡到C++
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
2.3 小结
- objc_msgSend是使用汇编写的,主要是速度够快,够灵活(C语言做不到写一个函数来保留未知的参数并且跳转到任意的函数指针)
- objc_msgSend首先通过快速路径对缓存进行查找,如果查找不到则进入MethodTableLookup方法列表查找,通过_class_lookupMethodAndLoadCache3进行查找并且缓存
3. 方法查找流程
3.1 分析
- 对象方法查找:SelfClass -> SuperClass ->...->NSObject -> nil
- 类方法查找:SelfMetaClass -> SuperMetaClass -> ... -> NSObjectMetaClass-> NSObject -> nil
- 如果找不到,则经典报错
3.2 流程分析
- objc_msgSend先通过缓存进行快速查找,如果找不到,则进行慢速常规查找
- 快速查找的流程就是前面分析的消息发送流程,那么现在来看_class_lookupMethodAndLoadCache3普通查找流程
lookUpImpOrForward分析
- 首先进行一系列前戏,保证当对象方法或者类方法没有找到时,能够有父类或者元类能够去让我们去继续去查找,一系列递归
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
}
- 之后进入正题,可能是当前类也可能是当前类的父类(如果当前类找不到目标方法,那么之前的realizeClass方法已经准备好了即将要查找的父类,继续查找父类即可),具体流程都是一样的,还是先判断一下当前imp有没有被缓存,如果存在缓存那么直接调用即可(这个时候可能有缓存进来)
- 之后通过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;
}
}
// 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.
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.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
- 在方法内部,通过对methods列表进行遍历,找到当前sel对应的method,如果找不到则返回nil
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过程之后,通过findMethodInSortedMethodList(sel, mlist)进行二分查找,保证能够快速寻找到目标方法
// 关键二分查找代码
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
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--;
}
return (method_t *)probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
- 最后,如果方法成功被找到,则进入log_and_fill_cache方法,内部调用cache_fill正式进入缓存流程,之后进入goto done,具体内容可以看一下iOS方法缓存-cache
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);
}
- 如果方法没有找到,那么首先会进行动态方法解析_class_resolveMethod,在这个过程中,系统会调用一次已经存在的事先定义好的两个类方法,在这里给我们提供了一次容错的机会,
// objc源码
_class_resolveInstanceMethod(cls, sel, inst)
_class_resolveClassMethod(cls, sel, inst)
// NSObject内部方法
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
- 如果我们没有做任何处理,那么则进入消息转发阶段,消息转发阶段全局搜索一下该方法。在其汇编方法内部,调用了__objc_msgForward
imp = (IMP)_objc_msgForward_impcache;
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,全局搜索找到定义,并且找到了objc_defaultForwardHandler方法的具体实现
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;
- 如果处理失败那么则报出经典错误unrecognized selector sent to instance 0x123456789
3.3 总结
- 方法查找,先是通过objc_msgSend的快速缓存查找,之后再通过对类以及父类的方法列表进行二分法常规查找
- 最后如果NSObject中都没有需要实现的方法,那么先进入动态方法解析,之后进入消息的快速转发和常规转发,消息转发处理失败之后则报错crash
- 在之后的消息转发分析中,也会向大家具体分析一下消息转发的三个阶段,以及每个阶段的作用
结束语
- iOS底层的消息发送以及方法查找的大致流程已经介绍的差不多了,之后就是底层的另一个大的功能消息转发 objc_msgForward,下次再分析哈~
- objc_msgSend的部分由于是汇编写的(本人太菜),所以只是简单介绍了一下功能,没有具体深入分析,下面给大家整一个流程图,就到这结束了~
- 诙谐学习,不干不燥,如有疑问,欢迎回复骚扰~