发送消息
在runtime学习一里边,介绍了Objective-C是动态语言,尽可能把决策推迟到运行时。runtime是由C、C++及汇编写成的一套API为Objective-C提供运行时功能。当源码编译完成,runtime将Objective-C转成C语言,今天主要研究下OC底层方法调用机制。
TestMessage *test = [[TestMessage alloc]init];
[test message];
举一个简单的列子说明,自定义一个TestMessage类,实例化一个对象test,然后使用test实例调用TestMessage类的一个实例方法。给[test message]
这段代码打上断点,运行代码查看汇编。
如图,用红线画出来的一个重要的方法名
objc_msgSend
.按住command键点击objc_msgSend,进入objc/message.h文件。由这就引出一个重要的概念,OC调用方法时其本质是给这个实力对象发送消息。
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
self
就是这个调用方法的实例对象,op
就是这个方法名的SEL。SEL
是方法编号,在method_t里边与IMP函数实现地址一一对应。objc_msgSend
函数括号里边的省略号是方法的参数列表。
由上述函数的字面意思也能大概意识到,我们使用OC的实例对象调用方法到底层是使用objc_msgSend这个函数,给我们的是实例对象发送一个@selector(message)
的一个消息。
在OC的方法中隐藏了两个参数,第一个就是id 类型的self和SEL 类型的_cmd
概念总结
isa及父类的走位流程图.
这张图一定要印在脑海里,因为它真的很重要。在底层查找方法的流程,利用这张图我们能够更好更清晰的理解其中的机制。
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
void setInfo(uint32_t set) {
assert(isFuture() || isRealized());
data()->setFlags(set);
}
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
void setFlags(uint32_t set)
{
OSAtomicOr32Barrier(set, &flags);
}
void clearFlags(uint32_t clear)
{
OSAtomicXor32Barrier(clear, &flags);
}
// set and clear must not overlap
void changeFlags(uint32_t set, uint32_t clear)
{
assert((set & clear) == 0);
uint32_t oldf, newf;
do {
oldf = flags;
newf = (oldf | set) & ~clear;
} while (!OSAtomicCompareAndSwap32Barrier(oldf, newf, (volatile int32_t *)&flags));
}
};
从类的底层结构、上图的走位图,再加上对元类的理解,简单总结几点:
- isa指针,万事万物皆对象,每个对象都有isa指针,isa指向对象所属的类。实例对象的isa指向类对象,类对象的isa指向元类,元类的isa指向根元类。
- 子类的superclass指向父类,父类superclass指向根类,根类的superclass指向nil。子元类的superclass指向父元类,父元类的superclass指向根元类,根元类的superclass又指向了根类。
- 实例对象的方法存储在类对象里边,类对象的方法(也就是通常我们所说的类方法)存储在元类里边,类对象及元类对象里边的方法都是实例方法。
当实例变量调用实例方法时,编译器将OC代码翻译成C语言,然后使用runtime系统给这个实例变量发送这个方法编号的消息。runtime发送消息本质上就是使用运行时动态的查找函数具体实现的IMP指针。
由于runtime是由汇编、C及C++编写的API,所以在动态查找方法实现时,用到了汇编,C及C++.
- 当查找类的缓存时,以及最后的消息转发用的都是汇编。
- 当缓存中没有找到时,使用C及C++查找当前类及递归查找父类的方法列表,还有就是当找到NSObject类都没有找到时,来到第一次方法动态决议时,都是C和C++。
源码分析
为了研究苹果底层发送消息的流程,我们需要在下载苹果的objc4-750源码,或者在github上下载可编译的源码。
在源码里边添加测试代码如上,然后打上断点,运行代码,进入到汇编
如上图我们可以清楚的看到objc_msgSend这个函数在libobjc.A.dylib库里边定义,也就是我们所下载的源码(objc4-750).在项目里边全局搜索objc_msgSend,我们可以看到objc-msg-arm64.s、objc-msg-simulator-i386.s及objc-msg-simulator-x86_64.s文件。我们看objc-msg-arm64.s文件,里边都是汇编代码。
1.查找缓存
ENTRY _objc_msgSend
这个是objc_msgSend
函数的入口,由这个入口开始对寄存器一顿的整,汇编我也不太会,只要重点的方法名即可。上边汇编判断完成之后进入CacheLookup NORMAL
.CacheLookup
是一个宏定义,它有三种参数:NORMAL
,GETIMP
和LOOKUP
。由入口函数我们明显可以看出本次使用的是NORMAL
参数。进入CacheLookup
宏定义从上图可以看出
CacheLookup
里边定义了三个方法:
-
CacheHit
:一个宏定义,字面理解就是命中或者找到,注释的意思就是调用或者返回imp指针。 -
CheckMiss
:宏定义,字面理解就是没有找到。 -
add
:这个方法就是进行缓存。
CacheHit
宏里边NORMAL
值表示成功找到了函数的具体实现地址IMP指针。
CheckMiss
宏里边的NORMAL
流程会调用一个__objc_msgSend_uncached
的函数。
__objc_msgSend_uncached
函数里边有一个重要的宏MethodTableLookup
,通过代码明显可以看出__objc_msgSend_uncached
执行到MethodTableLookup
宏之后就结束了。由MethodTableLookup函数名称我们很容易意会到其中的意思,那就是方法列表查找,没错由MethodTableLookup就开启了我们的慢速的方法列表查找。
由此我们先总结一下快速的缓存查找:
- 缓存的查找源代码是用汇编写成的。
- 缓存的查找结果:
-
CacheHit
即在缓存中找到了对应的IMP指针。 -
CheckMiss
即在缓存中没有找到对应的IMP指针,将进入到慢速的方法列表查找方式。 - 缓存中没有找到IMP,但是在其他的途径找到IMP则将方法编号和IMP指针进行缓存。
-
2.方法列表查找
由缓存的查找流程得到,当CheckMiss
之后,就会跳转至MethodTableLookup
宏。
宏定义里边一顿的整之后就会跳转至
__class_lookupMethodAndLoadCache3
这个函数,这个函数是汇编到C函数的一个入口。到了这,就是我们友好的能够接受的语言。从这里开始就进入到另一个方法查找的流程:方法列表查找。
进入
lookUpImpOrForward
函数慢慢的进行梳理。lookUpImpOrForward
函数是方法列表查找的主函数,下面对此函数的主要方法及流程进行分析:
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
此判断是不会走的,因为在调用方法的时候cache变量是个NO.也可以理解,因为如果有缓存就不会来到这个lookUpImpOrForward函数里边。
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
这块是给类对象发送initialize方法,暂时先不看,因为和查找方法的流程没什么关系。
retry:
runtimeLock.assertLocked();
// Try this class's cache.
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;
}
}
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;
}
retry开始就进入了重要的查找流程了.
- 判断cache里边是否有,因为多线程等原因有可能此时缓存列表中已经有了对应的IMP如果有会跳转至
done
, - 进入当前类的方法列表里边查找,如果能够找到
meth
这个方法,则会log_and_fill_cache(cls, meth->imp, sel, inst, cls);
缓存这个方法然后跳转至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;
}
}
}
递归的在父类里边查找:
- 递归的由当前类获取到父类直至NSObject类。
- 在父类的缓存中查找,由代码可知,此过程走的是汇编.(这里省略),如果拿到IMP,且IMP不是消息转发的缓存,则将IMP进行缓存。
- 如果在父类的缓存中没有找到,则进入到父类的方法列表中进行查找。找到则缓存起来。
慢速的查找流程总结:
- 由于多线程原因还是会使用汇编去查找当前类的缓存,找到则返回IMP。
- 当前类缓存中没有,则会进入该类的方法列表中查找,如果找到则缓存然后返回IMP.
- 在当前类的方法列表中没有找到,则会的获取父类,使用汇编查找父类的缓存,如果找到且不是消息转发的缓存,便会缓存然后返回IMP,如果没有找到又会进入到父类的方法列表继续进行2和3的操作直到NSObject类的方法列表中也没有找,就会进入到消息动态决议流程。
消息的查找流程有快速的查找缓存流程和慢速的查找方法列表流程
消息动态解析及转发流程
当消息经过缓存及方法列表这两种方式都没有找到IMP的话,就会进入消息的动态解析及转发。
1.消息转发流程图
看流程图可以清楚的知道消息转发处理流程。
2.消息转发代码分析
当消息查找流程结束就到了消息的动态决议流程:
- 消息的动态决议只会执行一次,从
triedResolver
这个变量可以看出,进入之后会被赋为YES. -
_class_resolveMethod
函数里边,将类和元类分开处理。 - 从当前类开始一直找到NSObject类,是否重写了
SEL_resolveInstanceMethod
(+resolveInstanceMethod)这个方法。 - 如果有重写,就会给这个类发送
resolveInstanceMethod
消息。
在resolveInstanceMethod
方法里使用使用runtime的API给这个类添加实例方法。类方法的原理一样。在这个方法里边处理完成就不会到消息转发流程,未处理里便会来到消息转发。
由_objc_msgForward_impcache
这个函数便会进入到汇编。
进入到汇编代码,此处不再看汇编的具体实现,只看下回调方法的使用.
动态决议是给类添加方法,来解决问题。
把消息转发给能响应方法的对象
将方法签名,再转发。