美文网首页
消息转发全流程

消息转发全流程

作者: 莫道别离伤 | 来源:发表于2023-08-28 11:40 被阅读0次

源码层面:

调用_objc_msgSend 汇编

  1. 先查找cache_t,没有的话找methodlist
  2. 指向父类,并重复1
  3. 如果最后父类都没有实现则走到OC层面的转发流程
  4. 如果找到了则将方法放入当前接受者类的cache中加入此方法,如果加入的时候判断,cache列表的空间即将用完,则清空cache并加倍申请空间,并将方法加入cache中,并调用此方法

源码解读:

  1. 对receiver判空
  2. 空则调用LNilOrTagged或者LReturnZero,否则3 —> 这就是我们给nil发送方法不会出问题的原因,因为直接就返回了
  3. 将receiver的地址赋值给p13,调用 GetClassFromIsa_p16
  4. GetClassFromIsa_p16调用ExtractISA计算地址
  5. 经过运算【receiver的地址与上isamask】得到isa地址
  6. 接着往下走 LGetisaDone(标签),调用 CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
  7. CacheLookup往下走到 LLookupStart\Function标签,将isa指针向后移16个地址,计算出cache_t的地址,cache_t的地址等于mask|buckets,高16位是mask,低48位是buckets,但是第一位因为与的时候没有用,应该是存了某个标志位
  8. cache_t的地址与上 #0x0000fffffffffffe得到buckets的地址
  9. 判断cache_t地址的第0位是否为0,不为0则跳转到LLookupPreopt,否则10
  10. 将sel右移7位与sel异或,再将cachet_t右移48位得到mask,将两者相与得到要查找的方法在buckets中的下标 —> 这里本质是利用哈希函数得到下表 cache_hash
  11. 从当前方法应该在buckets中的地址往前开始遍历buckets,判断bucket的sel是否和要查找的相同,找到则走cacheHit 13,未找到则往下14 —> 向前遍历的原因是因为哈希冲突的解决方案是下标位置-1,如果前面都放满了则指向buckets的尾部,再往前找位置 cache_next
  12. 每次判断sel和要查找的sel相等时,会做一个安全检测如果bucket中的某个sel为0了,则调用__objc_msgSend_uncached。这里的缘由应该这么理解,因为在cache中查找的过程中发现了一例脏数据,则默认这一段数据不可用了,则直接走了没找到的流程。
  13. cachehint会调用TailCallCachedImp,将cache中找到的imp异或上isa得到真实的imp地址返回。 —> 因为将imp存入cache时编码了一次 就是用的 imp异或isa,则cache里面存储的值是异或过后的值。
  14. 计算出当前buckets的尾部地址,从此地址往前遍历到上一次循环前得到的首地址,避免重复遍历,如果匹配到就走13,未匹配到则走__objc_msgSend_uncached
__objc_msgSend_uncached
  1. __objc_msgSend_uncached调用 MethodTableLookup
  2. MethodTableLookup调用 _lookUpImpOrForward(c语言方法)
  3. 首先检查类是否注册,初始化类、父类以及元类,做查找准备
  4. 判断是否需要再次查询缓存,需要则查一次,查到则 done_unlock,否则继续往下
  5. 调用getmethodnosupre_nolock,由于运行时的存在获得的methods是一个二维数组,循环获取调用search_method_list_inline查找方法
  6. search_method_list_inline中判断方法列表是否排序,未排序则直接暴力循环查找,排序则调用多态方法 findMethodInSortedMethodList
  7. 在findMethodInSortedMethodList方法中调用它的多态方法多一个参数的findMethodInSortedMethodList,多出来的参数根据架构不同传参不同
  8. 多参数的findMethodInSortedMethodList方法中,使用位运算的二分查找搜索对应的方法,多出来的参数是通过method_t的首地址获取methon的sel的函数,在对比中 通过传入的sel和获取到的sel对比来判断是否相等
  9. 如果没找到对应的方法,则判断是否父类是否存在,不存在为局部变量imp赋值为_objc_msgForward_impcache消息转发方法,然后break 走向11;如果找到方法则走done 13
  10. 如果父类存在则查询父类的缓存,找到则break往下走到done 13,未找到则继续循环,查询父类的方法列表,一直重复,只到触发9的父类不存在走向11
  11. 用接受者类调用 resolveMethod_locked,此方法中根据接收者类是实例类还是元类,分别调用 resolveInstanceMethod或者 resolveClassMethod,两者都调用 lookUpImpOrNilTryCache查找方法,一个查找 resolveInstanceMethod:,一个查找 resolveClassMethod:,找到后就调用(这样就走到了oc层面的动态解析),在调用完上述方法后,则会再次调用 lookUpImpOrForwardTryCache,lookUpImpOrForwardTryCache会再次调用lookUpImpOrForward
  12. 在11调用完后,这个时候已经标记为动态解析过,则不会再次走11,则走到了调用9里面赋值的_objc_msgForward_impcache,里面会调用一个未开源的 forwarding方法,在里面则是会调用走到oc的消息转发的第二步,第二步走不通,则调用第三步,第三步走不通则调用doesnotrecognizeselecoter报错
  13. 找到方法后,调用 log_and_fill_cache,在其中再调用cache的insert方法将sel和imp放入接收者类的缓存;结束后再返回imp

cache_insert:

