Objc源码之对象创建alloc和init
Objc源码之initialize实现
Objc源码之Load方法实现
Objc源码之NSObject和isa
Objc源码之引用计数实现
objc源码之Method消息发送
前言
在我们进行方法调用的时候,我们的对象是如何找到我们的方法呢?这个问题大家基本都知道是通过isa找到的,实例对象通过isa找到类对象,在类对象中查找方法,类对象通过isa指针找到元类,在元类对象中查找,那么在这个过程中究竟查找过程是怎么实现的,除了查找方法,还会进行哪些操作呢?这篇文章我们通过objc的源码来看下具体的查找过程。
一、方法调用过程
TestObject *obj = [TestObject new];
[obj test];
我们以实例对象的方法调用为例,来说明一下方法的调用过程:
1.首先[obj test]会转换成objc_msgSend(self,@ selector(test))函数调用。
2.obj通过isa指针找到类对象,实例对象的方法列表存在于类对象中。
3.类对象是一个objc_class结构体,objc_class结构中存在一个cache_t类型的cache,从cache里面的bucket_t中通过@ selector(test)为key来查找方法实现IMP。
4.如果objc_class的cache中没有查找到,就通过class_data_bits_t来获取class_rw_t来获取中的methods方法列表来查找test方法。
5.如果类对象中没有查找到对应的方法,就通过objc_class结构体中的superclass来找到对象的父类对象,然后重复3、4、5这个过程,如果还没有查找到,就会到到根类NSObject,NSObject的父对象是nil的(参考下面经典的类关系图),这个时候如果还没有查找到,就开始进入消息转发了。
6.进入消息转发阶段以后。
- 首先是调用resolveInstanceMethod:或者resolveClassMethod:,这一步可以给当前类添加方法,来响应这个过程。
- 调用forwardingTargetForSelector:,这一步是寻找一个备援接受者来响应这个而方法。
-
调用methodSignatureForSelector和forwardInvocation,完整的消息转发,通过NSInvocation来响应这个方法。
7.如果上述过程都没有响应,那么则会crash,报unrecognized selector sent to instance的错误。
二、objc_msgSend
当编译器遇到一个方法调用时,它会将方法的调用翻译成以下函数中的一个 objc_msgSend、objc_msgSend_stret、objc_msgSendSuper 和 objc_msgSendSuper_stret。发送给对象的父类的消息会使用 objc_msgSendSuper 有数据结构作为返回值的方法会使用 objc_msgSendSuper_stret 或 objc_msgSend_stret 其它的消息都是使用 objc_msgSend 发送的。
在objc_msgSend是OC实例对象和类对象发送消息的核心引擎,用来查找方法实现,对性能要求较高,因此这一部分是通过汇编代码来编写的。下面是欧阳大哥通过汇编代码,翻译的c代码深入解构objc_msgSend函数的实现
//下面的结构体中只列出objc_msgSend函数内部访问用到的那些数据结构和成员。
/*
其实SEL类型就是一个字符串指针类型,所描述的就是方法字符串指针
*/
typedef char * SEL;
/*
IMP类型就是所有OC方法的函数原型类型。
*/
typedef id (*IMP)(id self, SEL _cmd, ...);
/*
方法名和方法实现桶结构体
*/
struct bucket_t {
SEL key; //方法名称
IMP imp; //方法的实现,imp是一个函数指针类型
};
/*
用于加快方法执行的缓存结构体。这个结构体其实就是一个基于开地址冲突解决法的哈希桶。
*/
struct cache_t {
struct bucket_t *buckets; //缓存方法的哈希桶数组指针,桶的数量 = mask + 1
int mask; //桶的数量 - 1
int occupied; //桶中已经缓存的方法数量。
};
/*
OC对象的类结构体描述表示,所有OC对象的第一个参数保存是的一个isa指针。
*/
struct objc_object {
void *isa;
};
/*
OC类信息结构体,这里只展示出了必要的数据成员。
*/
struct objc_class : objc_object {
struct objc_class * superclass; //基类信息结构体。
cache_t cache; //方法缓存哈希表
//... 其他数据成员忽略。
};
/*
objc_msgSend的C语言版本伪代码实现.
receiver: 是调用方法的对象
op: 是要调用的方法名称字符串
*/
id objc_msgSend(id receiver, SEL op, ...)
{
//1............................ 对象空值判断。
//如果传入的对象是nil则直接返回nil
if (receiver == nil)
return nil;
//2............................ 获取或者构造对象的isa数据。
void *isa = NULL;
//如果对象的地址最高位为0则表明是普通的OC对象,否则就是Tagged Pointer类型的对象
if ((receiver & 0x8000000000000000) == 0) {
struct objc_object *ocobj = (struct objc_object*) receiver;
isa = ocobj->isa;
}
else { //Tagged Pointer类型的对象中没有直接保存isa数据,所以需要特殊处理来查找对应的isa数据。
//如果对象地址的最高4位为0xF, 那么表示是一个用户自定义扩展的Tagged Pointer类型对象
if (((NSUInteger) receiver) >= 0xf000000000000000) {
//自定义扩展的Tagged Pointer类型对象中的52-59位保存的是一个全局扩展Tagged Pointer类数组的索引值。
int classidx = (receiver & 0xFF0000000000000) >> 52
isa = objc_debug_taggedpointer_ext_classes[classidx];
}
else {
//系统自带的Tagged Pointer类型对象中的60-63位保存的是一个全局Tagged Pointer类数组的索引值。
int classidx = ((NSUInteger) receiver) >> 60;
isa = objc_debug_taggedpointer_classes[classidx];
}
}
//因为内存地址对齐的原因和虚拟内存空间的约束原因,
//以及isa定义的原因需要将isa与上0xffffffff8才能得到对象所属的Class对象。
struct objc_class *cls = (struct objc_class *)(isa & 0xffffffff8);
//3............................ 遍历缓存哈希桶并查找缓存中的方法实现。
IMP imp = NULL;
//cmd与cache中的mask进行与计算得到哈希桶中的索引,来查找方法是否已经放入缓存cache哈希桶中。
int index = cls->cache.mask & op;
while (true) {
//如果缓存哈希桶中命中了对应的方法实现,则保存到imp中并退出循环。
if (cls->cache.buckets[index].key == op) {
imp = cls->cache.buckets[index].imp;
break;
}
//方法实现并没有被缓存,并且对应的桶的数据是空的就退出循环
if (cls->cache.buckets[index].key == NULL) {
break;
}
//如果哈希桶中对应的项已经被占用但是又不是要执行的方法,则通过开地址法来继续寻找缓存该方法的桶。
if (index == 0) {
index = cls->cache.mask; //从尾部寻找
}
else {
index--; //索引减1继续寻找。
}
} /*end while*/
//4............................ 执行方法实现或方法未命中缓存处理函数
if (imp != NULL)
return imp(receiver, op, ...); //这里的... 是指传递给objc_msgSend的OC方法中的参数。
else
return objc_msgSend_uncached(receiver, op, cls, ...);
}
/*
方法未命中缓存处理函数:objc_msgSend_uncached的C语言版本伪代码实现,这个函数也是用汇编语言编写。
*/
id objc_msgSend_uncached(id receiver, SEL op, struct objc_class *cls)
{
//这个函数很简单就是直接调用了_class_lookupMethodAndLoadCache3 来查找方法并缓存到struct objc_class中的cache中,最后再返回IMP类型。
IMP imp = _class_lookupMethodAndLoadCache3(receiver, op, cls);
return imp(receiver, op, ....);
}
上面的代码,总结一下:
1.对象空值判断,这个就是在OC中为什么给空对象发送消息,不crash的原因。
2. 获取或者构造对象的isa数据,通过isa查找类或者元类
3. 遍历缓存哈希桶并查找缓存中的方法实现,通过cache查找是否命中缓存
4. 执行方法实现或方法未命中缓存处理函数objc_msgSend_uncached
三、lookUpImpOrForward
lookUpImpOrForward是方法调用过程的核心类,方法的查找、类的初始化、initialize都可能在这里面调用。
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
//1. 缓存查找
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.lock();
checkIsKnownClass(cls);
//2. 类是否实现
if (!cls->isRealized()) {
realizeClass(cls);
}
//3. 类是否初始化
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
retry:
runtimeLock.assertLocked();
imp = cache_getImp(cls, sel);
if (imp) goto done;
// 4.方法列表查找,查找到以后,进行缓存。
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// 5.父类方法列表查找,查找到进行缓存。
{
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 {
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;
}
}
}
// 6.如果还没有查找到。进入消息转发resolveMethod方法
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;
}
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
lookUpImpOrForward方法有如下过程:
1. 缓存中查找方法
2. 类是否实现
3.类是否初始化
4.方法列表查找,查找到以后,进行缓存。
5.父类方法列表查找,查找到进行缓存。
6.如果还没有查找到。进入消息转发resolveMethod方法
这里的方法查找过程,我在第一部分的方法调用过程中都有描述过,我重点说一下2和3,这两部分是做什么。
- 类是否实现,这一部分主要是判断类是否是第一次调用,第一次调用的时候,class_rw_t可能还没有创建好,因为方法是存在这里面的,所以要保证类已经实现。
- 类是否初始化,这一部分主要是初始化类的一些参数,包括isa指针,同时我们熟悉的Initialize方法也是在这里调用的。
四、消息转发
消息转发是在运行时进行的,大致分为三个阶段:
第一阶段是先检查接收者,看是否能通过runtime动态添加一个方法,来处理这个方法;
第二阶段就是备援接收者,看看有没有对象可以响应这个方法。
第二阶段就是把该消息的全部信息封装到NSInvocation对象中,看哪个对象能否处理,如果还无法处理,则报错unrecognized selector sent to instance。
1.动态方法解析
// 类方法专用
+ (BOOL)resolveClassMethod:(SEL)sel
// 对象方法专用
+ (BOOL)resolveInstanceMethod:(SEL)sel
2.备援接收者
- (id)forwardingTargetForSelector:(SEL)aSelector
3.完整消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation
在方法签名的过程中,注意签名符号:
* 代表 char *
char BOOL 代表 c
: 代表 SEL
^type 代表 type *
@ 代表 NSObject * 或 id
^@ 代表 NSError **
# 代表 NSObject
v 代表 void
五、总结
方法的调用过程:
1.缓存查找
2.查找当前类的缓存及方法。
3.查找父类的缓存及方法
4.消息转发
参考:
objc4-750源码
从源代码看 ObjC 中消息的发送.md
深入解构objc_msgSend函数的实现
iOS消息转发机制实例
iOS的消息转发机制详解