前言
在面试或者平常的工作中,我们总会听到或者看到 runtime 。 但是 runtime 是什么呢?他能干什么呢?我们就带着疑问进入今天的内容。
OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行,OC的动态性就是由Runtime来支撑和实现的,所以我们经常会听到说OC是动态语言,是C语言的超集。下面这张图比较生动形象表明C 和OC的关系。
在平常的开发中,我们也在不经意之间使用了 runtime 比如 关联对象,分类,Method Swizzling,block,消息转发机制 等等,可见runtime 对于 OC 非常重要。
isa
在OC中一切皆为对象(这里不是你们想的对象那种意思),在iOS中,OC对象是通过 isa 指针来指向一个元类或者类对象。那我们不得不说一下 NSObject 。
首先,关于NSObject,objc_class 和 objc_object 三者之间的关系,我们可以用下面的图来更清晰的了解:
在arm64之前,实例对象的isa指向类对象,类对象的isa指向元类对象。一个比较经典的类,元类,根类的关系图。
在arm64之后,isa经过了优化,采取了共用体的结构,将一个64位的内存数据分开存储了很多的信息,其中的33位才是存储类对象、元类对象的地址值的(系统不一样可能对应位可能存在不同 __arm64__(对应ios 移动端) 和 __x86_64__(对应macOS)),可以通过一个位运算取出instance的isa包含的class的地址,取出class的isa包含的meta-class的地址。
isa 提供了两个成员,cls 和 bits,由联合体的定义所知,这两个成员是互斥的,也就意味着,当初始化isa指针时,有两种初始化方式
通过cls初始化,bits无默认值
通过bits初始化,cls有默认值
union isa_t {
Class cls;
#if defined(ISA_BITFIELD)
struct {
uintptr_t nonpointer : 1; //0,代表普通的指针,存储着Class、Meta-Class对象的内存地址,1,代表优化过,使用位域存储更多的信息
uintptr_t has_assoc : 1; //是否有设置过关联对象,如果没有,释放时会更快 当我们设置了关联对象这个值会被设置成1
uintptr_t has_cxx_dtor : 1; //是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
uintptr_t shiftcls : 33; //存储着Class、Meta-Class对象的内存地址信息
uintptr_t magic : 6; //用于调试器判断当前对象是真的对象 还是 没有初始化的空间,占6位
uintptr_t weakly_referenced : 1; //是否有被弱引用指向过,如果没有,释放时会更快
uintptr_t deallocating : 1; //对象是否正在释放
uintptr_t has_sidetable_rc : 1; //里面存储的值是引用计数器减1
uintptr_t extra_rc : 19 //引用计数器是否过大无法存储在isa中,如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
};
#endif
};
生成isa 过程
通过alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone方法路径,查找到initInstanceIsa,在这里生成isa。(后续在讲alloc的时候我们会对这个再讲解一下)
什么是位域
对于一些初学者我们可能看不太懂上面的内容含义(可能需要补充一些C语言的只是了),这里我们做一个简单的讲解。 这里的
uintptr_t nonpointer : 1;
这里的1表示位域中位的数量
有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有 0 和 1 两种状态,用 1 位二进位即可。为了节省存储空间,并使处理简便,C 语言又提供了一种数据结构,称为 “位域”。
:后的数字用来限定成员变量占用的位数,带有与定义宽度的变量称为位域。
struct bs {
unsigned a;
unsigned int b : 3;
unsigned char c : 5;
};
a无限制,根据数据类型 可推算出它占用4个字节(Byte)。 b、c被位宽限制,不能再根据数据类型计算长度,它们分别占用 3、5 位(Bit)。
我们被问到一个对象生成以后所占用的最小内存是多大,我们总是脱口而出是8个字节,通过上面我们可以更清晰知道 isa 到底里面有什么,为什么 是8个字节(8*8=64位);
注意,在平常的开发中,我们可能经常会说一个对象的isa 指向xx,注意 这里是不准确的,指向一般都用来说明 指针对应的对象,但是为了便于理解,才说的指向。
关键字段解析
nonpointer
标志位直接关系到 isa 指针,是否使用的是 "non-pointer isa"(非指针 isa),即是否采用了压缩和优化存储形式。而不是直接存储类指针,压缩 isa 存储了类指针以及一些额外的元数据。
如果 nonpointer 标志位为 0
isa 指针是传统形式:直接存储该对象的类指针,即指向类的内存地址。 isa 真正是一个指向类结构的普通指针。 这在 32 位架构中比较常见。
如果 nonpointer 标志位为 1
isa 指针采取非指针形式进行压缩存储,包括多个信息: 类指针(通过位移存储,节省空间)。 标志位(如 has_assoc, has_cxx_dtor 等等)。 引用计数的额外部分。 一些辅助信息。 这种形式在 64 位架构中可以更有效地利用内存,同时在保留类指针的基础上,存储更多的状态信息
has_assoc
如果我们给这个类设置了关联对象,这个值就会被设置成1 ,当我们析构的时候也会通过这个标志位会析构对应的关联对象。
shiftcls
Class、Meta-Class对象存储在shiftcls。 cls 与 isa 关联原理就是isa指针中的shiftcls位域中存储了类信息。 shiftcls 是一个经过位移处理的类指针,它通过压缩存储模式在 isa 结构中占用更少的空间,同时允许多存储一些其他元数据。通过这种方式,苹果公司在保留原始功能的同时显著提升了内存的使用效率。
weakly_referenced
当我们该对象生成了弱应用这个值就设置成了1,析构的时候通过这个值是否取析构他的弱应用。
extra_rc
存储引用计数,实际上是引用计数值减1,如果对象的引用计数为10,那么extra_rc为9(这个仅为举例说明),如果引用计数值较小,可以直接保存在 extra_rc 字段中,如果引用计数值较小,可以直接保存在 extra_rc 字段中,对于大多数对象,引用计数不会非常高,通过 extra_rc 可以避免频繁访问和更新外部引用计数存储区域,从而提升性能,当引用计数超出 extra_rc 能表示的范围时,has_sidetable_rc 标志位用于指示存在额外的引用计数表。
has_sidetable_rc
当 extra_rc 超限,会将这个值设置成 1,将多超出的引用计数存储在 sidetable 中。
has_cxx_dtor
表示该对象是否有C++/OC的析构器(类似于dealloc),占1位,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象
扩展
我们这里做一下扩展,后续会有专门的讲解,先做一个了解。在存储一些简单的数据,例如NSNumber、NSDate、NSString等一类的变量,本身他们的值大小范围需要占用的内存大小常常不需要8个字节,拿整数来说,4 个字节所能表示的有符号整数就可以达到 20 多亿 (注:2^31=2147483648) ,另外 1 位作为符号位,对于绝大多数情况都是可以处理的。 为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念。对于64位程序,引入Tagged Pointer后,将值的信息直接存储到了指针本身里面。要注意的是,当8字节可以承载用于表示的数值时,系统就会以Tagged Pointer的方式生成指针,如果8字节承载不了时,则又用以前的方式来生成普通的指针,才会将对象存储在堆上。 由于Tagged Pointer不是对象,所以它的isa应该是无指向的。
NSNumber *number = @1;