iOS之武功秘籍 文章汇总html
上文说到cache_t
缓存的是方法,咱们分析了cache
的写入流程,在写入流程以前,还有一个cache
读取流程,即objc_msgSend
和 cache_getImp
.那么方法又是什么呢?这一切都要从Runtime
开始提及...c++
Runtime
是一套API
,由c、c++、汇编
一块儿写成的,为OC
提供了运行时.github
OC、Swift
)翻译成机器语言(汇编等),最后变成二进制Runtime
有两个版本——Legacy
和Modern
,苹果开发者文档都写得清清楚楚算法
源码中-old
、__OBJC__
表明Legacy
版本,-new
、__OBJC2__
表明Modern
版本,以此作兼容缓存
Runtime
底层通过编译会提供一套API
和供FrameWork
、Service
使用sass
Runtime
调用方式:markdown
Runtime API
,如 sel_registerName()
,class_getInstanceSize
NSObject API
,如 isKindOf()
OC
上层方式,如 @selector()
原来日常在用的这么多方法都是Runtime
啊,那么方法到底是什么呢?app
经过clang
编译成cpp文件
能够看到底层代码,获得方法的本质iphone
clang -rewrite-objc main.m -o main.cpp
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
或xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
((TCJPerson *(*)(id, SEL))(void *)
是类型强转(id)objc_getClass("TCJPerson")
获取TCJPerson
类对象sel_registerName("alloc")
等同于@selector()
便可以理解为((类型强转)objc_msgSend)(对象, 方法调用)
方法的本质是经过objc_msgSend
发送消息,id
是消息接收者,SEL
是方法编号.
注意:若是外部定义了C函数
并调用如void sayHello() {}
,在clang
编译以后仍是sayHello()
而不是经过objc_msgSend
去调用.由于发送消息就是找函数实现的过程,而C函数
能够经过函数名
——指针
就能够找到.
为了验证,经过objc_msgSend
方法来完成[person sayHello]
的调用,查看其打印是不是一致. 其打印结果以下,发现是一致的,因此
[person sayHello]
等价于objc_msgSend(person,sel_registerName("sayHello"))
这其中须要注意两点:
objc_msgSend
,须要导入头文件#import <objc/message.h>
target --> Build Setting -->搜索msg -- 将enable strict checking of obc_msgSend calls由YES 改成NO
,将严厉的检查机制关掉,不然objc_msgSend
的参数会报错子类TCJTeacher
有实例方法sayHello
、sayNB
, 类方法sayNC
父类TCJPerson
有实例方法sayHello
、sayCode
, 类方法sayNA
消息接收者——实例对象
注意前面的细节:父类TCJPerson
中实现了sayHello
方法,而子类TCJTeacher
没有实现sayHello
方法.如今咱们能够尝试让teacher
调用sayHello
执行父类中实现,经过objc_msgSendSuper
实现.
由于objc_msgSend
不能向父类发送消息,须要使用objc_msgSendSuper
,并给objc_super
结构体赋值(在objc2
中只须要赋值receiver
、super_class
)
receiver——实例对象
;super_class——父类类对象
发现不管是
[teacher sayHello]
仍是objc_msgSendSuper
都执行的是父类中sayHello
的实现,因此这里,咱们能够做一个猜想:方法调用,首先是在类中查找,若是类中没有找到,会到类的父类中查找
.
receiver——实例对象
;super_class——父类类对象
receiver——类对象
;super_class——父类元类对象
消息查找流程实际上是经过上层的方法编号sel
发送消息objc_msgSend
找到具体实现imp
的过程
objc_msgSend
是用汇编写成的,至于为何不用C而
是用汇编写
,是由于:
C语言
不能经过写一个函数,保留未知的参数,跳转到任意的指针,而汇编有寄存器打开objc4
源码,因为主要研究arm64结构
的汇编实现,来到objc-msg-arm64.s
,先附上其汇编总体执行的流程图
p0表示0寄存器的指针,x0表示它的值,w0表示低32位的值(不用过多在乎)
objc_msgSend
消息接收者
是否为空,为空直接返回tagged_pointers
(以后会讲到)isa
存一份到p13
中isa
进行mask
地址偏移获得对应的上级对象
(类、元类)查看GetClassFromIsa_p16
定义,主要就是进行isa & mask
获得class
操做
imp
——开始了快速流程从CacheLookup
开始了快速查找流程(此时x1
是sel
,x16
是class
)
①经过cache
首地址平移16字节
(由于在objc_class
中,首地址距离cache
正好16
字节,即isa
首地址 占8
字节,superClass
占8
字节),获取cahce
,cache
中高16
位存mask
,低48
位存buckets
,即p11 = cache
②从cache
中分别取出buckets
和mask
,并由mask
根据哈希算法计算出哈希下标
cache
和掩码(即0x0000ffffffffffff)
的 &
运算,将高16位mask抹零
,获得buckets
指针地址,即p10 = buckets
cache
右移48
位,获得mask
,即p11 = mask
objc_msgSend
的参数p1
(即第二个参数_cmd)& msak
,经过哈希算法
,获得须要查找存储sel-imp
的bucket
下标index
,即p12 = index = _cmd & mask
,为何经过这种方式呢?由于在存储sel-imp
时,也是经过一样哈希算法计算哈希下标进行存储
,因此读取
也须要经过一样的方式读取
,以下所示③根据所得的哈希下标index
和 buckets
首地址,取出哈希下标对应的bucket
PTRSHIFT
等于3
,左移4位
(即2^4 = 16
字节)的目的是计算出一个bucket
实际占用的大小,结构体bucket_t
中sel
占8
字节,imp
占8
字节index
乘以 单个bucket占用的内存大小
,获得buckets
首地址在实际内存
中的偏移量
首地址 + 实际偏移量
,获取哈希下标index
对应的bucket
④根据获取的bucket,取出其中的imp存入p17,即p17 = imp,取出sel存入p9,即p9 = sel
⑤第一次递归循环
bucket
中sel
与 objc_msgSend
的第二个参数的_cmd
(即p1)是否相等CacheHit
,即缓存命中,返回imp
CheckMiss
,由于$0
是normal
,会跳转至__objc_msgSend_uncached
,即进入慢速查找流程
index
获取的bucket
等于 buckets
的第一个元素,则人为的将当前bucket
设置为buckets
的最后一个元素(经过buckets首地址+mask右移44位
(等同于左移4位)直接定位到bucker的最后一个元素
),而后继续进行递归循环(第一个
递归循环嵌套第二个
递归循环),即⑥bucket
不等于buckets
的第一个元素,则继续向前查找
,进入第一次递归循环
⑥第二次递归循环:重复⑤的操做,与⑤中惟一区别是,若是当前的bucket
仍是等于 buckets
的第一个元素,则直接跳转至JumpMiss
,此时的$0
是normal
,也是直接跳转至__objc_msgSend_uncached
,即进入慢速查找流程
如下是整个快速查找
过程值的变化
过程流程图
在快速查找流程中,若是没有找到方法实现,不管是走到CheckMiss
仍是JumpMiss
,最终都会走到__objc_msgSend_uncached
汇编函数
在objc-msg-arm64.s
文件中查找__objc_msgSend_uncached
的汇编实现,其中的核心是MethodTableLookup
(即查询方法列表),其源码以下
搜索MethodTableLookup
的汇编实现,其中的核心是_lookUpImpOrForward
,汇编源码实现以下
验证 上述汇编的过程,能够经过汇编调试
来验证
main
中,例如[person sayHello]
对象方法调用处加一个断点,而且开启汇编调试【Debug -- Debug worlflow -- 勾选Always show Disassembly】
,运行程序objc_msgSend
加一个断点,执行断住,按住control + stepinto
,进入objc_msgSend
的汇编_objc_msgSend_uncached
加一个断点,执行断住,按住control + stepinto
,进入汇编从上能够看出最后走到的就是lookUpImpOrForward
,此时并非汇编实现.
注意
C/C++
中调用 汇编
,去查找汇编时
,C/C++调用
的方法须要多加一个下划线
汇编
中调用 C/C++
方法时,去查找C/C++
方法,须要将汇编调用的方法去掉一个下划线
根据汇编部分的提示,全局续搜索lookUpImpOrForward
,最后在objc-runtime-new.mm
文件中找到了源码实现,这是一个c
实现的函数 其总体的慢速查找流程如图所示
慢速流程主要分为几个步骤:
cache
缓存中进行查找,即快速查找
,找到则直接返回imp
,反之,则进入②已知类
,若是不是,则报错
实现
,若是没有,则须要先实现,肯定其父类链,此时实例化的目的是为了肯定父类链、ro、以及rw等,方便后续数据的读取以及查找的循环初始化
,若是没有,则初始化for
循环,按照类继承链
或者 元类继承链
的顺序查找
cls
的方法列表中使用二分查找算法
查找方法,若是找到,则进入cache写入流程
(在iOS之武功秘籍⑤:cache_t分析文章中已经详述过),并返回imp
,若是没有找到,则返回nil
cls
被赋值为父类
,若是父类等于nil
,则imp = 消息转发
,并终止递归,进入④父类链
中存在循环
,则报错,终止循环
父类缓存
中查找方法
未找到
,则直接返回nil
,继续循环查找
找到
,则直接返回imp
,执行cache写入
流程判断
是否执行过动态方法解析
没有
,执行动态方法解析
执行过一次
动态方法解析,则走到消息转发流程
以上就是方法的慢速查找流程
,下面在分别详细解释二分查找原理
以及 父类缓存查找
详细步骤
查找方法列表的流程以下所示
其二分查找核心的源码实现以下
算法原理
简述为:从第一次查找开始,每次都取中间位置
,与想查找的key的value值
做比较,若是相等
,则须要排除分类方法
,而后将查询到的位置的方法实现返回,若是不相等
,则须要继续二分查找
,若是循环至count = 0
仍是没有找到,则直接返回nil
,以下所示:
以查找TCJPerson
类的sayHello
实例方法为例,其二分查找过程以下
cache_getImp
方法是经过汇编_cache_getImp
实现,传入的$0
是 GETIMP
,以下所示
父类缓存
中找到了方法实现,则跳转至CacheHit
即命中,则直接返回imp
父类缓存
中,没有找到
方法实现,则跳转至CheckMiss
或者 JumpMiss
,经过判断$0
跳转至LGetImpMiss
,直接返回nil
.总结
对象方法(即实例方法)
,即在类中查找
,其慢速查找的父类链
是:类--父类--根类--nil
类方法
,即在元类中查找
,其慢速查找的父类链
是:元类--根元类--根类--nil
快速查找、慢速查找
也没有找到方法实现,则尝试动态方法决议
动态方法决议
仍然没有找到,则进行消息转发
若是在快速查找、慢速查找、方法解析流程中,均没有找到实现,则使用消息转发,其流程以下
消息转发会实现
其中_objc_msgForward_impcache
是汇编实现,会跳转至__objc_msgForward
,其核心是__objc_forward_handler
汇编实现中查找__objc_forward_handler
,并无找到,在源码中去掉一个下划线
进行全局搜索_objc_forward_handler
,有以下实现,本质是调用的objc_defaultForwardHandler
方法
看着objc_defaultForwardHandler
有没有很眼熟,这就是咱们在平常开发中最多见的错误:没有实现函数,运行程序,崩溃时报的错误提示
.
🌰:定义TCJPerson
父类,其中有sayNB
实例方法 和 sayHappay
类方法
定义子类:TCJStudent
类,有实例方法sayHello
和sayMaster
,类方法sayObjc
,其中实例方法sayMaster
未实现.
在main
中 调用TCJStudend
的实例方法sayMaster
,运行程序报错,提示方法未实现,以下所示
下面,咱们来说讲如何在崩溃前,如何操做,能够防止方法未实现的崩溃.
在慢速查找
流程未找到
方法实现时,首先会尝试一次动态方法决议
,其源码实现以下: 主要分为如下几步
类是不是元类
类
,执行实例方法
的动态方法决议resolveInstanceMethod
元类
,执行类方法
的动态方法决议resolveClassMethod
,若是在元类中没有找到
或者为空
,则在元类
的实例方法
的动态方法决议resolveInstanceMethod
中查找,主要是由于类方法在元类中是实例方法
,因此还须要查找元类中实例方法的动态方法决议动态方法决议
中,将其实现指向了其余方法
,则继续查找指定的imp
,即继续慢速查找lookUpImpOrForward
流程其流程以下
针对实例方法
调用,在快速-慢速查找均没有找到实例方法的实现时,咱们有一次挽救的机会,即尝试一次动态方法决议
,因为是实例方法
,因此会走到resolveInstanceMethod
方法,其源码以下 主要分为如下几个步骤:
resolveInstanceMethod
消息前,须要查找cls
类中是否有该方法的实现,即经过lookUpImpOrNil
方法又会进入lookUpImpOrForward
慢速查找流程查找resolveInstanceMethod
方法
resolveInstanceMethod
消息lookUpImpOrNil
方法又会进入lookUpImpOrForward
慢速查找流程查找实例方法针对实例方法say666
未实现的报错崩溃,能够经过在类
中重写resolveInstanceMethod
类方法,并将其指向其余方法的实现,即在TCJPerson
中重写resolveInstanceMethod类方法
,将实例方法say666
的实现指向sayMaster
方法实现,以下所示
假如咱们在resolveInstanceMethod
类方法中,不指向其余方法的实现,它会来两次,为何会这样呢?咱们在后面在解释...
针对类方法
,与实例方法相似,一样能够经过重写resolveClassMethod
类方法来解决前文的崩溃问题,即在TCJPerson
类中重写该方法,并将sayNB
类方法的实现指向类方法sayHappy
resolveClassMethod
类方法的重写须要注意一点,传入的cls再也不是类
,而是元类
,能够经过objc_getMetaClass
方法获取类的元类,缘由是由于类方法在元类中是实例方法
.
上面的这种方式是单独在每一个类中重写,有没有更好的,一劳永逸的方法呢?其实经过方法慢速查找流程能够发现其查找路径有两条
它们的共同点是若是前面没找到,都会来到根类即NSObject中查找
,因此咱们是否能够将上述的两个方法统一整合在一块儿呢?答案是能够的,能够经过NSObject添加分类
的方式来实现统一处理
,并且因为类方法的查找,在其继承链,查找的也是实例方法,因此能够将实例方法 和 类方法的统一处理放在resolveInstanceMethod
方法中,以下所示 这种方式的实现,正好与源码中针对类方法的处理逻辑是一致的,即完美阐述为何调用了类方法动态方法决议,还要调用对象方法动态方法决议,其根本缘由仍是类方法在元类中是实例方法.
固然,上面这种写法仍是会有其余的问题,好比系统方法也会被更改
,针对这一点,是能够优化的,即咱们能够针对自定义类中方法统一方法名的前缀,根据前缀来判断是不是自定义方法,而后统一处理自定义方法
,例如能够在崩溃前pop到首页,主要是用于app线上防崩溃的处理,提高用户的体验.
实例方法
能够重写resolveInstanceMethod
添加imp
类方法
能够在本类重写resolveClassMethod
往元类
添加imp
,或者在NSObject分类
重写resolveInstanceMethod
添加imp
动态方法解析
只要在任意一步lookUpImpOrNil
查找到imp
就不会查找下去——即本类
作了动态方法决议,不会走到NSObjct分类
的动态方法决议NSObject分类
重写resolveInstanceMethod
添加imp
解决崩溃那么把全部崩溃都在NSObjct分类
中处理,加之前缀区分业务逻辑,岂不是美滋滋?错!
NSObjct分类
动态方法决议以前已经作了处理SDK
封装的时候须要给一个容错空间所以前面的 ④ 优化方案
也不是一个最完美的解决方案.那么,这也不行,那也不行,那该怎么办?放心,苹果爸爸已经给咱们准备好后路了!
在慢速查找的流程(lookUpImpOrForward
)中,咱们了解到,若是快速+慢速没有找到方法实现,动态方法决议也不行,就使用消息转发
,可是,咱们找遍了源码也没有发现消息转发的相关源码,能够经过如下方式来了解,方法调用崩溃前都走了哪些方法
instrumentObjcMessageSends
方式打印发送消息的日志经过lookUpImpOrForward --> log_and_fill_cache --> logMessageSend
,在logMessageSend
源码下方找到instrumentObjcMessageSends
的源码实现,因此,在main
中调用instrumentObjcMessageSends
打印方法调用的日志信息,有如下两点准备工做
一、打开 objcMsgLogEnabled
开关,即调用instrumentObjcMessageSends
方法时,传入YES
二、在main
中经过extern
声明instrumentObjcMessageSends
方法
经过logMessageSend
源码,了解到消息发送打印信息存储在/tmp/msgSends
目录,以下所示
运行代码,并前往/tmp/msgSends
目录,发现有msgSends
开头的日志文件,打开发如今崩溃前,执行了如下方法
resolveInstanceMethod
方法forwardingTargetForSelector
方法methodSignatureForSelector + resolveInvocation
forwardingTargetForSelector
在源码中只有一个声明,并无其它描述,好在帮助文档中提到了关于它的解释:
sel
的新对象,也就是本身处理不了会将消息转发给别的对象进行相关方法的处理,可是不能返回self
,不然会一直找不到forwardInvocation:
方法进行处理objc_msgSend(forwardingTarget, sel, ...);
来实现消息的发送以下代码就是经过快速转发解决崩溃——即TCJPerson
实现不了的方法,转发给TCJStudent
去实现(转发给已经实现该方法的对象)
也能够直接不指定消息接收者,直接调用父类的该方法
,若是仍是没有找到,则直接报错
在快速转发流程找不到转发的对象后,会来到慢速转发流程methodSignatureForSelector
依葫芦画瓢,在帮助文档中找到methodSignatureForSelector
点击查看
forwardInvocation
forwardInvocation
和methodSignatureForSelector
必须是同时存在的,底层会经过方法签名,生成一个NSInvocation
,将其做为参数传递调用NSInvocation
中编码的消息的对象(对于全部消息,此对象没必要相同)anInvocation
将消息发送到该对象.anInvocation
将保存结果,运行时系统将提取结果并将其传递给原始发送者慢速转发流程
就是先methodSignatureForSelector
提供一个方法签名,而后forwardInvocation
经过对NSInvocation
来实现消息的转发
其实也能够对forwardInvocation
方法中的invocation
不进行处理,也不会崩溃报错
因此,由上述可知,不管在forwardInvocation
方法中是否处理invocation
事务,程序都不会崩溃.
Hopper和IDA是一个能够帮助咱们静态分析可视性文件的工具,能够将可执行文件反汇编成伪代码、控制流程图等,下面以Hopper为例.
运行程序崩溃,查看堆栈信息
发现___forwarding___
来自CoreFoundation
经过image list
,读取整个镜像文件,而后搜索CoreFoundation
,查看其可执行文件的路径
经过文件路径,找到CoreFoundation
的可执行文件
打开hopper
,选择Try the Demo
,而后将上一步的可执行文件拖入hopper
进行反汇编,选择x86(64 bits)
如下是反汇编后的界面,主要使用上面的三个功能,分别是 汇编、流程图、伪代码
经过左侧的搜索框搜索__forwarding_prep_0___
,而后选择伪代码
如下是__forwarding_prep_0___
的汇编伪代码,跳转至___forwarding___
如下是___forwarding___
的伪代码实现,首先是查看是否实现forwardingTargetForSelector
方法,若是没有响应,跳转至loc_6459b
即快速转发没有响应,进入慢速转发流程
跳转至loc_6459b
,在其下方判断是否响应methodSignatureForSelector
方法
若是没有响应,跳转至loc_6490b
,则直接报错
若是获取methodSignatureForSelector
的方法签名为nil
,也是直接报错
若是methodSignatureForSelector
返回值不为空,则在forwardInvocation
方法中对invocation
进行处理
经过上面两种查找方式能够验证,消息转发的方法有3个
消息转发总体的流程以下
消息转发的处理主要分为两部分:
forwardingTargetForSelector
方法
消息接收者
,在消息接收者中仍是没有找到方法实现,则进入另外一个方法的查找流程methodSignatureForSelector
方法
方法签名
为nil
,则直接崩溃报错
方法签名
不为nil
,走到forwardInvocation
方法中,对invocation
事务进行处理,若是不处理也不会报错
在前文中说起了动态方法决议方法执行了两次,有如下两种分析方式
在慢速查找流程中,咱们了解到resolveInstanceMethod
方法的执行是经过lookUpImpOrForward --> resolveMethod_locked --> resolveInstanceMethod
来到resolveInstanceMethod
源码,在源码中经过发送resolve_sel
消息触发,以下所示 因此能够在
resolveInstanceMethod
方法中IMP imp = lookUpImpOrNil(inst, sel, cls);
处加一个断点,经过bt
打印堆栈信息
来看到底发生了什么
在resolveInstanceMethod
方法中IMP imp = lookUpImpOrNil(inst, sel, cls);
处加一个断点,运行程序,直到第一次“来了”,经过bt
查看第一次动态方法决议
的堆栈信息,此时的sel
是say666
继续往下执行,直到第二次“来了”打印,查看堆栈信息,在第二次中,咱们能够看到是经过CoreFoundation
的-[NSObject(NSObject) methodSignatureForSelector:]
方法,而后经过class_getInstanceMethod
再次进入动态方法决议
经过上一步的堆栈信息,咱们须要去看看CoreFoundation
中到底作了什么?经过Hopper
反汇编CoreFoundation
的可执行文件,查看methodSignatureForSelector
方法的伪代码
经过methodSignatureForSelector
伪代码进入___methodDescriptionForSelector
的实现
进入 ___methodDescriptionForSelector
的伪代码实现,结合汇编的堆栈打印,能够看到,在___methodDescriptionForSelector
这个方法中调用了objc4源码
的class_getInstanceMethod
在objc4-818.2
源码中搜索class_getInstanceMethod
,其源码实现以下所示
这一点能够经过代码调试来验证,以下所示,在class_getInstanceMethod
方法处加一个断点,在执行了methodSignatureForSelector
方法后,返回了签名,说明方法签名是生效的,苹果在走到invocation
以前,给了开发者一次机会再去查询,因此走到class_getInstanceMethod
这里,又去走了一遍方法查询say666
,而后会再次走到动态方法决议
因此,上述的分析也印证了前文中resolveInstanceMethod
方法执行了两次的缘由
若是在没有上帝视角的状况下,咱们也能够经过代码
来推导在哪里再次调用了动态方法决议
TCJPerson
类中重写resolveInstanceMethod
方法,并加上class_addMethod
操做即赋值IMP
,此时resolveInstanceMethod
会走两次吗?经过运行发现,若是赋值了IMP,动态方法决议只会走一次
,说明不是在这里走第二次动态方法决议
继续往下探索
resolveInstanceMethod
方法中的赋值IMP
,在TCJPerson
类中重写forwardingTargetForSelector
方法,并指定返回值为[TCJStudent alloc]
,从新运行,若是resolveInstanceMethod
打印了两次,说明是在forwardingTargetForSelector
方法以前执行了动态方法决议,反之,在forwardingTargetForSelector
方法以后结果发现resolveInstanceMethod
中的打印仍是只打印了一次,那说明第二次动态方法决议 在forwardingTargetForSelector
方法后
TCJPerson
类中重写 methodSignatureForSelector
和 forwardInvocation
,运行结果发现第二次动态方法决议在 methodSignatureForSelector
和 forwardInvocation
方法之间.
第二种分析一样能够论证前文中resolveInstanceMethod
执行了两次的缘由. 通过上面的论证,咱们了解到其实在慢速消息转发流程中,在methodSignatureForSelector
和 forwardInvocation
方法之间还有一次动态方法决议,即苹果再次给的一个机会,以下图所示
到目前为止,objc_msgSend
发送消息的流程就分析完成了,在这里简单总结下
【快速查找流程】
首先,在类的缓存cache
中查找指定方法的实现【慢速查找流程
】若是缓存中没有找到,则在类的方法列表
中查找,若是仍是没找到,则去父类链的缓存和方法列表
中查找【动态方法决议】
若是慢速查找仍是没有找到时,第一次补救机会
就是尝试一次动态方法决议
,即重写resolveInstanceMethod/resolveClassMethod
方法【消息转发】
若是动态方法决议仍是没有找到,则进行消息转发
,消息转发中有两次补救机会:快速转发+慢速转发
unrecognized selector sent to instance
最后,和谐学习,不急不躁.我仍是我,颜色不同的烟火.