OC基础-内存管理

403 阅读28分钟
本文已参与新人创作礼活动,一起开启掘金创作之路。

一、内存布局

image.png

上面的图代表一个内存区域,内存区域分为内核区的内存(最上边),程序加载的控件(中间),保留的内存空间(最下面) 地址的表示是由下到上是低地址到高地址 比如说程序加载到内存会分成三段:未初始化区,已初始化区和代码段

  • 代码段: 我们写的程序所有的代码数据段都在代码段(.text)中
  • 已初始化区: 我们声明的已初始化的静态变量,全局变量都在已初始化数据区(.data)中
  • 未初始化区: 我们声明的未初始化的静态变量和枚举变量都在未初始化数据区(.bss)中
  • 栈区(stack): iOS定义的方法和函数都是在栈上工作, 栈是从高地址到低地址进行扩展,所以说栈是向下扩展
  • 堆区(heap): 创建的对象,或者block经过copy之后,都会被转移到堆上面,堆是向上增长的

不同内存段分别代表的详细含义:

  • stack: 代表栈区,栈区一般都是方法调用会在这个内存区进行展开。
  • heap: 代表堆区,通过alloc等分配的对象,实际上都是在堆上面体现的。
  • bss: 未初始化的全局变量/静态变量等
  • data: 已初始化的全局变量等
  • text: 程序代码,加载到内存后都放在text段中

二、内存管理方案(更好的简述内存管理方案相关的问题,就要明白他们的数据结构)

iOS操作系统是怎么对内存进行管理的?

iOS操作系统是针对不同场景,会提供不同的内存管理方案,有以下几种方案

  • TaggedPointer 对一些小对象,如NSNumber等,采用的是TaggedPointer这种内存管理方案

  • NONPOINTER_ISA 对于64位架构下的iOS应用程序采用的是NONPOINTER_ISA这种内存管理方案 在64位架构下,ISA这个指针本身是占64个bit位的,但其实有32位或者40位就够用了,剩余的bit位其实是浪费的,苹果为了提高内存的利用率,在iSA剩余的这些bit位当中,存储了一些关于内存管理方面的相关内容,这个叫非指针型的ISA

  • isa指针用来维护 “对象” 和 “类” 之间的关系,并确保对象和类能够通过isa指针找到对应的方法、实例变量、属性、协议等;

  • 在 arm64 架构之前,isa就是一个普通的指针,直接指向objc_class,存储着ClassMeta-Class对象的内存地址。instance对象的isa指向class对象,class对象的isa指向meta-class对象;

  • 从 arm64 架构开始,对isa进行了优化,用nonpointer表示,变成了一个共用体(union)结构,还使用位域来存储更多的信息。将 64 位的内存数据分开来存储着很多的东西,其中的 33 位才是拿来存储classmeta-class对象的内存地址信息。要通过位运算将isa的值& ISA_MASK掩码,才能得到classmeta-class对象的内存地址。

如果isanonpointer,即 arm64 架构之前的isa指针。由于它只是一个普通的指针,存储着ClassMeta-Class对象的内存地址,所以它本身不能存储引用计数,所以以前对象的引用计数都存储在一个叫SideTable结构体的RefCountMap(引用计数表)散列表中。

如果isanonpointer,则它本身可以存储一些引用计数。从以上union isa_t的定义中我们可以得知,isa_t中存储了两个引用计数相关的东西:extra_rchas_sidetable_rc

截屏2022-05-26 下午4.48.11.png

image.png image.png

在arm64架构下,ISA指针一共有64个bit位,我们逐一分析这64个bit位分别都存储了什么内容
首先看0-15位:第一位是(indexed)标志位,如果这个位置是0,代表我们使用的这个ISA指针只是一个纯的ISA指针,里面内容代表当前对象的类对象的地址。若这个标志是1,代表ISA指针里面不仅存储类对象的地址,还有一些内存管理方面的数据

