1. 对象与类
1.1 对象
|
|
对象(Class或id)内部只有一个isa_t联合体指针。
isa_t联合体内部只有两种成员: Class和bits。其内存结构依照下面的匿名struct进行分配(使用“位域”方式)。
|
|
以x86_64架构为例,bits的内存结构如下:
|
|
故可以看出,在64位环境下,对象占用的内存是8字节。
1.2 类
类Class是objc_class的结构体指针。其继承了objc_object。主要结构如下:
|
|
除了isa_t数据成员,还包含了父类指针,缓存成员及class_data_bits_t数据。
其中,在运行时,类中的相关信息(成员、方法列表、协议列表等)都保存在bits中,也就是class_rw_t指向的数据中。
在编译期,data方法获取到的是class_ro_t的数据,其内部包含的都是在编译期决议的东西。在运行期(runtime系统加载时),其他信息才会加载到类中(如category实现的相关对象、方法等),data返回的才是完整的class_rw_t指针信息。
2. 内存管理容器
2.1 SideTable
SideTable作为内存管理信息保存的容器,在系统中存储多个。系统通过实例对象的hash值与SideTable进行绑定。
一个实例对象对应一个SideTable,而一个SideTable可以供多个实例对象共用。
|
|
其中说明,由于libc在C++初始化对象之前就调用了SideTable,且没有使用全局指针。故使用了这种方式初始化。
根据源码可以看到,SideTables是模板类StripedMap使用SideTable结构体初始化的类实例的指针。StripedMap是一个使用分离锁实现的类,内部成员是一个数组,使用hash算法进行索引存储。其算法如下:
|
|
故在生成SideTable时,系统使用实例对象的地址,将其进行hash计算后,得到index索引,最终从数组中取出SideTable对象:
|
|
SideTable的获取:
|
|
2.2 RefcountMap
RefcountMap是引用计数表存储的数据结构。
|
|
其中存储的key是objc_object的指针。
根据继承关系可以发现,DenseMap继承的是DenseMapBase类,其内部存储的元素BucketT是c++键值对,而其主要功能,就是返回c++的迭代器iterator。
|
|
因此,从代码可以知道,RefcountMap是一个存储键值对(实例对象指针作为key,size_t作为value),使用迭代器进行索引的散列表。
3. 内存管理相关
3.1 alloc & init
|
|
根据调用流程,NSObject的实例创建中,实际上调用的是 id obj = class_createInstance(cls, 0); 。在内部实现中,可以发现zone已经被忽略。
|
|
而最终,经过多步跳转,最后执行的是 obj = (id)calloc(1, size); ,也就是给对象分配内存。
然后通过 obj->initInstanceIsa(cls, hasCxxDtor); 给对象初始化isa_t中的相关信息。
注:
在创建实例对象的过程中,我们发现,系统在类的结构中会尽量使用nonpointer的方式创建对象,以便直接使用自身指针所占空间进行信息保存,提高效率。
最后的isa_t信息赋值过程如下:
|
|
在这里,我们也并未发现系统对新对象的引用计数进行过操作。
而对于init方法,系统默认只是简单地返回了实例对象自身:
|
|
总结:
二段式初始化实例对象,只是分配内存空间,并给isa成员赋值,然后通过init方法返回实例对象。且 并没有 将引用计数设置为1。
3.2 retainCount
由于初始化时系统没有处理引用计数,故我们可以从retainCount中下手,查看如何实现。实例对象的引用计数实现如下:
|
|
在实现中可以看出,新对象的引用计数的1是由retainCount质检设置的。而较小数量的计数时,则直接从Class中的extra_rc数据域中进行读取;而较大计数(超过255以上的)则从SideTable中的refcountMap中进行读取累加后返回。
注意:读取SideTable中的引用计数时需要保证线程安全,进行加锁操作(SideTable内部的是自旋锁,性能好)。
在SideTable中读取引用计数的方式如下:
|
|
这里,table.reccnts.find(this) 就是通过开始提到的DenseMapBase中的查找方法得到引用计数表的迭代器对象。
3.3 retain
说到了读取retainCount值,保留操作又是如何实现的呢?
|
|
由于在ARC环境下,系统在编译期添加的retain实现为底层的c函数(跳过消息发送流程,直接执行函数,效率高),故我们从c的retain方法看起:
|
|
由于我们创建的对象一般不是taggedPointer,故执行的是标准retain函数。
|
|
由于默认没有自定义引用计数相关方法,走的是rootRetain函数。
|
|
这里可以看到,在isa_t中的数据位extra_rc没有超限(小于255)时,直接操作其值+1,完成retain;如果超限,则将extra_rc中的一半(128)拷贝到SideTable中的refcnts中进行存储。其中,在SideTable中进行retain操作如下:
|
|
这里可以看出,SideTable的refcnts中存储的对象的引用计数值(size_t类型,即8字节)中,最高位为上限标识,最低位为weak标识,次低位为deallocating标识,故共有61位用于存储引用计数。
若对象不是nonpointer的,则是使用SideTable直接进行引用计数的,实现就简单多了:
|
|
3.4 release
由于ARC环境,我们也从objc_release函数说起。
|
|
其中,最终执行的是rootRelease函数:
|
|
注:
extra_rc的8位(x86_64架构下)从左往右是低位到高位的存储,故1是0b10000000。
故RC_ONE的 1ULL << 57 对应的正好是extra_rc的最低位,也就是最左边的位。
所以,release时,isa.bits - RC_ONE时,如果引用计数为0,则会出现负值,也就是下溢出。
总结一下,其实release干了这几件事:
- 如果本身不是nonpointer,则直接从SideTable对应的refcnts中进行计数减1操作。
- 是nonpointer的情况:直接对extra_rc引用计数减1。根据结果进行区分:
- 如果原来extra_rc是0,减1后发生下溢出。但是此时需要看一下是否SideTable中还存储着额外的引用计数:如果存在,则“借位”,将原来移过去的128挪回来,减去alloc的1后,将127存储到extra_rc中,结束;SideTable中的引用计数时0,则代表彻底没有引用计数了,需要dealloc。
- 原来extra_rc不是0,减1后,结束。
其中,在SideTable的refcnts计数表中对计数进行减1操作,则很简单:
|
|
最终,同步执行的SEL_dealloc就是我们NSObject释放对象时的dealloc实例方法。
3.5 dealloc
不废话,直接上流程:
|
|
这里可以看到,当对象的isa指针数据中没有相关信息的标志位,也就是说,没有其他额外信息(优化指针,且没有weak指向、没有相关对象、没有实现自定义的c++释放函数且没有使用SideTable进行外部引用计数存储),则直接释放内存(因为相关信息都保存在了isa的位域中了,不存在外部关联内存,故可以直接释放)。因此,如果类没有其他相关设置,释放对象操作会特别高效。
其他情况下,则会调用object_dispose函数进行正常释放:
|
|
在object_dispose中,需要额外进行的,就是在objc_destructInstance函数中,依照顺序将相关联的其他信息移除。顺序如下:
**1. 执行自定义的c++释放方法(如过存在)
- 将相关对象从全局相关信息表中移除(如果存在)
- 移除SideTable中的weak信息及引用计数信息
最终,返回object_dispose中,释放指针内存,结束。**
检查并执行c++的释放函数,依照“子类 -> 父类” 的继承体系依次查找执行。没有实现则直接返回。
12345678910111213141516171819static void object_cxxDestructFromClass(id obj, Class cls){void (*dtor)(id);// Call cls's dtor first, then superclasses's dtors.for ( ; cls; cls = cls->superclass) {if (!cls->hasCxxDtor()) return;dtor = (void(*)(id))lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);if (dtor != (void(*)(id))_objc_msgForward_impcache) {if (PrintCxxCtors) {_objc_inform("CXX: calling C++ destructors for class %s",cls->nameForLogging());}(*dtor)(obj);}}}
注:
这个c++的析构函数(SEL_cxx_destruct,对应的是.cxx_destruct),实际上系统将对象的ivar变量的释放操作插入了这里进行。因此实现了在ARC环境下,对象的成员变量自动释放的功能。
且实现了自动在最后执行[super dealloc]的功能(ARC下dealloc过程及.cxx_destruct的探究,后面llvm实现看不懂,mark一下)。
- 移除相关对象,则是从全局hash表AssociationsHashMap中根据对象地址获取到存储该相关对象的子表,然后移除子表,并将子表中所有指向的对象依次释放。
|
|
释放每个相关对象的操作如下:
|
|
- 最后,清除链接到的其他信息。如外部SideTable中的引用计数表和weak表。
|
|
通过查看对象是否为nonpointer优化指针,根据情况进行SideTable中信息的移除。
|
|
可以看到,二者实现的功能相同,都是在自身对应的SideTable中将refcnts和weak_table对应的信息移除。唯一区别是,nonpointer的优化对象指针需要在执行前判断对应的isa位域的值。
但是重要的是,在对象清除自身引用计数表之前,将weak_table中自身的所有weak指针清除,置为nil。
|
|
因此,我们可以看到,在dealloc执行的最后一步(free指针之前),运行时系统将所有指向释放对象的weak指针置为了nil,完成了weak在ARC下的功能。然后清除引用计数表。
3.6 autorelease
|
|
由于autorelease优化涉及到汇编知识,自己不懂。只明白大体意思:在通过查看调用方的入口地址,通过偏移量算出自身函数返回的地址,得到返回值的接收方的内存管理方式。如果接受对象为strong对象,则使用objc_autoreleaseReturnValue替代autorelease函数(将对象地址保存到当前线程绑定的地址空间中—-TLS,Thread Local Storage,线程局部存储)。
这里我们先看标准的autorelease—-rootAutorelease2。
|
|
这里实际上使用的是AutoReleasePoolPage类进行实现。主要功能如下:
3.6.1 AutoReleasePoolPage的基本结构
|
|
根据AutoReleasePoolPage类的基本结构可以看出:
- 其在内存中的存储结构为:地址由低到高,首先保存的是类中的成员变量,然后是大小为4096字节的存储空间,用于保存设置为autorelease的对象实例。
- 两个指针begin和end分别指向存储空间开始和末尾,使用next指针指向空间中最后一个对象的下一位置,相当于栈结构中的栈顶指针。
- 整体的自动释放池是由一个个AutoReleasePoolPage对象通过parent和child指针连接而成的双向链表。其中,EMPTY_POOL_PLACEHOLDER占位pool并不相当于头指针或头节点,而是只是一个标记位,在尚未使用AutoReleasePool之前节省内存。
现在,我们回到刚才的问题。
3.6.2 objc_object::autorelease的实现
|
|
可以看出autorelease的本质是通过autoreleaseFast将obj插入到AutoReleasePoolPage实例对象中。
|
|
这里,根据当前使用的AutoreleasePoolPage对象的存储状态,分情况进行处理:
- 当前page没有存满,则直接插入;
- 当前page已经存满,创建新page然后插入;
- 不存在page(只有一个EMPTY_POOL_PLACEHOLDER占位用),创建新page然后插入。
相关实现如下(直接插入的不用说了,将next指针的数据域存储上obj地址,然后next后移即可):
- 向已存满的AutoReleasePoolPage对象插入新对象:
|
|
- 只有一个初始状态下的占位pool,没有真正用于存储的AutoReleasePoolPage对象,创建新的page对象,然后插入。
|
|
这里有两个点要注意:
- 获取和创建占位pool:
|
|
这里使用的tls_get_direct和tls_set_direct函数的实现如下:
|
|
它们实际上就是使用一个固定的内存空间来供指定线程来进行数据存取(线程与内存空间一对一绑定)。这种方式叫做TLS—-Thread Local Storage,线程局部存储。
这里就是使用此方式,将1(id指针)存储到当前线程的绑定存储区域中,使用key进行绑定(key - value方式存取)。
- hotPage的读取与设置。
|
|
hotPage,即代表当前正在使用的AutoReleasePoolPage对象(一般意味着自动释放池链表的最后一个pool对象)。也是使用了TLS的方式将当前使用的pool对象的地址与key绑定存储到指定区域中。
3.6.3 建立新的@autoreleasepool{}
注意:
一定要明确,在上层创建@autoreleasepool{}代码块,与底层实现中的AutoReleasePoolPage对象的创建没有任何关系!
底层的整体释放池存储使用的是链表结构,一个AutoReleasePoolPage对象存满之后才会创建新的对象,而创建@autoreleasepool{}代码块,只是在当前的hotPage对应的AutoReleasePoolPage对象中插入一个标志位,用于在RunLoop执行drain时,标记此代码块在hotPage对象中开始的位置。
- 代码块开始对应着objc_autoreleasePoolPush函数:
|
|
在非debug模式下,实际上就是autoreleaseFast(nil) 。即在hotPage对应的AutoReleasePoolPage对象中插入一个标志位。
而在debug模式下,执行的是autoreleaseNewPage(nil) 。也就是每次创建一个新AutoReleasePoolPage对象,然后再插入标志位。只有此时与@autoreleasepool{}代码块是一一对应的。
- 代码块结束对应着objc_autoreleasePoolPop函数:
|
|
这里的实现经过简化。主要目的就是将push时得到的token地址作为标记,将token及其后面插入的autorelease对象依次释放。
其中,主要的实现有两点:
- 根据token确定所处的AutoReleasePoolPage对象。
- 在此page对象中,倒序删除所有保存的数据,直到token地址为止(包括token位置的nil数据)。
依次来看实现。
- 根据插入的对象地址,获取所在的AutoReleasePoolPage对象。
|
|
我们知道,AutoReleasePoolPage对象可以看作为一个类似数组的容器,将p地址减去在一个page容器中的偏移量,即可得到此page对象的起始地址。
- 在AutoReleasePoolPage对象中,倒序移除保存的数据指针。
|
|
实际的过程可以看做是在栈中将对象的地址进行出栈操作,然后对每个出栈的对象调用release方法,直到nil标志位出栈后停止。
在后面检查移除page对象的空的子page中,kill函数也可以看一下:
|
|
由于AutoReleasePoolPage组成的是双向链表,故
- 首先根据child指针域依次找到最末尾的page对象。
- 然后从后往前,根据parent指针找到父page对象,将自身置为nil,断开连接。直到调用方最初的page对象为止。