源码层面:
调用_objc_msgSend 汇编
- 先查找cache_t,没有的话找methodlist
- 指向父类,并重复1
- 如果最后父类都没有实现则走到OC层面的转发流程
- 如果找到了则将方法放入当前接受者类的cache中加入此方法,如果加入的时候判断,cache列表的空间即将用完,则清空cache并加倍申请空间,并将方法加入cache中,并调用此方法
源码解读:
- 对receiver判空
- 空则调用LNilOrTagged或者LReturnZero,否则3 —> 这就是我们给nil发送方法不会出问题的原因,因为直接就返回了
- 将receiver的地址赋值给p13,调用 GetClassFromIsa_p16
- GetClassFromIsa_p16调用ExtractISA计算地址
- 经过运算【receiver的地址与上isamask】得到isa地址
- 接着往下走 LGetisaDone(标签),调用 CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
- CacheLookup往下走到 LLookupStart\Function标签,将isa指针向后移16个地址,计算出cache_t的地址,cache_t的地址等于mask|buckets,高16位是mask,低48位是buckets,但是第一位因为与的时候没有用,应该是存了某个标志位
- cache_t的地址与上 #0x0000fffffffffffe得到buckets的地址
- 判断cache_t地址的第0位是否为0,不为0则跳转到LLookupPreopt,否则10
- 将sel右移7位与sel异或,再将cachet_t右移48位得到mask,将两者相与得到要查找的方法在buckets中的下标 —> 这里本质是利用哈希函数得到下表 cache_hash
- 从当前方法应该在buckets中的地址往前开始遍历buckets,判断bucket的sel是否和要查找的相同,找到则走cacheHit 13,未找到则往下14 —> 向前遍历的原因是因为哈希冲突的解决方案是下标位置-1,如果前面都放满了则指向buckets的尾部,再往前找位置 cache_next
- 每次判断sel和要查找的sel相等时,会做一个安全检测如果bucket中的某个sel为0了,则调用__objc_msgSend_uncached。这里的缘由应该这么理解,因为在cache中查找的过程中发现了一例脏数据,则默认这一段数据不可用了,则直接走了没找到的流程。
- cachehint会调用TailCallCachedImp,将cache中找到的imp异或上isa得到真实的imp地址返回。 —> 因为将imp存入cache时编码了一次 就是用的 imp异或isa,则cache里面存储的值是异或过后的值。
- 计算出当前buckets的尾部地址,从此地址往前遍历到上一次循环前得到的首地址,避免重复遍历,如果匹配到就走13,未匹配到则走__objc_msgSend_uncached
__objc_msgSend_uncached
- __objc_msgSend_uncached调用 MethodTableLookup
- MethodTableLookup调用 _lookUpImpOrForward(c语言方法)
- 首先检查类是否注册,初始化类、父类以及元类,做查找准备
- 判断是否需要再次查询缓存,需要则查一次,查到则 done_unlock,否则继续往下
- 调用getmethodnosupre_nolock,由于运行时的存在获得的methods是一个二维数组,循环获取调用search_method_list_inline查找方法
- search_method_list_inline中判断方法列表是否排序,未排序则直接暴力循环查找,排序则调用多态方法 findMethodInSortedMethodList
- 在findMethodInSortedMethodList方法中调用它的多态方法多一个参数的findMethodInSortedMethodList,多出来的参数根据架构不同传参不同
- 多参数的findMethodInSortedMethodList方法中,使用位运算的二分查找搜索对应的方法,多出来的参数是通过method_t的首地址获取methon的sel的函数,在对比中 通过传入的sel和获取到的sel对比来判断是否相等
- 如果没找到对应的方法,则判断是否父类是否存在,不存在为局部变量imp赋值为_objc_msgForward_impcache消息转发方法,然后break 走向11;如果找到方法则走done 13
- 如果父类存在则查询父类的缓存,找到则break往下走到done 13,未找到则继续循环,查询父类的方法列表,一直重复,只到触发9的父类不存在走向11
- 用接受者类调用 resolveMethod_locked,此方法中根据接收者类是实例类还是元类,分别调用 resolveInstanceMethod或者 resolveClassMethod,两者都调用 lookUpImpOrNilTryCache查找方法,一个查找 resolveInstanceMethod:,一个查找 resolveClassMethod:,找到后就调用(这样就走到了oc层面的动态解析),在调用完上述方法后,则会再次调用 lookUpImpOrForwardTryCache,lookUpImpOrForwardTryCache会再次调用lookUpImpOrForward
- 在11调用完后,这个时候已经标记为动态解析过,则不会再次走11,则走到了调用9里面赋值的_objc_msgForward_impcache,里面会调用一个未开源的 forwarding方法,在里面则是会调用走到oc的消息转发的第二步,第二步走不通,则调用第三步,第三步走不通则调用doesnotrecognizeselecoter报错
- 找到方法后,调用 log_and_fill_cache,在其中再调用cache的insert方法将sel和imp放入接收者类的缓存;结束后再返回imp
cache_insert:
解决几个问题:
- 搜寻内存扩充机制 —> 大于4分之3,则翻倍申请空间,并将之前缓存的空间释放掉,会丢失之前存储的方法
- 探索哈希函数的构成 —> (sel ^ (sel >> 7)) & mask
- 探索哈希冲突的解决方案 —> i = i-1,if i == 0, i == mask(末尾),继续i=i-1
- 找到为什么cache里面的imp需要异或isa才能得到真实imp的原因 —> bucket set方法,将imp异或isa后才存入的,所以取出的时候需要再次异或得到原值
x为之前已存入cache的方法数
- 计算如果存入此方法后的缓存大小 x+1 = X;拿到当前实际申请的地址空间大小 = Y
- 根据缓存的占用量做分支判断;
2.1 如果x==0并且没有申请过缓存空间,则创建缓存,默认大小为1<<2 【初始化】
2.2 如果X小于等于 4分之3Y,则什么也不做
2.3 如果X大于了4分之3Y,则重新申请内存空间,申请的大小为Y的2倍,如果Y为0则是默认大小1<<2
注:在重新申请空间时,会将之前的cache里面存的方法全丢掉。实际上是指向了新申请的内存空间块,之前的直接释放了。 - 存入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
- GetClassFromIsa_p16 得到isa
- CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
- 执行汇编搜索isa的cache,找到后执行cachehint
- 判断找到的imp是否为nil,如果是nil直接返回到上一级6,不是nil则走 AuthAndResignAsIMP
- 和 TailCallCachedImp一样的isa异或imp得到真实的imp地址 —> 因为将imp存入cache时编码了一次 就是用的 imp异或isa,则cache里面存储的值是异或过后的值。
- 调用 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,则走向第三步
第三步:
-
生成方法签名
NSMethodSignatrue - methodSigntureForSelector:SEL如果生成的话则走向2.
否则就报错,未找到方法 dosenotrecognizeSelector 方法签名包装后,随意处理
此时已经不崩溃了
void - forwardInvocation:NSInvocation
Invocation中包含了 之前原始的target、SEL以及上一个方法生成的签名