iOS之Runtime
Runtime是什么
Runtime是一个库,这个库使我们可以在程序运行时创建对象、检查对象,修改类和对象的方法。
- OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行;
- Runtime是一套C语言的API,封装了很多动态性相关的函数;
- OC的动态性就是由Runtime API来支撑和实现的
基础知识
- 在
arm64
架构之前,isa
就是一个普通的指针,存储着Class
、Meta-Class
对象的内存地址; - 从arm64架构开始,对
isa
进行了优化,变成了一个共用体(union
)结构,还使用位域来存储更多的信息; - 类对象、元类对象的地址值(二进制)最后三位一定是0,所以16进制的地址值末位总是0或者8;
isa
的位域信息:
isa共用体的64位数中有33位存储的是类/元类的内存地址信息
iOS中对象的底层结构:
类Class的结构
类和元类本质上都是Class,Class是一个objc_class类型的结构体指针:
1 | typedef struct objc_class *Class; |
class_rw_t
的结构
1 | struct class_rw_t { |
可读可写:可以动态地给对象添加协议、方法、属性
class_rw_t
里面的methods
、properties
、protocols
是二维数组,包含了类的初始内容和category
的内容。
class_ro_t的结构
包含了类的初始内容:成员变量、属性、方法、协议
baseMethodList
、baseProtocols
、ivars
、baseProperties
是一维数组只读的:类注册完成后,无法动态地添加成员变量
method_t的结构
method_t
是对方法/函数的封装
1 | struct method_t { |
IMP:代表数据的具体实现
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
SEL:方法/函数名,一般叫做选择器,底层结构跟
char *
类似typedef struct objc_selector *SEL;
获取SEL:
@selector()
或sel_registerName()
获取SEL的字符串格式:
NSStringFromSelector()
或者sel_getName()
不同类中相同名字的方法,所对应方法选择器是相同的
types:包含了函数的返回值、参数编码的字符串
比如
- (int)age:(int)age height:(float)height
函数的types是
"i 24 @ 0 : 8 i 16 f 20"
i
:返回值类型”int”;24
:所有参数占的字节数;@
:第一个参数,”id”类型;指针,8个字节;:
:第二个参数: “SEL”; 指针,8个字节;i
:第三个参数,”int”;4f
:第四个参数, “float”;4“0”、”8”、”16”、”20” : 参数从多少字节开始
方法缓存cache_t
Class内部结构中有个方法缓存cache_t,是用散列表来缓存曾经调用过的方法,可以提高方法的查找速度
缓存方法过程:
- 通过方法名 & 当前的
_mask
计算出下标 - 将方法存放到下标位置
- 如果当前位置已经存储了别的方法,那么将下标-1,向上存储
- 如果第一个位置也存储了,就从最后一个位置继续
- 如果全部都满了,就会重新分配内存,重新进行缓存
如果缓存列表满了,会清空缓存列表,扩容内存为原来的两倍,等待下次调用方法的时候重新进行缓存,因为 _mask
存放的数量 已经改变了,进行 &运算的时候,肯定是和之前的值不一样,所以需要清空,重新缓存
SideTable
1 | // NSObject.mm |
SideTable
存储在SideTables()
中,SideTables()
本质也是一个散列表,可以通过对象指针来获取它对应的(引用计数表或者弱引用表)在哪一个SideTable
中。在非嵌入式系统下,SideTables()
中有 64 个SideTable
。
SideTables()
的定义:
1 | // NSObject.mm |
查找对象的引用计数表需要经过两次哈希查找:
① 第一次根据当前对象的内存地址,经过哈希查找从SideTables()
中取出它所在的SideTable
;
② 第二次根据当前对象的内存地址,经过哈希查找从SideTable
中的refcnts
中取出它的引用计数表。
为什么不是一个SideTable,而是使用多个SideTable组成SideTables()结构?
如果只有一个SideTable
,那我们在内存中分配的所有对象的引用计数或者弱引用都放在这个SideTable
中,那我们对对象的引用计数进行操作时,为了多线程安全就要加锁,就存在效率问题。 系统为了解决这个问题,就引入 “分离锁” 技术方案,提高访问效率。把对象的引用计数表分拆多个部分,对每个部分分别加锁,那么当所属不同部分的对象进行引用操作的时候,在多线程下就可以并发操作。所以,使用多个SideTable
组成SideTables()
结构。
[super message]
的本质
底层汇编实现——
objc_msgSendSuper(struct objc_super, SEL)
函数:该方法第一个参数是个结构体,第二个参数是方法名;
结构体中第一个成员表示receiver
,第二个成员是父类对象,用来表示从父类开始查找message()
方法的实现;1
2
3
4
5
6
7struct objc_super {
id receiver;
Class super_class; // receiver的父类对象
}
// Student: Person: NSObject
objc_msgSendSuper({self, [Person class]}, @selelctor(message));运行时实现——
objc_msgSendSuper2(struct objc_super2, SEL)
函数:receiver
是消息接收者,current_class
是receiver
的类对象。objc_msgSendSuper2
函数是通过传入的类对象的superclass
指针找到当前类的父类,并从父类开始查找方法的实现的。1
2
3
4
5
6struct objc_super2 {
id receiver;
Class current_class; // 当前类的类对象
};
objc_msgSendSuper({self, [Student class]}, @selelctor(message));
isKindOfClass和isMemberOfClass
作为实例方法:**
[obj isMemberOfClass: [XXX class]]
:判断obj
是否是XXX
的实例对象[obj isKindOfClass: [XXX class]]
:判断obj是否是XXX或其子类的实例对象作为类方法:**
isKindOfClass
或isMemberOfClass
后面应该传元类对象,结果才是YES:1
2
3
4
5
6
7// ZZPerson: NSObject
// NO, ZZPerson是ZZPerson的元类对象的实例
[ZZPerson isMemeberOfClass: [ZZperson class]];
// NO, ZZPerson是ZZPerson的元类对象NSObject的元类也就是NSObject的实例
[ZZPerson isKindOfClass: [ZZperson class]];
// YES,OC中任意一个类最终都继承自NSObject,NSObject的元类对象仍是NSObject
[ZZPerson isKindOfClass: [NSObject class]];
消息发送机制
OC中的方法调用,都是转换为objc_msgSend(receiver, SEL) objc_msgSend的执行流程分为三大阶段:消息发送——动态方法解析——消息转发。
消息发送阶段
动态方法解析
消息转发
Runtime的应用
- 修改私有属性
- 字典转模型
- 自动归档解档
- 交换方法实现
- 利用关联对象给分类添加属性
- 利用消息转发机制解决方法找不到的异常问题
- 热修复(JSPatch实现)