【iOS】方法与消息底层分析
目录
前言
方法的本质
向不同对象发送消息
发送实例方法
发送类方法
对象调用方法 实际执行是父类
向父类发送类方法
消息查找流程
开始查找
快速查找流程
慢速查找流程
动态方法决议
应用场景
优化方案
消息转发机制
快速转发流程
应用场景
慢速转发流程
应用场景
前言
在OC底层中,方法的调用实质上是通过消息的发送实现的,这篇文章我们来看一看消息的发送是怎么样的
方法的本质
方法的本质就是通过objc_msgSend
发送消息,有两个参数,第一个是id类型,表示消息接受者,第二个,表示方法编号。
向不同对象发送消息
发送实例方法
消息接收者是实例对象
发送类方法
本质上是向类对象发送消息
objc_getClass得到的是类对象
对象调用方法 实际执行是父类
Runtime中提供了一个接口处理这种情况:父类中实现了该方法,而子类没有实现该方法,子类对象调用方法,会执行父类中实现(符合继承的特性)
这个接口是objc_msgSendSuper
,使用时还需要用到objc_super
结构体,并给结构体赋值(receiver、super_class)
该结构体中receiver表示接收消息的实例对象,super_class表示父类类对象,根据这个赋值
可以看到这两种方式都是执行父类的实现,因此可以推断:方法调用首先在类中查找,如果找不到就到父类中查找
向父类发送类方法
上面向父类发送实例方法时,receiver表示实例对象,super_class表示父类类对象。而如果向父类发送类方法,reciever表示类对象,super_class表示父类元类对象
消息查找流程
消息查找的流程就是通过上层的sel发送消息objc_msgSend找到底层具体imp的实现的过程,objc_msgSend是用汇编写的而不是用C语言
开始查找
在开始objc_msgSend之后
-
首先会判断消息接受者是否为空,为空就直接返回
-
然后会判断是否为小对象,也就是是否为tagged_pointers
-
之后取对象中的isa存到寄存器p13中,根据isa进行mask地址偏移来得到对应的上级对象(类、元类)
取得了上级对象之后,就可以开始快速查找流程了,也就是在缓存中找imp的过程
快速查找流程
-
首先通过类的首地址偏移16字节找到
cache
的地址(cache离首地址16字节,isa占8字节,superclass占8字节),cache
高16位存mask
,低48位存buckets
-
然后从cache中分别取出buckets和mask,根据mask通过哈希算法算出哈希下标,根据哈希下标和bukets首地址来得到对应的bucket,bucket中存放着imp和sel
-
那么怎么确定找到的imp和sel就是要找的那个呢?主要是通过两层循环:
-
第一层循环:比较bucket中的sel和objc_msgSend中第二个参数_cmd是否相等:如果相等,就直接跳转到CacheHit,即缓存命中,返回imp;如果不相等,有三种情况:
-
一种是一直找不到,就直接跳转到
CheckMiss
,因为参数$0是normal,会跳转到__objc_msgSend_uncached,看英文就能明白意思就是没找到,这时就会进入慢速查找流程 -
第二种是如果获取到的bucket是第一个元素,那么就手动把它设置为最后一个元素,然后进行第二层循环
-
如果当前bucket不是第一个元素,那就继续当前的循环
-
-
第二层循环:和第一层循环基本相同,只是如果bucket还是等于buckets中第一个元素,就直接跳转到
JumpMiss
,此时也会跳转到没找到__objc_msgSend_uncached,进入慢速查找
-
慢速查找流程
慢速查找的过程分为汇编和C两个部分,这里我们不纠结汇编部分,汇编最后调用的是lookUpImpOrForward,这是一个C实现的函数
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{// 定义的消息转发const IMP forward_imp = (IMP)_objc_msgForward_impcache;IMP imp = nil;Class curClass;
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {// The first message sent to a class is often +new or +alloc, or +self// which goes through objc_opt_* or various optimized entry points.//// However, the class isn't realized/initialized yet at this point,// and the optimized entry points fall down through objc_msgSend,// which ends up here.//// We really want to avoid caching these, as it can cause IMP caches// to be made with a single entry forever.//// Note that this check is racy as several threads might try to// message a given class for the first time at the same time,// in which case we might cache anyway.behavior |= LOOKUP_NOCACHE;}
// runtimeLock is held during isRealized and isInitialized checking// to prevent races against concurrent realization.
// runtimeLock is held during method search to make// method-lookup + cache-fill atomic with respect to method addition.// Otherwise, a category could be added but ignored indefinitely because// the cache was re-filled with the old value after the cache flush on// behalf of the category.
//加锁,目的是保证读取的线程安全runtimeLock.lock();
// We don't want people to be able to craft a binary blob that looks like// a class but really isn't one and do a CFI attack.//// To make these harder we want to make sure this is a class that was// either built into the binary or legitimately registered through// objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.//判断是否是一个已知的类:判断当前类是否是已经被认可的类,即已经加载的类checkIsKnownClass(cls);
//判断类是否实现,如果没有,需要先实现,此时的目的是为了确定父类链,方法后续的循环cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);// runtimeLock may have been dropped but is now locked againruntimeLock.assertLocked();curClass = cls;
// The code used to lookup the class's cache again right after// we take the lock but for the vast majority of the cases// evidence shows this is a miss most of the time, hence a time loss.//// The only codepath calling into this without having performed some// kind of cache lookup is class_getInstanceMethod().//----查找类的缓存// unreasonableClassCount -- 表示类的迭代的上限//(猜测这里递归的原因是attempts在第一次循环时作了减一操作,然后再次循环时,仍在上限的范围内,所以可以继续递归)for (unsigned attempts = unreasonableClassCount();;) {if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHESimp = cache_getImp(curClass, sel);if (imp) goto done_unlock;curClass = curClass->cache.preoptFallbackClass();
#endif} else {// curClass method list.//---当前类方法列表(采用二分查找算法),如果找到,则返回,将方法缓存到cache中Method meth = getMethodNoSuper_nolock(curClass, sel);if (meth) {imp = meth->imp(false);goto done;}//当前类 = 当前类的父类,并判断父类是否为nilif (slowpath((curClass = curClass->getSuperclass()) == nil)) {// No implementation found, and method resolver didn't help.// Use forwarding.//--未找到方法实现,方法解析器也不行,使用转发imp = forward_imp;break;}}
// Halt if there is a cycle in the superclass chain.// 如果父类链中存在循环,则停止if (slowpath(--attempts == 0)) {_objc_fatal("Memory corruption in class list.");}
// Superclass cache.// --父类缓存imp = cache_getImp(curClass, sel);if (slowpath(imp == forward_imp)) {// Found a forward:: entry in a superclass.// Stop searching, but don't cache yet; call method// resolver for this class first.// 如果在父类中找到了forward,则停止查找,且不缓存,首先调用此类的方法解析器break;}if (fastpath(imp)) {// Found the method in a superclass. Cache it in this class.//如果在父类中,找到了此方法,将其存储到cache中goto done;}}
// No implementation found. Try method resolver once.//没有找到方法实现,尝试一次方法解析
if (slowpath(behavior & LOOKUP_RESOLVER)) {//动态方法决议的控制条件,表示流程只走一次behavior ^= LOOKUP_RESOLVER;return resolveMethod_locked(inst, sel, cls, behavior);}
done:if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHESwhile (cls->cache.isConstantOptimizedCache(/* strict */true)) {cls = cls->cache.preoptFallbackClass();}
#endif//存储到缓存log_and_fill_cache(cls, imp, sel, inst, curClass);}done_unlock://解锁runtimeLock.unlock();if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {return nil;}return imp;
}
上面是慢速查找的源码,用自然语言来表述就是:
-
首先进行一次快速查找,也就是在cache缓存中查找,找到就直接返回imp,没找到就继续
-
先判断cls是否是已知类,如果不是就报错;再判断类是否实现,如果没实现需要先实现,这个时候实现的目的是为了确定它的父类链,ro以及rw等,方便之后数据读取和查找;还要判断是否初始化,没有就初始化
-
接下来进入for循环,沿着类或元类的继承链进行查找:
-
对于当前cls,在方法列表中使用二分查找进行查找,如果找到就进入cache写入流程并返回imp,如果没找到就返回nil
-
当前cls赋值为父类,如果父类为nil,
imp = 消息转发
,并终止递归,开始判断是否执行过动态方法解析 -
如果父类链中存在循环就报错
-
在父类中查找时,会先在父类缓存中查找,再在方法列表中查找
-
-
判断是否执行过动态方法解析,如果没有就执行动态方法解析,执行过一次的话就走消息转发流程
在二分查找过程中,如果找到的与key的value值相等,需要先排除分类方法
在进行完快速查找和慢速查找的流程之后,会进入动态方法决议和消息转发流程
动态方法决议
在查找流程没找到方法时,有一次机会补救就是动态方法决议,以实例方法为例,程序会走到resolveInstanceMethod方法:
用自然语言描述如下:
-
在发送resolveInstanceMethod消息前,先查找cls中有没有这个方法的实现,也就是通过lookUpImpOrNil方法进入lookUpImpOrForward慢速查找流程找这个方法:
-
如果没找到就直接返回
-
如果找到了就发送resolveInstanceMethod消息
-
-
再慢速查找实例方法的实现,又进行一次慢速查找
应用场景
使用动态方法决议可以解决一些方法未实现的报错,重写resolveInstanceMethod类方法并在其中将其指向其他方法的实现,比如有一个say666没实现,但是实现了sayMaster方法
类方法同理,将方法名改为resolveClassMethod即可
优化方案
在上面的场景中,我们需要对每一个类的方法进行重写,并且我们又知道慢速方法查找路径最后都会走到根类,因此我们可以为NSObjct添加分类来统一处理
消息转发机制
如果前面的过程都没找到该方法,那我也是没招了(bushi),那就会进行消息转发流程,消息转发流程分为快速转发和慢速转发,如果方法没有实现而崩溃报错,在崩溃之前会调用两遍动态方法决议,两遍快速转发,两遍慢速转发
快速转发流程
forwardingTargetForSelector在源码中只有声明,但是我们可以从帮助文档中看到有关于它的解释:
-
该方法的返回对象是执行
sel
的新对象,也就是自己处理不了会将消息转发给别的对象进行相关方法的处理,但是不能返回self
,否则会一直找不到 -
该方法的效率较高,如果不实现,会走到
forwardInvocation:
方法进行处理 -
底层会调用
objc_msgSend(forwardingTarget, sel, ...);
来实现消息的发送 -
被转发消息的接受者参数、返回值等应和原方法相同
应用场景
比如TCJPerson没实现的方法,转发给实现了的TCJStudent
也可以直接调用父类的该方法,如果没找到的话会直接报错
慢速转发流程
methodSignatureForSelector慢速查找流程同样在帮助文档中寻找,可以发现forwardInvocation
和methodSignatureForSelector
必须同时存在
底层会通过方法签名生成一个NSInvocation,作为参数传递使用,接着查找可以响应NSInvocation中编码的消息的对象,找到后使用anInvocation将消息发送给该对象,并且anInvocation保存结果,运行时系统将提取结果并传递给原始发送者
应用场景
慢速转发的流程就是methodSignatureForSelector提供一个方法签名,然后forwardInvocation通过NSInvocation来实现消息的转发
无论在forwardInvocation方法中是否处理invocation事务,程序都不会崩溃
方法和消息的流程就到这里了,在上面的过程中你有没有注意到动态方法决议进行了两遍这个问题?它为什么会执行两遍呢?
其实第二次动态方法决议是在methodSignatureForSelector
和 forwardInvocation
方法之间,是开始进行慢速消息转发之前再给的一次机会