第二位(has_assoc)表示当前对象是否有关联对象,若是0则没有,若是1代表有
第三位(has_cxx_dtor)表示当前对象是否有使用到C++相关的代码
剩下的33位(shiftcls)0,1的一个bit位表示当前对象的类对象的指针地址。
再后6位(magic),不影响内存管理的解答
再后一位(weakly_referenced)标识了这个对象是否有相应的一个弱引用指针
再后一位(deallocating)标识当前对象是否正在进行dealloc操作
再后一位(has_siderable_rc)标识当前ISA指针中存储的引用计数是否达到了上线,若达到了,需要外挂一个sidetable这样的数据结构来存储相关的引用计数内容,也就是我们接下来要了解的散列表。
剩余的(extra_rc)代表的就是额外的引用计数,当引用计数值很小的时候,会存在ISA指针中,当大的时候,会有单独的引用计数表去存储。

通过NONPOINTER_ISA 64个bit位的分析,可以看出,关于内存管理不仅仅是散列表,其实还有ISA部分的extra_rc来存储相关的引用计数值

SideTable散列表方式(关于散列表这种内存管理方案的相关面试问题)

散列表的方案在源码中是通过Side Tables()结构来实现,Side Tables()结构是什么:

image.png

Side Table()结构下挂了很多Side Table这样的数据结构,这些结构在不同的架构下有不同的个数 例如在非嵌入式系统下面,一共有64个Side Table表 Side Table()实际上是一个哈希表 可以通过一个对象指针来具体找到对应的引用计数标或弱引用表在哪一张Side Table中

image.png

Side Table结构下包含了三个元素 1.自旋锁 2.引用计数表 3.弱引用表

面试当中进程会针对引用计数表和弱引用表提出一些相关技术问题。 也会有一些涉及到自旋锁的相关面试问题,不过涉及到一些多线程的和资源竞争方面相关的问题。

为什么不是一个Side Table来实现,而是由多个Side Table共同组成一个Side Tables()这样一个数据结构?

image.png

假如只有一张Side Table,相当于我们在内存当中分配的所有对象的引用计数或者说弱引用都放到了一张大表中, 如果要操作某个对象的引用计数值进行修改(进行+1或者-1的操作), 由于所有的对象可能是在不同的线程中分配创建的(包括调用他们的return或者release等方法,也可能是在不同线程里面进行操作的), 那么对这张表操作时就需要进行加锁处理,来保证数据访问的安全,这样就存在了效率问题。 假如用户的内存空间一共有4GB,我们可能分配出成千上百万个内存对象, 如果每一个对象在进行引用计数改变时,都操作这张表,很显然就存在了效率问题。 当对象A操作时,因为加了锁,下一个对象就要等当前对象操作完之后,将锁释放后,B才能操作。 系统为了解决这样的效率问题,引用了分离锁的技术方案。

image.png

分离锁:可以把内存对象所对应的引用计数表分拆成多个部分,假设分拆成8个,需要对8个表分别加锁, 假如对象A在表1中,对象B在表2中,当A和B同时进行引用计数操作时,可以并发操作, 但如果只有一张表就只能按顺序操作,分离锁可以提高访问效率.

如何实现快速分流(如何通过一个对象指针,如何快速定位到它属于哪张Side Table表)? 快速分流是指: Side Table本质是张Hash表,这张Hash表中,可能有64张具体的Side Table, 存储不同对象的引用计数表和弱引用表

Hash表的概念是:(哈希查找 、哈希算法)

image.png

对象指针可以作为一个key 经过Hash函数的一个运算,会计算出一个值Value,来决定出这个对象所对应的Side Table是哪张,或者说在数组的位置索引是哪个。

下面看下Hash查找的过程

image.png

假如给定的值是对象内存地址,目标值是Side Table结构(数组)下标索引

  • ptr是对象内存指针地址,

  • 通过哈希函数f,把指针ptr作为函数f的参数,

  • 经过函数f的运算,可以得出数组的下标索引值index 哈希函数f对于Side Table具体的情况来讲,实际表达式如图所示f(ptr) = (unitptr_t)ptr % array.count,

  • 通过对象的内存地址,来和Side Table这个数组的个数进行取余运算,

  • 计算出对象指针所对应的引用技术表或者弱引用表是在哪一张Side Table中

