iOS中内存管理机制是开发中一项很重要的知识,了解iOS中内存管理的规则不管是在开发中还是在学习中都能很大程度的帮助我们提升效率。下面我就根据自己的理解,详细梳理一下内存管理相关的知识。
在说内存管理之前,我们首先要了解什么内存。一块内存条,是一个从下至上地址依次递增的结构,内存条中主要分为几大类:栈区(stack)、堆区(heap)、常量区、代码区(.text)、保留区。常量区分为未初始化区域(.bss)和已初始化区域(.data),栈区stack存储顺序是由高地址存向低地址,而堆区是由低地址向高地址存储。内存条中地址由低到高的区域分别为:保留区,代码区,已初始化区(.data),未初始化区(.bss),堆区(heap),栈区(stack),内核区。而程序员操作的主要是栈区与堆区还有常量区。
关于iOS内存管理的方案其实并非只有散列表一种,还有一种更为高效且为内存高效节省空间的方法叫做TaggedPointer,表明加标记的指针,我们可以理解为在是指针内部增加一些特殊的信息。
那么为什么要使用taggedPointer这种内存管理方法呢,其如何达到节省内存的目的呢。举个例子,比如在OC中一个NSNumber对象,在32位中的系统中占用4个字节的空间,但是迁移至64位系统中后,其占用空间达到了8字节,以此类推,所有在64位系统中占用空间会翻倍的对象,在迁移后会导致系统内存剧增,即时他们根本用不到这么多的空间,所以苹果对于一些小型数据,采用了taggedPointer这种方式管理内存。
其主要的原理就是在对象的指针中加入特定需要记录的信息,以及对象所对应的值,在64位的系统中,一个指针所占用的内存空间为8个字节,已足以存下一些小型的数据量了,当对象指针的空间中存满后,再对指针所指向的内存区域进行存储,这就是taggedPointer。距离NSNumber,最低4位用于标记是什么类型的数据(long为3,float则为4,Int为2,double为5),而最高4位的“b”表示是NSNumber类型;其余56位则用来存储数值本身内容。
之前runtime文章中有提到过objc_objcet对象中isa指针分为指针型isa与非指针型isa(NONPOINTER_ISA),运用的便是类似这种技术。下面详细解读一下NONPOINTER_ISA:
在一个64位的指针内存中,第0位存储的是indexed标识符,它代表一个指针是否为NONPOINTER型,0代表不是,1代表是。第1位has_assoc,顾名思义,1代表其指向的实例变量含有关联对象,0则为否。第2位为has_cxx_dtor,表明该对象是否包含C++相关的内容或者该对象是否使用ARC来管理内存,如果含有C++相关内容或者使用了ARC来管理对象,这一块都表示为YES,第3-35位shiftcls存储的就是这个指针的地址。第42位为weakly_referenced,表明该指针对象是否有弱引用的指针指向。第43位为deallocing,表明该对象是否正在被回收。第44位为has_sidetable_rc,顾名思义,该指针是否引用了sidetable散列表。第45-63位extra_rc装的就是这个实例变量的引用计数,当对象被引用时,其引用计数+1,但少量的引用计数是不会直接存放在sideTables表中的,对象的引用计数会先存在NONPOINTER_ISA的指针中的45-63位,当其被存满后,才会相应存入sideTables散列表中。
所以综上所述,iOS中内存管理的方式主要有三大,1.taggedPointer,2.NONPOINTER_ISA,3.散列表。
下面再进行散列表的分析:
散列表在系统中的提现是一个sideTables的哈希映射表,其中所有对象的引用计数(除上述存在NONPOINTER_ISA中的外)都存在这个sideTables散列表中,而一个散列表中又包含众多sideTable。每个SideTable中又包含了三个元素,spinlock_t自旋锁,RefcountMap引用计数表,weak_table_t弱引用表。所以既然SideTables是一个哈希映射的表,为什么不用SideTables直接包含自旋锁,引用技术表和弱引用表呢?因为在众多线程同时访问这个SideTables表的时候,为了保证数据安全,需要给其加上自旋锁,如果只有一张SideTable的表,那么所有数据访问都会出一个进一个,单线程进行,非常影响效率,而且会带来不好的用户体验,针对这种情况,将一张SideTables分为多张表的SideTable,再各自加锁保证数据的安全,这样就增加了并发量,提高了数据访问的效率,所以这就是一张SideTables表下涵盖众多SideTable表的原因。
基于此,我们进行SideTable的表分析,那么当一个对象的引用计数增加或减少时,需要去查找对应的SideTable并进行引用计数或者弱引用计数的操作时,系统又是怎样实现的呢。
当一个对象访问SideTables时,首先会取到对象的地址,将地址进行哈希运算,与SideTables的个数取余,最后得到的结果就是该对象所要访问的SideTable所在SideTables中的位置,随后在取到的SideTable中的RefcountMap表中再次进行一次哈希查找,找到该对象在引用计数表中所对应的位置,如果该位置存在对应的引用计数,则对其进行操作,如果没有对应的引用计数,则创建一个对应的size_t对象,其实就是一个uint类型的无符号整型。
对于Spinlock_t自旋锁,其本质是一种“忙等”的锁,所谓“忙等”就是当一条线程被加上Spinlock自旋锁后,当线程执行时,会不断的去获取这个锁的信息,一旦获取到这个锁,便进行线程的执行。这对于一般的高性能锁比如信号量不同,信号量是当线程获取到信号量小于等0时,便自动进行休眠,当信号量发出时,对线程进行唤醒操作,这样就致使了两种锁的性质不同。Spinlock自旋锁只适用于一些小型数据操作,耗时很少的线程操作。
对于每张SideTable表中的弱引用表weak_table_t,其也是一张哈希表的结构,其内部包含了每个对象对应的弱引用表weak_entry_t,而weak_entry_t是一个结构体数组,其中包含的则是每一个对象弱引用的对象所对应的弱引用指针。
以上大概就是内存在ios中的管理方式,以及关于内存管理相关的一些数据类型。
关于iOS的两种管理方式:MRC与ARC
MRC是上古时期iOS开发程序员用的一种手动管理对象引用计数的方式,但这也是内存管理的立足之本,ARC就是现代程序员常用的对象引用计数管理方式,ARC是由编译器和runtime协作,共同完成对对象引用计数的控制,而不需要程序员自己手动控制。在MRC中可以调用alloc,retain,release,retainCount,dealloc等方法,这些方法在ARC中只能调用alloc方法,调用其他的会引起编译报错,不过在ARC模式中可以重写dealloc方法。相比起MRC,在ARC中新增了weak和strong等属性关键字。下面详细解读一下MRC ARC中的各个方法。
alloc:这个方法实质上是经过了一系列封装调用之后的calloc,需要注意的是调用该方法时,对象的引用计数并没有+1.
retain:这个方法是先在SideTables中通过哈希查找找到对象所在的那张SideTable表,随后在SideTable中的引用计数表中再次通过哈希查找找到对象所对应的size_t,再加上一个系统的(引用计数+1宏)。为什么这里没有+1而是加上一个系统的宏呢,因为在size_t结构中,前两位不是储存引用计数的,第一位存储的是是否有弱引用指针指向,第二位存储的是对象是否在被回收中。所以,在增加其引用计数时需要右移两位再进行增加,所以用到了这个系统的宏SIDE_TABLE_RC_ONE。
release:这个方法跟retain方法原理一样,只不过是减一个系统的宏SIDE_TABLE_RC_ONE
retainCount:这个方法的实现同样是先查找系统的SideTables表,并找到对象对应的SideTable表,但在之前要先申明一个size_t为1的对象,随后在对应的引用计数表中找到了对象对应的引用计数后,通过右移找到的count对象,与之前创建好的1相加,最后返回其结果便是引用计数。所以这就是为什么系统在调用alloc方法后并没有给对象的引用计数+1,但retainCount方法调用后对象的引用计数就是1的原因。
dealloc:对象在被回收时,就会调用dealloc方法,其内部实现流程首先要调用一个_objc_rootDealloc()方法,再方法内部再调用一个rootDealloc()方法,此时在rootDealloc中会判断该对象的isa指针,依次判断指针内的内容:nonpointer_isa,weakly_referenced,has_assoc,has_cxx_dtor,has_sidetable_rc,如果判断结果为:该isa指针不是非指针型的isa指针,没有弱引用的指针指向,没有相应的关联对象,没有c++相关的内容,没有使用ARC模式,没有关联到散列表中,即判断的内容都为否,则可以直接调用c语言中的free()函数进行相应的内存释放,否则就会调用objc_dispose()这个函数。
而objc_dispose()函数的内部是经由objc_destructInstence()函数调用,随后调用c函数的free()的,顾名思义,objc_destructInstence()函数就是一个销毁对象的函数,那么objc_destructInstence()函数内部实现是什么样的结构呢?
首先,destructInstence()函数内部会来判断该对象是否有C++相关内容以及ARC相关的内容,如果有的话就会调用object_cxxDestruct函数来销毁相应的内容,随后会判断改对象是否有关联对象相关的内容,如果有的话就会调用_object_remove_associations()这个方法来清楚相关的关联对象内容,在这两个判断步骤完成之后,调用clearDeallocating()方法。
在clearDeallocating()方法中,会调用一个sidetable_clearDeallocating()的方法,在方法内部会调用两个方法,1.weak_clear_no_lock()这个方法会将每个弱引用表中的指向该对象的弱引用指针,置为nil。2.table.refcnts.erase()方法,这个方法会从引用计数表中,擦除该对象的引用计数。最后再调用c函数的free()方法,完成一次对象的回收。
所以总结一下dealloc方法的内容大致就是:1.先调用_objc_rootDealloc()方法——>2.在方法内部调用rootDealloc()方法——>3.依次判断5个要素:是否非指着型isa,是否含有关联对象相关内容,是否含有弱引用指针的指向,是否含有c++相关内容以及在ARC模式下使用,是否使用了散列表,如果判断都为否,则直接调用C函数的free()释放对象空间,反之则调用object_dispose()方法。
在object_dispose()方法中的调用流程为:1.调用objc_destructInsetence()方法——>2.调用C函数的free()释放对象空间
在objc_destructInstence()方法内部实现原理为:1.判断是否含有C++相关内容以及使用了ARC模式——>2.调用objcet_cxxDestruct()函数进行清除相关内容——>3.判断是否含有关联对象相关内容——>4.调用_object_remove_associations()方法清除关联对象相关内容——>5.调用clearDeallocating()方法。
在clearDeallocating()方法内部,调用了一个sidetable_clearDeallocating()方法,旨在清除散列表相关的数据信息。
在sidetable_clearDeallocating()方法内部,进行了两个步骤:1.调用weak_clear_no_lock()方法的调用,旨在将对象对应的弱引用表中对应的弱引用结构体数组中的指针全部置为nil——>2.调用table.refcnts.erase(),将对象对应的引用计数表中的引用计数擦除。
完成之后回到之前的步骤,调用free()函数,完成对一个对象的内存回收。
其实通过以上对一些内存管理数据结构的解释以及对象回收的一个完整过程,相应的弱引用创建的的过程以及回收的过程自然也一目了然了。
当我们创建一个弱引用变量weakPointer的时候在编译器中可以这么写
id __weak weakPointer = object;
这行代码实际上在系统内部实现的时候转化为了两行代码:
id weakPointer;
objc_initWeak(&weakPointer,object);
首先定义了一个变量weakPointer,其次调用objc_initWeak方法来给weakPointer的这个弱引用指针来赋值。
内部原理与上面相似,当弱引用指针指向这个objcet变量时,首先去SideTables散列表中通过哈希查找,来找到object这个对象的SideTable表,再通过一次哈希查找,利用object对象的地址,在SideTable中的弱引用表中找到其对应的弱引用结构体数组,如果这个数组存在则在里面添加一个之前weakPointer的地址作为弱引用指针指向object,如果没有这个结构体数组,则创建一个数组,将这个指针添加到第0个元素,同时给第2,3,4个元素设置为nil。这样就完成了一个弱引用指针的定义实现过程了。
关于如何清除弱引用指针的,Dealloc方法调用过程已经说的很明白了,过程也与上面一行说的类似,就是在最后调用sidetable_clearDeallocating()方法中将对象对应的弱引用列表找到,将所有弱引用指针置为nil的时候就把相应的弱引用指针擦除了,这样说就一目了然了。
希望通过这篇文章也可以帮助到同样在学习iOS内存管理的你!
本文章由作者原创,未经允许禁止转载
---------------------本文来自 Horson19 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/Horson19/article/details/82593484?utm_source=copy