2.iOS开发之Runtime

2020-03-06  本文已影响0人  苹果我咬了一口

前言:做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进行)

可以使用第三方的JRSwizzleRSSwizzleRSSwizzle更安全防爆还支持block

2.动态添加方法(class_addMethod)

3.给分类添加属性 (关联对象)

4.字典转模型(遍历成员变量class_copyIvarList)

5.自动归档解档(遍历成员变量class_copyIvarList)

6.动态变量控制(遍历后找到对应的变量进行改值)

7.万能控制器跳转(获取推送后的一些跳转)


部分内容转载自 深入浅出 Runtime 、 Runtime 10种用法 与 runtime源码

上一篇下一篇

猜你喜欢

热点阅读