为什么要通过Hash查找

  • 可以提高查找效率, 存储时通过Hash进行存储,假如数组是8,假如内存地址是1,取余就是1,我们就把对象存储在第一个位置, 当访问对象时,也不用对数组遍历并比较指针值, 只需要也通过这个函数进行运算,找到第一个索引位置,直接取出内容
  • 这样就不涉及遍历操作了,查找效率比较高
  • 内存地址的分布是均匀分布,我们可以称这个hash函数为均匀散列函数

三、散列表数据结构

1.自旋锁Spinlock_t(你是否使用过自旋锁,自旋锁和普通锁有什么区别,自旋锁有哪些使用场景呢?)

  • 是一种"忙等"的锁,如果当前锁已被其他线程获取,当前线程会不断探测这个锁有没有被释放,如果被释放了,线程就会第一时间去获取这个锁
  • 比如说其他的锁,比如信号量:当它获取不到这个锁时,会把自己的线程进行阻塞休眠,然后等到其他线程释放这个锁的时候,再唤醒当前线程
  • 自旋锁适用于轻量访问。(比如说上面Side Table表,如果说我们对某一个对象来进行引用计数操作的话,来访问这个表,实际上做+1-1操作是非常快的操作,那么我们可以把它定义为轻量访问。我们在这种轻量访问的场景下,可以使用自旋锁。)

2.引用计数表RefcountMap

image.png

  1. 引用计数表是哈希表,可以理解为是一个字典,可以通过指针,找到对应对象的引用计数,这个查找过程是一个哈希查找,
  2. 这个哈希算法实际上是对传入对象的指针做一个伪装的操作,然后去获取对应的引用计数(size_t) 3.之所以使用哈希查找,是为了提高查找效率。 4.size_t表达的就是对象的引用计数值,是一个无符号long型的变量
  • 查找效率的提高,是因为我们存储一个对象的引用计数时,是通过同一个函数来计算存储位置的,而获取对象的引用计数值的时候,也通过同一个函数来计算应该获取的索引位置 因为插入和获取都是通过同一个函数来计算位置,就会避免循环遍历的操作。
  • 所以才说,哈希查找可以提高查找效率

size_t每一个bit为代表的含义

image.png

假如引用计数存储是用64位来表示的

  • 第1个二进制位(weakly_referenced)表示对象是否有弱引用

  • 第2位(deallocating)表示当前对象是否正在delloc

  • 其他(RC)存储这个对象的实际引用计数值

  • 当我们计算对象的引用计数时,需要对这个值进行向右偏移两位,因为要去掉后面两位,才可以取到真实的引用计数值 3.弱引用表weak_table_t

image.png

  • 在Runtime源码中可以看到,弱引用表示根据weak_table_t来定义的,weak_table_t也是一张哈希表,给与一个对象的指针作为key,通过一个哈希函数,就可以计算出对应的弱引用的对象的存储位置
  • weak_entry_t实际上也是一个结构体数组,这个数组中存储的每一个对象就是实际的弱引用指针,也就是我们在代码当中定义的类似于__weak id obj,那么这个obj内存地址或者说这个指针就存储在weak_entry_t这个结构体数组中

四、ARC&MRC(什么是ARC,什么是MRC,他们的区别以及各自实现的机制、原理)

内存管理涉及到以下几个方法:

  • alloc : 分配对象的内存空间。
  • retain : 使一个对象的引用计数加1
  • release : 使对象的引用计数减1
  • retainCount : 获取当前对象的引用计数值
  • autorelease : 当前对象会在autoreleasePool结束的时候,调用这个对象的release操作,进行引用计数减1
  • dealloc : 在MRC中若调用dealloc,需要显示的调用[super dealloc],来释放父类的相关成员变量
属性修饰符
  • strong:强引用,对象生命周期与属性绑定。
  • weak:弱引用,对象释放后自动置nil,避免循环引用。
  • copy:复制对象(深拷贝),常用于不可变类型(如NSString)。
  • assign:直接赋值,用于基本数据类型或旧式代理(非对象类型)。
  • unsafe_unretained:类似weak,但对象释放后不置nil(ARC前使用)。

MRC 什么是MRC:通过手动引用计数来进行对象的内存管理。 MRC中方法retain / release / retainCount / autoreleaset / dealloc, 除了dealloc外,其他的都是MRC特有的,在ARC中若调用这些方法,会引起编译报错

