前言:做iOS开发有些时间了,日常开发上架都熟练于心了,然而连一些最基本的相关原理知识有时候都说不上来,想着有空的时候整理整理,写下来记录一下。可能了解的不够全面或者不够准确,如果有看到的朋友希望能不吝赐教。
Runtime是什么?
首先了解OC是一门动态语言,与在编译器就已经决定了各种数据结构的静态语言不同,动态语言可以在运行期动态的修改一个类的结构,比如修改方法实现,绑定实例变量等。
Runtime是一套比较底层的基于C和汇编的API,是OC运行时框架的基石。编写的OC代码在运行过程都会转换成Runtime的C语言代码。OC需要Runtime来创建类和对象,进行消息发送和转发。
与runtime进行交互的三种方式对象(object)和类(class)
OC的面向对象都是基于C/C++的数据结构实现的。
所有的对象都是由其对应的类实例化而来,OC对象本质是objc_object结构体。
objc_object结构体OC的类本质上也是对象。
objc_class结构体
isa指针
对象都有isa指针,isa指针用来维护对象和类之间的关系,确保对象和类能够通过isa指针找到对应的方法、实例变量、属性、协议等。实例对象的方法存储于类中,而类方法则以实例方法存在存于元类中,类是元类的实例对象。
在arm64架构之前,isa就是单纯的指针。直接指向objc_class,存储着Class、Meta-Class对象的内存地址。实例指向类,类指向元类,元类指向根元类,根元类指向自身,根元类的父类是NSObject。
在arm64架构之后,对isa进行了优化,变成了一个共用体(union)结构,使用位域来存储更多的信息。64位等于8个字节,其中33位才用来存Class、Meta-Class对象的内存地址。(NON_POINTER_ISA)
isa经典流程图(图片转自网络)
关于class_ro_t与class_rw_t
class_data_bits_t主要是对class_rw_t的封装,可以通过bits & FAST_DATA_MASK获得class_rw_t。
在编译期,类的相关方法,属性,协议会被添加到class_ro_t这个只读的结构体中,class_ro_t中的list都是一维数组(例method_list_t)。【ivar_list_t】
在运行期,类第一次被调用的时候,class_rw_t会被初始化,编译期确定下来的信息会被拷贝进去,category中的内容也是在这个时候被添加进来的,还有其他运行时添加的信息。class_r_t中的list则是可读可写的二维数组(例method_array_t)。
class_rw_t与class_ro_t中的methodList(图片转自网络)获得 class的方式
获得 class的方式isKindOfClass和isMemberOfClass的区别
isKindOfClass不但会判断接受者是否为该Class的实例,还会判断接受者是否为该Class的任何继承类的实例,而isMemberOfClass只会判断接受者是否为该Class的实例。
消息发送与转发
OC中方法的本质就是向对象发送消息 。
objc_msgSend
除了objc_msgSend,还有objc_msgSendSuper(当方法调用者为super时)以及objc_msgSend_stret(当数据结构作为返回值时)。
objc_msgSend的参数 1是消息接受者,参数2是SEL方法名,之后参数为SEL方法的参数。
方法查找
通过SEL找IMP
消息发送的流程(图片转自网络) 消息发送的流程(图片转自网络)cache_t
向对象发送消息后通过isa指针找到对象的class,去class的cache方法缓存中查找方法,找到就调用。
cache_t的结构cache_t是可增量扩展的哈希表结构,用哈希表来缓存曾经使用过的方法,可以提高方法的查找速度(空间换时间:牺牲内存空间来换取执行效率)。子类没有实现方法会调用父类的方法,会将父类方法加入到子类自己的cache里。
缓存容量如果不够会设置新的缓存bucket_t,容量是旧的两倍,并且扩展的时候,会清空数组里原有的缓存内容。bucket_t中的存的是SEL与IMP的键值对。
cache_t 中没有找到则去class的class_rw_t以及父类继续查找,若找到则加入class的cache方法缓存中。 查找方法getMethodNoSuper_nolock(cls, sel),如果方法列表是经过排序的,则进行二分法查找,没有经过排序,则进行线性遍历查找。
cache_t详情可以参考 方法缓存cache_t 探究
动态方法解析
动态方法解析流程(图片转自网络)
根据实例方法或类方法重写+(BOOL)resolveInstanceMethod:(SEL)sel或 +(BOOL)resolveClassMethod:(SEL)sel这两个方法,在方法中调用class_addMethod(Class cls, SEL name, IMP imp, const char *types)方法实现动态方法解析。标记为已经动态解析后会再次进入“消息发送”流程。
注:在NSObject重写resolveInstanceMethod方法,都可以动态解析实例方法与类方法。
消息转发
消息转发流程(图片转自网络)消息转发”阶段分两步进行:Fast forwarding 和 Normal forwarding。
Fast forwarding:将消息转发给一个其它 OC 对象(找一个备用接收者),我们可以重写以下方法+/- (id)forwardingTargetForSelector:(SEL)sel,返回一个!= receiver的对象,来完成这一步骤。
Normal forwarding:实现一个完整的消息转发过程。
(1)重写 +/- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 返回一个签名(方法签名就是对返回值类型、参数类型的描述,可以使用 Type Encodings 编码)。Runtime 会根据这个方法签名,创建一个NSInvocation对象(NSInvocation封装了未知消息的全部内容,包括:方法调用者 target、方法名 selector、方法参数 argument 等),然后调用第二个方法并将该NSInvocation对象作为参数传入。
(2)重写 +/- (void)forwardInvocation:(NSInvocation *)invocation 可以做以下事
1.将未知消息转发给其它对象;
2. 改变未知消息的内容(如方法名、方法参数)再转发给其它对象
3.可以定义任何逻辑。
如果(1)中没有返回方法签名,或者我们没有重写(2),系统就会认为我们彻底不想处理这个消息了,这时候就会调用+/- (void)doesNotRecognizeSelector:(SEL)sel方法并抛出经典的 crash:unrecognized selector sent to instance/class,结束 objc_msgSend 的全部流程。
self 和 super
OC 方法都带有两个隐式参数:(id)self和(SEL)_cmd;
self 是一个对象指针,指向当前方法的调用者/消息接收者;
如果是实例方法,它就是指向当前类的实例对象;
如果是类方法,它就是指向当前类的类对象。
当使用 self 调用方法的时候,底层会转换为objc_msgSend()函数的调用,通过上一篇文章可以知道,该函数会从当前消息接收者类中开始查找方法的实现。
super 是一个编译器指令,当使用 super 调用方法的时候,底层会转换为objc_msgSendSuper2()函数的调用,该函数会从当前消息接受者类的父类中开始查找方法的实现。要注意消息接收者还是子类对象,而不是父类对象,只是查找方法实现的范围变了。
Runtime的日常应用
1.方法交换(Swizzle黑魔法method_exchangeImplementations)
(拦截替换或者额外增加功能 通常在 +(void)load进行)
可以使用第三方的JRSwizzle与RSSwizzle,RSSwizzle更安全防爆还支持block
2.动态添加方法(class_addMethod)
3.给分类添加属性 (关联对象)
4.字典转模型(遍历成员变量class_copyIvarList)
5.自动归档解档(遍历成员变量class_copyIvarList)
6.动态变量控制(遍历后找到对应的变量进行改值)
7.万能控制器跳转(获取推送后的一些跳转)
部分内容转载自 深入浅出 Runtime 、 Runtime 10种用法 与 runtime源码
网友评论