消息转发全流程

源码层面:

调用_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以及上一个方法生成的签名

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容