ARC 什么是ARC:通过自动引用计数来管理内存。

之前我认为编译器为我们在对应的位置自动插入相应的retain和release操作,但不完善

image.png

  1. ARC不仅是需要编译器LLVM自动为我们在对应的位置插入相应的retain和release操作,还需要Runtime的功能支持,然后由编译器LLVM和Runtime来共同协作才能组成ARC的全部功能。

  2. ARC中方法alloc/dealloc ARC中禁止手动调用retain / release / retainCount / dealloc, ARC可以重写某个对象的delloc方法,但不能在dealloc中显示调[superdealloc]

  3. ARC中新增weak, strong属性关键字

ARC和MRC的区别

  1. MRC手动管理内存,ARC是由编译器LLVM和Runtime协作进行自动引用计数来自动管理内存
  2. MRC可以调用一些引用计数相关方法,但ARC中不能调用 另外,因为ARC是由编译器为我们自动插入retain和release的,说明ARC中有相当一大部分是由MRC的机制和原理组成的, 所以为了更好的理解iOS的内存管理方式,我们需要深入的了解关于引用计数原理来管理内存的一个方式和原理。 比如说ARC里面涉及到了Runtime协作,针对这个点考察比如 weak变量为什么在对象释放的时候会自动置为nil?

五、引用计数机制(什么是引用计数机制?内存是怎样管理的?)

实现原理分析

alloc /retain/release/retainCount/ dealloc 系统方法内部实现

alloc内部实现

  • 经过一系列的函数封装及调用,最终调用了C函数calloc
  • 关于通过alloc函数分配内存之后的对象,并没有设置引用计数为1
    但当我们通过retainCount获取引用计数却为1
    原因是在retainCount内部实现

retain内部实现

/*通过当前对象的指针this,到SideTables当中,
  经过哈希运算,去获取它所属的SideTable
*/
SideTable& table = SideTables()[this];
/*再通过当前对象的指针this,在SideTable中,也是通过哈希查找,
  从SideTable当中的引用计数表refcnts中,
  去获取当前对象的引用计数值refcntStorage(无符号long型的值)
*/
size_t& refcntStorage = table.refcnts[this];
/*
然后再对引用计数值进行+1操作
*/
refcntStorage += SIDE_TABLE_RC_ONE;


  • 经历两次哈希查找, size_t是无符号long型的一个值
  • 注意当对引用计数进行+1操作时,并不是直接加的1,而是加的一个宏定义,是因为size_t在存储引用计数变量时,里面的64个bit位,前两个位置不是存储引用计数的,后面62位存储的才是引用计数,所以我们所谓的+1操作,加的是对应的偏移量操作(这个偏移量是4,给我们反映的结果就是+1操作)。

在进行retain操作时,系统是如何查找对应的引用计数的?

  • 经过两次哈希查找,找到对应的引用计数值,然后进行相应的加1操作

release内部实现 (和retain正好相反 -1操作)

/*通过当前对象的指针this,到SideTables当中,
  经过哈希运算,去获取它所属的SideTable
*/
SideTable& table = SideTables()[this];
/*根据当前对象指针,
  访问SideTable当中的引用计数表refcnts,
  去查找这个对象对应的引用计数值
*/
 
RefcountMap :: iterator it = table.refcnts.find[this];
/*
  然后对这个值进行减1操作
*/
it -> second -= SIDE_TABLE_RC_ONE;


retainCount内部实现

SideTable& table = SideTables()[this];
//声明一个局部变量,指定它的值为1
size_t refcnt_result = 1;
//通过当前对象到引用计数表中去查找
RefcountMap :: iterator it = table.refcnts.find[this];
//把查找的结果做一个向右偏移的操作,再结合局部变量1做一个增加1的操作,最后返回给调用方
refcnt_result += it -> second >> SIDE_TABLE_RC_SHIFT;


关于通过alloc函数分配内存之后的对象,并没有设置引用计数为1,但当我们通过retainCount获取引用计数却为1问题:

  • 刚刚新alloc出来的一个对象,在引用计数表中,是没有这个对象相关联的key-value映射,这个值读出来的it -> second应该为0,然后由于局部变量refcnt_result是1, 所以此时,只经过alloc调用的对象,调用它的retainCount,获取到的就是1

