当前位置: 首页 > news >正文

【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之后

  1. 首先会判断消息接受者是否为空,为空就直接返回

  2. 然后会判断是否为小对象,也就是是否为tagged_pointers

  3. 之后取对象中的isa存到寄存器p13中,根据isa进行mask地址偏移来得到对应的上级对象(类、元类)

取得了上级对象之后,就可以开始快速查找流程了,也就是在缓存中找imp的过程

快速查找流程

  1. 首先通过类的首地址偏移16字节找到cache的地址(cache离首地址16字节,isa占8字节,superclass占8字节),cache高16位存mask,低48位存buckets

  2. 然后从cache中分别取出buckets和mask,根据mask通过哈希算法算出哈希下标,根据哈希下标和bukets首地址来得到对应的bucket,bucket中存放着imp和sel

  3. 那么怎么确定找到的imp和sel就是要找的那个呢?主要是通过两层循环:

    1. 第一层循环:比较bucket中的sel和objc_msgSend中第二个参数_cmd是否相等:如果相等,就直接跳转到CacheHit,即缓存命中,返回imp;如果不相等,有三种情况:

      1. 一种是一直找不到,就直接跳转到CheckMiss,因为参数$0是normal,会跳转到__objc_msgSend_uncached,看英文就能明白意思就是没找到,这时就会进入慢速查找流程

      2. 第二种是如果获取到的bucket是第一个元素,那么就手动把它设置为最后一个元素,然后进行第二层循环

      3. 如果当前bucket不是第一个元素,那就继续当前的循环

    2. 第二层循环:和第一层循环基本相同,只是如果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;
}

上面是慢速查找的源码,用自然语言来表述就是:

  1. 首先进行一次快速查找,也就是在cache缓存中查找,找到就直接返回imp,没找到就继续

  2. 先判断cls是否是已知类,如果不是就报错;再判断类是否实现,如果没实现需要先实现,这个时候实现的目的是为了确定它的父类链,ro以及rw等,方便之后数据读取和查找;还要判断是否初始化,没有就初始化

  3. 接下来进入for循环,沿着类或元类的继承链进行查找:

    1. 对于当前cls,在方法列表中使用二分查找进行查找,如果找到就进入cache写入流程并返回imp,如果没找到就返回nil

    2. 当前cls赋值为父类,如果父类为nil,imp = 消息转发,并终止递归,开始判断是否执行过动态方法解析

    3. 如果父类链中存在循环就报错

    4. 在父类中查找时,会先在父类缓存中查找,再在方法列表中查找

  4. 判断是否执行过动态方法解析,如果没有就执行动态方法解析,执行过一次的话就走消息转发流程

在二分查找过程中,如果找到的与key的value值相等,需要先排除分类方法

在进行完快速查找和慢速查找的流程之后,会进入动态方法决议和消息转发流程

动态方法决议

在查找流程没找到方法时,有一次机会补救就是动态方法决议,以实例方法为例,程序会走到resolveInstanceMethod方法:

用自然语言描述如下:

  1. 在发送resolveInstanceMethod消息前,先查找cls中有没有这个方法的实现,也就是通过lookUpImpOrNil方法进入lookUpImpOrForward慢速查找流程找这个方法:

    1. 如果没找到就直接返回

    2. 如果找到了就发送resolveInstanceMethod消息

  2. 再慢速查找实例方法的实现,又进行一次慢速查找

应用场景

使用动态方法决议可以解决一些方法未实现的报错,重写resolveInstanceMethod类方法并在其中将其指向其他方法的实现,比如有一个say666没实现,但是实现了sayMaster方法

类方法同理,将方法名改为resolveClassMethod即可

优化方案

在上面的场景中,我们需要对每一个类的方法进行重写,并且我们又知道慢速方法查找路径最后都会走到根类,因此我们可以为NSObjct添加分类来统一处理

消息转发机制

如果前面的过程都没找到该方法,那我也是没招了(bushi),那就会进行消息转发流程,消息转发流程分为快速转发和慢速转发,如果方法没有实现而崩溃报错,在崩溃之前会调用两遍动态方法决议,两遍快速转发,两遍慢速转发

快速转发流程

forwardingTargetForSelector在源码中只有声明,但是我们可以从帮助文档中看到有关于它的解释:

  • 该方法的返回对象是执行sel的新对象,也就是自己处理不了会将消息转发给别的对象进行相关方法的处理,但是不能返回self,否则会一直找不到

  • 该方法的效率较高,如果不实现,会走到forwardInvocation:方法进行处理

  • 底层会调用objc_msgSend(forwardingTarget, sel, ...);来实现消息的发送

  • 被转发消息的接受者参数、返回值等应和原方法相同

应用场景

比如TCJPerson没实现的方法,转发给实现了的TCJStudent

也可以直接调用父类的该方法,如果没找到的话会直接报错

慢速转发流程

methodSignatureForSelector慢速查找流程同样在帮助文档中寻找,可以发现forwardInvocationmethodSignatureForSelector必须同时存在

底层会通过方法签名生成一个NSInvocation,作为参数传递使用,接着查找可以响应NSInvocation中编码的消息的对象,找到后使用anInvocation将消息发送给该对象,并且anInvocation保存结果,运行时系统将提取结果并传递给原始发送者

应用场景

慢速转发的流程就是methodSignatureForSelector提供一个方法签名,然后forwardInvocation通过NSInvocation来实现消息的转发

无论在forwardInvocation方法中是否处理invocation事务,程序都不会崩溃

方法和消息的流程就到这里了,在上面的过程中你有没有注意到动态方法决议进行了两遍这个问题?它为什么会执行两遍呢?

其实第二次动态方法决议是在methodSignatureForSelectorforwardInvocation方法之间,是开始进行慢速消息转发之前再给的一次机会

http://www.dtcms.com/a/276925.html

相关文章:

  • 动物世界一语乾坤韵芳华 人工智能应用大学毕业论文 -仙界AI——仙盟创梦IDE
  • Docker Compose文件内容解释
  • 鸿蒙选择本地视频文件,并获取首帧预览图
  • 14.ResourceMangaer启动解析
  • 【java】AI内容用SSE流式输出
  • 【读书笔记】《C++ Software Design》第七章:Bridge、Prototype 与 External Polymorphism
  • 数据库3.0
  • IPC框架
  • DAY01:【ML 第一弹】机器学习概述
  • php生成二维码
  • 15.手动实现BatchNorm(BN)
  • Spring Boot中的路径变量
  • JavaEE Tomcat
  • AI大模型计数能力的深度剖析:从理论缺陷到技术改进
  • 傅里叶变换中相位作用
  • 并查集 UnionFind Test01
  • 字符串问题(哈希表解决)
  • linux:进程详解(2)
  • Java结构型模式---享元模式
  • 代码随想录|图论|14有向图的完全可达性
  • JavaScript加强篇——第八章 高效渲染与正则表达式
  • 50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | ToastNotification(推送通知)
  • C++进阶-多态2
  • 学习python调用WebApi的基本用法(2)
  • iw 命令 -- linux 无线管理
  • 利用 MySQL 进行数据清洗
  • C++类和对象(一)
  • Intel英特尔ICH7R/ICH8R/ICH9R/ICH10R系列下载地址--intel_msm_8961002 下载 Version 8.9.6.1002
  • 001_Claude开发者指南介绍
  • UNet改进(22):融合CNN与Transformer的医学图像分割新架构