解决几个问题:

  1. 搜寻内存扩充机制 —> 大于4分之3,则翻倍申请空间,并将之前缓存的空间释放掉,会丢失之前存储的方法
  2. 探索哈希函数的构成 —> (sel ^ (sel >> 7)) & mask
  3. 探索哈希冲突的解决方案 —> i = i-1,if i == 0, i == mask(末尾),继续i=i-1
  4. 找到为什么cache里面的imp需要异或isa才能得到真实imp的原因 —> bucket set方法,将imp异或isa后才存入的,所以取出的时候需要再次异或得到原值

x为之前已存入cache的方法数

  1. 计算如果存入此方法后的缓存大小 x+1 = X;拿到当前实际申请的地址空间大小 = Y
  2. 根据缓存的占用量做分支判断;
    2.1 如果x==0并且没有申请过缓存空间,则创建缓存,默认大小为1<<2 【初始化】
    2.2 如果X小于等于 4分之3Y,则什么也不做
    2.3 如果X大于了4分之3Y,则重新申请内存空间,申请的大小为Y的2倍,如果Y为0则是默认大小1<<2
    注:在重新申请空间时,会将之前的cache里面存的方法全丢掉。实际上是指向了新申请的内存空间块,之前的直接释放了。
  3. 存入cache中 —> 内存扩充机制
    3.1 通过哈希函数cache_hash算出下标位置i —> sel右移7位与sel异或,再与上mask mask等于 当前申请的容量-1 实际就是以0开始的末尾下标
    3.2 如果下标位置中不存在数据,则没有出现哈希冲突,则调用bucket的set方法存入,并返回 —> set方法里对imp进行了编码,编码规则为 imp异或isa
    3.3 如果下标位置存在数据,则调用cache_next解决冲突,解决方式为 i=i-1,如果i到头部了,则将i指向表的末尾mask,继续遍历i-1位置到起始i结束
    3.4 如果遍历完成仍然没有找到合适位置,则调用bad_cache报错,找到的话则插入并返回

Cache_getimp

  1. GetClassFromIsa_p16 得到isa
  2. CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
  3. 执行汇编搜索isa的cache,找到后执行cachehint
  4. 判断找到的imp是否为nil,如果是nil直接返回到上一级6,不是nil则走 AuthAndResignAsIMP
  5. 和 TailCallCachedImp一样的isa异或imp得到真实的imp地址 —> 因为将imp存入cache时编码了一次 就是用的 imp异或isa,则cache里面存储的值是异或过后的值。
  6. 调用 LGetImpMissDynamic 返回nil

OC 层面:

第一步: 动态解析【动态添加方法】
BOOL + resolveInstanceMethod:SEL
BOOL + resolveClassMethod:SEL

动态给self的sel添加imp和methodtype
本质是构建一个新的方法 让sel指向新的方法
可以是c语言方法 可以是oc方法
c语言方法直接书写type,oc可以通过method类获取

v16@0:8

v 表示返回值,16表示v在函数中的地址为函数首地址偏移16,@表示id,0表示其地址为函数首地址,:表示SEL,起地址偏移量为8
添加后返回yes,没添加返回no 返回值看源码实际没有使用

如果实现了此方法,则在源码中会多走一次重复流程,不论是否真的动态添加了方法,在第二次中则会往下走第二步;
如果没有实现则直接走向第二步消息转发

第二步: 消息转发

id - forwardingTargetForSelector:SEL

重新生成一个实例对象返回,让它处理SEL

如果未实现或者返回nil,则走向第三步

第三步:

  1. 生成方法签名
    NSMethodSignatrue - methodSigntureForSelector:SEL

    如果生成的话则走向2.
    否则就报错,未找到方法 dosenotrecognizeSelector

  2. 方法签名包装后,随意处理

此时已经不崩溃了

void - forwardInvocation:NSInvocation

Invocation中包含了 之前原始的target、SEL以及上一个方法生成的签名

相关文章

  • 消息转发流程

    一.消息转发流程 当向Objective-C对象发送一个消息,但runtime在当前类及父类中找不到此select...

  • 消息转发流程

    实例方法的流程 对象实例收到消息(SEL+参数) 根据存储在对象实例中的ISA到类对象,类对象依次查找Class ...

  • 消息转发流程

    消息转发机制 消息的查找流程分为:快速查找和慢速查找消息转发机制也分为:快速和慢速先来一个转发流程图 之前我们的消...

  • 消息转发流程

    1. 动态方法解析 voidsendMessage(idself,SEL_cmd,NSString*msg){ ...

  • 消息转发流程

    动态方法解析 备用接收者 完整消息转发

  • 消息转发流程

    iOS开发过程中我们经常会碰到这样的报错:unrecognized selector sent to instan...

  • 消息机制

    消息发送 objc_msgSend流程 动态方法解析流程 消息转发流程

  • 52个有效方法(12) - 理解消息转发机制

    NSObject的消息转发的方法 消息转发全流程 1.动态方法解析 2.备援接受者 倘若没有动态新增方法来响应该选...

  • ios 消息转发

    ios在类中,没有定义的函数,要走消息转发流程。如果不走消息转发流程,程序会奔溃。消息转发流程分四步调用。 第一步...

  • iOS开发之进阶篇(9)—— runtime运行时

    目录 前言 iOS编译流程 runtime介绍 消息发送流程 消息转发流程 Method Swizzling 参考...

网友评论

      本文标题:消息转发全流程

      本文链接:https://www.haomeiwen.com/subject/yjygmdtx.html