dealloc内部实现

image.png

首先调用_objc_rootDealloc这样一个私有函数,这个函数内会调用rootDealloc函数, 在rootDealloc函数内部判断当前对象是否可以直接释放,直接释放有以下几个判断条件

  1. 判断当前对象是否使用了非指针型的isa
  2. 判断当前对象是否被weak指针指向
  3. 判断当前对象是否有关联对象
  4. 判断当前对象的内部实现是否涉及C++相关内容或者当前对象是否使用ARC来管理内存
  5. 判断当前对象的引用计数是否是通过sideTable中的引用计数表来维护的

如果采用非指针型的isa指针,它当中会存储一部分引用计数的值,当超出上线时,会使用sideTable方式去存储

  • 只有当当前对象既不是非指针型的isa指针,
  • 同时没有弱引用,
  • 也没有关联对象,
  • 也没有涉及到C++的相关内容,
  • 也没有涉及到ARC,
  • 也没有采用sideTable来存储引用计数, 只有上面条件全部满足,才可以调用C函数的free直接进行内存释放, 否则就会调用object_dispose()对象清除函数,做后续的清理,然后结束

为什么要进行多种判断才能释放对象?

  • 如果当前对象有弱引用,那么在这个对象释放废弃的时候,我们需要对引用对象进行处理
  • 如果当前对象有关联对象,那么在这个对象释放废弃的时候,我们需要对它的关联对象进行处理
  • 包括引用计数和C++的相关内容 等都需要处理

object_dispose()函数的内部实现

image.png

  1. 首先调用objc_destructInstance()函数,这个函数是销毁实例的含义,
  2. 当这个方法调用之后,再调用C函数的free,最后结束dealloc

objc_destructInstance()函数的内部实现

image.png

  • 在objc_destructInstance()中,首先会判断,当前对象中是否有C++相关的内容,或者当前对象是否采用了ARC(hasCxxDtor)?

  • 如果没有的话.会判断当前对象是否有关联对象(hasAssociatedObjects), 如果有的话,调用_object_remove_assocations()对象的相关关联对象的移除函数后,再调用调用clearDeallocating()函数 如果没有的话,调用clearDeallocating()函数

  • 如果有的话,调用object_cxxDestruct()移除后,再判断当前对象是否有关联对象hasAssociatedObjects等 我们通过关联对象的技术,为一个类添加了一些实例变量,那么我们在对象的dealloc方法中,是否有必要对它的关联对象进行移除操作呢? 在系统的dealloc内部实现中,会自动判断当前对象是否有关联对象, 如果有的话,系统内部就帮助我们把相关的关联对象移除了

下面看clearDeallocating方法的内部实现

image.png

  • 首先调用sidetable_clearDeallocating函数,
  • 之后调用weak_clear_no_lock()函数,将指向该对象的弱引用指针置为nil
  • 然后调用table的引用计数擦除操作,实际上就是将当前对象在引用计数表中的一些存储数据给清除掉,然后就结束了调用流程 备注:如果说一个对象有weak指针指向它,当这个对象dealloc或者废弃之后,它的weak指针为何会被自动置为nil?
  • 是因为在dealloc的内部实现中,有做关于它相关的弱引用指针自动置为nil的操作

弱引用表(我们声明weak的一个变量,为什么在内存释放的时候,weak指针会自动置为nil?弱引用变量内存是怎么管理的?)

image.png

//使用__weak关键字修饰的obj1变量指向一个通过alloc分配的一个对象obj,此时有了__weak弱引用指针
 
{
   id __weak obj1 = obj;
}
 
               |  代码块经过编译之后变成下面
               |
{
   id obj1;
   objc_initWeak(&obj1,obj); //实际上是使用objc_initWeak函数,传递了两个参数(弱引用变量的地址,被修饰的对象)
}


下面看下上面发生了什么过程,objc_initWeak的调用栈:

image.png

  1. objc_initWeak函数会调用storeWeak函数,
  2. 然后调用weak_register_no_lock函数,weak指针被添加到弱引用表的具体实现是在weak_register_no_lock中实现的

