runtime 基础知识
runtime
是运行时,在运行的时候做一些事请,可以动态添加类和交换函数,那么有一个基础知识需要了解,arm64架构前,isa指针是普通指针,存储class和meta-class对象的内存地址,从arm64架构开始,对isa进行了优化,变成了一个union
共用体,还是用位域来存储更多的信息,我们首先看一下isa指针的结构:
1 | struct objc_object { |
objc_object
是结构体,包含了私有属性isa_t
,isa_t isa
是一个共用体,包含了ISA_BITFIELD
是一个宏(结构体),bits
是uintptr_t
类型,uintptr_t
其实是unsign long
类型占用8字节,就是64位,我们进入到ISA_BITFIELD
内部:
1 | # if __arm64__ |
ISA_BITFIELD
在arm64
和x86
是两种结构,存储了nonpointer
,has_assoc
,has_cxx_dtor
,shiftcls
,magic
,weakly_referenced
,deallocating
,has_sidetable_rc
,extra_rc
这些信息,:1
就占用了一位,:44
就是占用了44位,:6
就是占用了6位,:8
就是占用了8位,那么共用体isa_t
简化之后
1 | union isa_t { |
isa_t
是使用共用体结构,使用bits
存储了结构体的数据,那么共用体是如何使用的?我们来探究一下
共用体基础知识
首先我们定义一个FYPerson
,添加2个属性
1 | @interface FYPerson : NSObject |
然后查看该类的实例占用空间大小
1 | FYPerson *p=[[FYPerson alloc]init]; |
FYPerson
定义了三个属性,占用空间是16字节,那么我们换一种方法实现这个三个属性的功能。
我们定义6个方法,3个set方法,3个get方法。
1 | - (void)setTall:(BOOL)tall; |
我们定义了一个char类型的变量_richTellHandsome
,4字节,32位,可以存储32个bool类型的变量。赋值是使用_richTellHandsome = _richTellHandsome|FYRichMask
,或_richTellHandsome = _richTellHandsome&~FYRichMask
,取值是!!(_richTellHandsome&FYRichMask)
,前边加!!
是转化成bool
类型的,否则取值出来是1 or 2 or 4
。我们再换一种思路将三个变量定义成一个结构体,取值和赋值都是可以直接操作的。
1 | @interface FYPerson() |
结构体_richTellHandsome
包含三个变量char tall : 1;
,char rich : 1;
,char handsome : 1
。每一个变量占用空间为1位,3个变量占用3位。取值的时候使用!!(_richTellHandsome&FYHandsomeMask)
,赋值使用
1 | if (tall) { |
我们采用位域来存储信息,
位域是指信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几 个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。
另外一个省空间的思路是使用联合
,
使用union
,可以更省空间,“联合”是一种特殊的类,也是一种构造类型的数据结构。在一个“联合”内可以定义多种不同的数据类型, 一个被说明为该“联合”类型的变量中,允许装入该“联合”所定义的任何一种数据,这些数据共享同一段内存,以达到节省空间的目的(还有一个节省空间的类型:位域)。 这是一个非常特殊的地方,也是联合的特征。另外,同struct一样,联合默认访问权限也是公有的,并且,也具有成员函数。
1 | @interface FYPerson() |
使用联合
共用体,达到省空间的目的,runtime
源码中是用来很多union
和位运算。
例如KVO 的NSKeyValueObservingOptions1
2
3
4
5
6typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions){
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
NSKeyValueObservingOptionInitial = 0x04,
NSKeyValueObservingOptionPrior = 0x08
}
这个NSKeyValueObservingOptions
使用位域,当传进去的时候NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
,则传进去的值为0x3
,转化成二进制就是0b11
,则两位都是1
可以包含2个值。
那么我们来设计一个简单的可以使用或来传值的枚举
1 | typedef enum { |
这是一个名字为FYOptions
的枚举,第一个是十进制是1,二进制是0b 0001
,第二个十进制是2,二进制是0b 0010
,第三个十进制是4,二进制是0b 0100
,第四个十进制是8,二进制是0b 1000
。
那么我们使用的时候可以FYOne|FYTwo|FYTHree
,打包成一个值,相当于1|2|4 = 7
,二进制表示是0b0111
,后三位都是1,可以通过&mask取出对应的每一位的数值。
Class的结构
isa详解 – 位域存储的数据及其含义
参数 | 含义 |
---|---|
nonpointer | 0->代表普通的指针,存储着Class、Meta-Class对象的内存地址。1->代表优化过,使用位域存储更多的信息 |
has_assoc | 是否有设置过关联对象,如果没有,释放时会更快 |
has_cxx_dtor | 是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快 |
shiftcls | 存储着Class、Meta-Class对象的内存地址信息 |
magic | 用于在调试时分辨对象是否未完成初始化 |
weakly_referenced | 是否有被弱引用指向过,如果没有,释放时会更快 |
deallocating | 对象是否正在释放 |
extra_rc | 里面存储的值是引用计数器减1 |
has_sidetable_rc | 引用计数器是否过大无法存储在isa中 |
如果为1,那么引用计数会存储在一个叫SideTable的类的属性中|
class结构
1 | struct fy_objc_class : xx_objc_object { |
class_ro_t
是只读的,class_rw_t
是读写的,在源码中runtime
->Source
->objc-runtime-new.mm
->static Class realizeClass(Class cls) 1869行
1 |
|
开始cls->data
指向的是ro
,初始化之后,指向的rw
,rw->ro
指向的是原来的ro
。class_rw_t
中的method_array_t
是存储的方法列表,我们进入到method_array_t
看下它的数据结构:
1 | class method_array_t : |
method_array_t
是一个类,存储了method_t
二维数组,那么我们看下method_t
的结构
1 | struct method_t { |
method_t
是存储了3个变量的结构体,SEL
是方法名,types
是编码(方法返回类型,参数类型), imp
函数指针(函数地址)。
SEL
- SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似
- 可以通过@selector()和sel_registerName()获得
- 可以通过sel_getName()和NSStringFromSelector()转成字符串
- 不同类中相同名字的方法,所对应的方法选择器是相同的
Type Encoding
iOS中提供了一个叫做@encode的指令,可以将具体的类型转成字符编码,官方网站插件encodeing
code | Meaning |
---|---|
c | A char |
i | An int |
s | A short |
l | A long |
l | is treated as a 32-bit quantity on 64-bit programs. |
q | A long long |
C | An unsigned char |
I | An unsigned int |
S | An unsigned short |
L | An unsigned long |
Q | An unsigned long long |
f | A float |
d | A double |
B | A C++ bool or a C99 _Bool |
v | A void |
* | A character string (char *) |
@ | An object (whether statically typed or typed id) |
# | A class object (Class) |
: | A method selector (SEL) |
[array type] | An array |
{name=type…} | A structure |
(name=type…) | A union |
bnum | A bit field of num bits |
^type | A pointer to type |
? | An unknown type (among other things, this code is used for function pointers) |
我们通过一个例子来了解encode
1 | -(void)test:(int)age heiht:(float)height{ |
v24@0:8i16f20
是encoding的值,我们来分解一下,前边是v24
是函数返回值是void
,所有参数占用了24
字节,@0:8
是从第0开始,长度是8字节的位置,i16
是从16字节开始的int
类型,f20
是从20字节开始,类型是float
。
方法缓存
Class内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度。
我们来到cache_t
内部
1 | struct cache_t { |
散列表的数据结构表格所示
索引 | bucket_t |
---|---|
0 | bucket_t(_key,_imp) |
1 | bucket_t(_key,_imp) |
2 | bucket_t(_key,_imp) |
3 | bucket_t(_key,_imp) |
4 | bucket_t(_key,_imp) |
… | … |
通过cache_getImp(cls, sel)
获取IMP
。具体在cache_t::find
函数中
1 | bucket_t * cache_t::find(cache_key_t k, id receiver) |
首先获取buckets()
获取butket_t
,然后获取_mask
,通过cache_hash(k, m)
获取第一次访问的索引i
,cache_hash
通过(mask_t)(key & mask)
得出具体的索引
,当第一次成功获取到butket_t
则直接返回,否则执行cache_next(i, m)
获取下一个索引,直到获取到或者循环一遍结束。
那么我们来验证一下已经执行的函数的确是存在cache中的,我们自定义了class_rw_t
1 | #import <Foundation/Foundation.h> |
测试代码是
1 | FYPerson *p = [[FYPerson alloc]init]; |
可以看出来IMP1
和IMP2
、key1
和key2
分别对应了bucket_t
中的key2
,key3
和imp2
和imp3
。
1 | static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) |
cache_t
初始化是大小是4,当大于3/4时,进行扩容,扩容之后是之前的2倍,数据被清空,cacha->_occupied
恢复为0。
验证代码如下:
1 | FYPerson *p = [[FYPerson alloc]init]; |
总结
- arm64之后isa使用联合体用更少的空间存储更多的数据,arm64之前存储class和meta-class指针。
- 函数执行会先从cache中查找,没有的话,当再次找到该函数会添加到cache中
- 从
class->cache
查找bucket_t
的key需要先&_mask
之后再判断是否有该key
- cache扩容在大于3/4进行2倍扩容,扩容之后,旧数据删除,
imp
个数清空 class->rw
在初始化中讲class_ro_t
值赋值给rw
,然后rw->ro
指向之前的ro
。
资料下载
- 学习资料下载
- demo code
runtime可运行的源码
最怕一生碌碌无为,还安慰自己平凡可贵。
广告时间