一个weak变量是怎样被添加到弱引用表当中的?

  1. 一个被声明为__weak的一个对象指针,经过编译器的编译之后呢,会调用objc_initWeak方法。
  2. 然后经过一系列的函数调用栈,最终在这个weak_register_no_lock函数中进行弱引用变量的添加。
  3. 添加的位置是通过一个哈希算法来进行位置查找的。
  4. 如果说我们查找对应位置当中已经有了当前对象所对应的弱引用数组,就把新的弱引用变量添加到那个数组当中如果没有的话,我们就重新创建一个弱引用数组,然后把第0个位置添加上我们最新的weak指针,后面的都初始化为0/nil 当一个对象被废弃/释放之后,weak变量是如何处理的?

image.png weak_clear_no_lock具体方法实现:

 
/**
   weak_table_t:弱引用表
   id:引用的id,实际上就是dealloc的b那个对象
 */
void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    objc_object *referent = (objc_object *)referent_id;
    /**
     通过新声明的局部变量referent,在弱引用表weak_table去查找它的对应的弱引用数组entry。
     weak_entry_for_referent函数:添加weak变量的时候也遇到过。
         通过被废弃对象的指针,经过哈希算法的计算,求出弱引用数组对应的数组索引位置。
         通过索引返回给调用方当前对象所对应的弱引用数组。
     */
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        /// XXX shouldn't happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }
 
    // zero out references
    weak_referrer_t *referrers;
    size_t count;
    /**
     如果弱引用变量的个数小于4的话,就取inline_referrers,反之取referrers,
     总之referrersd所取到的,就是最终当前对象对应的所有弱引用指针的数组列表
     */
    if (entry->out_of_line) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    
    for (size_t i = 0; i < count; ++i) {
        // *referrer:当前对象曾被修饰过的所有的弱引用指针
        objc_object **referrer = referrers[i];
        if (referrer) {
            //如果 *referrer弱引用指针代表的地址就是被废弃的地址referent的话,就置为nil
            if (*referrer == referent) {
                *referrer = nil;
            }
            else if (*referrer) {
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }
    
    weak_entry_remove(weak_table, entry);
}


清除weak变量,同时设置指向为nil。

  • 在调dealloc后,经过一系列的调用,在内部最终会调用弱引用清除的相关函数weak_clear_no_lock()(技术引用章节有提到),
  • weak_clear_no_lock内部会根据当前对象指针查找弱引用表,把当前对象相对应的弱引用都拿出来,是一个数组, 然后遍历数组,遍历所有的弱引用指针,如果弱引用指针代表的地址就是被废弃的地址referent的话,就置为nil。

六、自动释放池(AutoReleasePool)的实现机制和原理是怎样的?

AutoreleasePool的实现原理是怎样的?

image.png

@autoreleasepool{}在编译器的内部实现如下:


//在autoreleasepool中的所有对象,都会添加到自动释放池中,当进行pop之后, autoreleasepool中所有对象都会被发送一次release消息
@ autoreleasepool {
  //AutoreleasePoolPage是C++类,调用它里面的push方法
    void *ctx = objc_autoreleasePoolPush(){
        void *objc_autoreleasePoolPush(void)
                                |
        void *AutoreleasePoolPage::push(void)
     };
 
//调用AutoreleasePoolPage中的pop函数,一次pop实际上相当于一次批量的pop操作
    objc_autoreleasePoolPop(ctx){
        void objc_autoreleasePoolPop(void *ctxt)
                                  |
        AutoreleasePoolPage::pop(void *ctxt)
    };
}

AutoreleasePool为何可以嵌套使用?

自动释放池的数据结构

  • 是以栈为结点通过双向链表的形式组合而成
  • 是和线程一一对应的

双向链表

image.png

黑色箭头代表父指针 红色箭头代表Child指针 Node是一个头结点,它的父指针指向空,后续会有各个结点,后续每个结点都有两个指针,父指针指向前一个结点,Child指针指向后一个结点,最后一个指针的Child指针指向一个空结点

image.png

栈是向下增长的,所以下面是高地址,上面是低地址,对栈的操作实际是有入栈和出栈两种操作 栈的特点是后入先出 后加入栈的对象会最先出栈

AutoreleasePoolPage

image.png

这个类的主要有四个成员变量

  • next: 指向栈中下一个可填充的位置
  • parent: 双向链表中的父指针
  • child: 双向链表中的child指针
  • thread: 说明AutoreleasePool是和线程一一对应的

下图是AutoreleasePoolPage的一个结构 最下面是自身占用内存,上面是用来存储AutoreleasePool中填充的对象,next指针指向当前栈的空位置,若此时进行入栈操作,就可以添加到next指针所指向的位置

image.png

AutoreleasePoolPage中Push方法的内部实现

image.png

假如next在上图位置,此时我们push操作,会把当前next的位置置为nil,也叫做哨兵对象,然后将next指针指向下一个可入栈的位置 实际上每次进行AutoreleasePool的代码块创建的时候,相当于不断的在栈中去插入哨兵对象

[obj autorelease]方法实现

image.png

当我们调用了一个对象的autorelease,首先会判断当前next指针是否指向栈顶,若没有指向栈顶,则直接把对象添加到当前栈的next位置 假如当前next已经位于栈顶,那么当前AutoreleasePoolPage就没办法添加新的autorelease对象了,于是需要增加一个栈结点拼接到链表上,之后再新的栈上面添加对象

下面看运行过程

image.png

若此时next指针指向某个位置,若我们添加了新的对象obj(3)(调用obj(3)的autorelease),放到next位置之后,next指针就会移动到新的位置,再添加新的对象到next位置,next指针继续移动到新的位置....

AutoreleasePoolPage中Pop方法的内部实现

  1. 根据传入的哨兵对象来找到pop的对应位置
  2. 给上次push操作之后添加的对象依次发送release消息,回退next指针到正确位置,根据上面的图,假如此时next指针指向obj(n)的上方,若此 时调用了autoreleasePop 操作,是要给红括号包含的所有对象(3-n)依次发送realease消息,假如发送完,这些对象就会从当前栈中清除,清除之后会把next指针指向正确的位置

image.png

自动释放池

1.在当次runloop将要结束的时候调用AutoreleasePoolPage::pop()

2.autoreleasePool的多层嵌套调用就是多次插入哨兵对象,当我们每次进行autoreleasePool代码块创建的时候,系统就会为我们进行哨兵对象的插入

3.autoreleasePool的使用场景: 在for循环中alloc出大量的图片数据等内存消耗较大,需要在for循环内部手动插入autoreleasePool,每一次for循环,都进行一次内存的释放,来降低内存的峰值

AutoreleasePool的实现原理

以栈为结点,通过双向链表形式组合而成的一个数据结构

参考文章: www.jianshu.com/p/ba49dac19…

七、循环引用

三种类型循环引用

  1. 自循环引用
  2. 相互循环引用
  3. 多循环引用

自循环引用

假如有一个对象,内部强持有它的成员变量obj,
若此时我们给obj赋值为原对象时,就是自循环引用

image.png

相互循环引用

对象A内部强持有obj,对象B内部强持有obj,
若此时对象A的obj指向对象B,同时对象B中的obj指向对象A,就是相互引用

image.png

多循环引用

假如类中有对象1...对象N,每个对象中都强持有一个obj,
若每个对象的obj都指向下个对象,就产生了多循环引用

image.png

常见循环引用

  • 代理

  • Block(Block章节详细说明)

  • NSTimer

  • 大环引用

如何破除循环引用

  1. 避免产生循环引用
    在使用代理时,两个对象,一个强引用,一个弱引用,避免产生相互循环引用
  2. 在合适的时机手动断环

具体方案

  1. __weak
  2. __block
  3. __unsafe_unretained 用这个的关键字修饰的对象也没有增加引用计数,和__weak在效果上是等效的

__weak破解

对象B会强持有A,对象A弱引用B

image.png

__block破解

__block在ARC和MRC中是不同的 MRC下,__block修饰对象不会增加其引用计数,避免了循环引用 ARC下,__block修饰对象会被强引用,无法避免循环引用,需手动解环

__unsafe_unretained破解

修饰对象不会增加其引用计数,避免了循环引用 如果被修饰的对象在某一时机被释放,会产生悬垂指针,再通过这个指针去访问原对象的话,会导致内存泄露,所以一般不建议用__unsafe_unretained去解除循环引用