导图社区 iOS 的内存管理
该导图为 iOS 下内存管理相关的原理和问题。
编辑于2020-05-13 16:18:11了解 iOS 的内存管理吗?
为什么需要内存管理?
ARC 只是在代码层面上自动添加内存管理的代码,并不能自动内存 管理,并且一些高内存消耗的场景我们必须进行手动内存管理。 所以理解内存管理是每一个 iOS 或者 macOS 应用开发者的必备能 力。
高内存消耗场景有哪些?
例如: 在 for 循环中 alloc 图片数据等内存消耗较大的场景
数据结构
有关散列表实现的内存管理方案涉及到的主要三个数据结构
自旋锁(Spinlock_t)
Spinlock_t 是“忙等”的锁 适用于轻量访问 这里所描述的“忙等”,指的是如果当前锁已被其他线程获取,那么当前线程会不断的探测这个锁是否有被释放,如果释放掉,自己第一时间去获取这个锁。 其他非“忙等”的锁,例如正常的信号量,当它获取不到锁的时候,它会把自己这个线程进行阻塞休眠,等到其他线程释放这个锁的时候,来唤醒这个线程。
更多相关,看《多线程》
你是否使用过自旋锁?
自旋锁和普通的锁有什么区别?
自旋锁适用于哪些场景呢?
引用计数表 (RefcountMap)
引用计数表实际上是一个哈希表。
引用计数表是通过什么来实现的?
它是用 Hash 表来实现的。
为什么引用计数要用 Hash 表来实现?
实际上是为了提高查找效率,而提高查找效率的本质原因,是因为插入和获取是通过同一个 Hash 算法或者说 Hash 函数来决定的,避免了 For 循环遍历。
我们通过一个指针,可以找到对应对象的一个引用计数,那么这个查找过程,实际上也是一个 Hash 查找,这个 Hash 查找的 Hash 算法实际上是对这个传入对象的指针做一个伪装的操作,然后去获取对应的引用计数。
size_t
size_t 表达的是对应对象的引用计数值。 实际上它就是一个无符号、long 型的变量。  图析: 比如说这个引用计数存储是用 64 位来表示的,那么在这个 size_t 中,第一个二进制位表示的是,这个对象是否有弱引用。用 weakly_referenced 这个标识位来代表,0 没有,1 有。 第二位用来表示当前对象是否正在 dealloc。 剩下的部分存储的是这个对象实际的引用计数值。我们在计算这个对象的具体引用计数值时,需要对这个值进行向右偏移两位,因为上面讲的两位需要将它们去掉,方可取到真实的引用计数值。
弱引用表(weak_table_t)
在 runtime 源码中,弱引用表系统是通过 weak_table_t 来定义的。  关于弱引用表的数据结构的描述: weak_table_t 这个弱引用表实际上也是一张 Hash 表。 同样的,如果说我们给予一个对象的指针作为 key 的话,那么通过一个 Hash 函数就可以计算出对应的弱引用对象的存储位置,或者说我们应该查找的位置。 那么这个 weak_entry_t ,它实际上也是一个结构体数组,这个结构体数组中存储的每个对象是实际的弱引用指针,也就是我们在代码当中定义的如 __weak id obj,那么这个 obj 的内存地址或者说指针,就存储到 weak_entry_t 这个结构体数组当中。
weak_entry_t
它实际上也是一个结构体数组,这个结构体数组中存储的每个对象是实际的弱引用指针,也就是我们在代码当中定义的如 __weak id obj,那么这个 obj 的内存地址或者说指针,就存储到 weak_entry_t 这个结构体数组当中。
ARC&MRC
MRC(手动引用计数)
ARC(自动引用计数)
ARC 是 LLVM 和 Runtime 协作的结果。 ARC 中禁止手动调用 retain/release/retainCount/dealloc ARC 中新增 weak、strong 属性关键字 ARC ,自动引用计数来管理内存。 ARC 实际上是有编译器自动为我们插入 retain/release 操作之外,还需要 Runtime 的功能进行支持。然后,由编译器和 Runtime 共同协作才能组成 ARC 的一个全部功能。 ARC 中是禁止手动调用一些 MRC 当中独有的方法,例如 retain 等,并且,我们在 ARC 当中可以重写某个对象的 dealloc 方法,但是不能在 dealloc 方法当中显示调用 [super dealloc]。 除此之外,ARC 当中,又新增了 weak、strong 两个属性关键字。
ARC 和 MRC 之间的区别
从它们各自的特点入手。 MRC 是手动管理内存,ARC 是有编译器和 Runtime 协作来进行自动引用计数的内存管理。同时,MRC 当中可以调用一些引用计数相关的方法,而 ARC 当中是不能调用的。
引用计数机制来管理内存的理解
weak 变量为何在对象释放的时候会自动置为 nil ?
内存管理机制的一部分。 Runtime 相关。
引用计数管理
面试官从来都不会问你引用计数相关的一些方法的实现原理。但是他所问的关于引用计数的相关面试问题都可以在对应方法的实现当中去找到答案。
alloc
alloc 的实现
经过一系列调用,最终调用了 C 函数的 calloc 此时并没有设置引用计数为 1
通过 alloc 函数分配之后的对象,并没有设置引用计数为 1, 但通过 retainCount 获取它的引用计数为 1,为什么?
retain
retain 的实现
retain 的实现 SideTable& table = SideTables()[this]; size_t& refcntStorage = table.refcnts[this]; refcntStorage += SIDE_TABLE_RC_ONE; (Objc-runtime-680 版本源码片段截取)
我们在进行 retain 操作的时候系统是怎样查找它对应的引用计数的?
经过两次 Hash 查找,找到它对应的一个引用计数值,然后进行相应的加 1 操作。
release
release 的实现 SideTable& table = SideTables()[this]; RefcountMap::iterator it = table.refcnts.find(this); it ->second = SIDE_TABLE_RC_ONE; 第一行,通过当前对象,经过 Hash 算法在 SideTables 当中找到它所属的的 SideTable。 然后,根据当前对象指针访问这个 table 当中的引用计数表去查找它对应的引用计数表(it),查找到之后,把对应的值进行减 1 操作。 和 retain 操作正好相反。
retainCount
retainCount 的实现 SideTable& table = SideTables()[this]; size_t refcnt_result = 1; RefcountMap::iterator it = table.refcnts.find(this); refcnt_result += it->second >> SIDE_TABLE_RC_ONE; 第二句,声明了一个局部变量,并指定它的值是 1。 第三句,通过当前对象到引用计数表中去查找。 第四句,把查找到的结果做一个向右偏移的操作,然后再结合第二句中局部变量的 1,进行一个加的操作,再返回给调用方。 所以说,如果我们刚创建出的一个对象,在引用计数表当中实际上是没有这个对象相关联的 Key-Value 的映射的。那么这个值读出来就是 0,然后,由于局部变量是 1,所以此时只经过 alloc 调用产生的对象,我们去调用它的 retainCount 就可以获取到它的值为 1。
通过 alloc 函数分配之后的对象,并没有设置引用计数为 1, 但通过 retainCount 获取它的引用计数为 1,为什么?
如果我们刚创建出的一个对象,在引用计数表当中实际上是没有这个对象相关联的 Key-Value 的映射的。那么这个值读出来就是 0,然后,由于局部变量是 1,所以此时只经过 alloc 调用产生的对象,我们去调用它的 retainCount 就可以获取到它的值为 1。
dealloc
dealloc 的实现
object_dispose() 的实现
在这个函数的实现当中,首先判断当前对象当中是否有 C++ 相关的内容,或者说是否当前对象采用的是 ARC,如果有的话,会调用 object_cxxDestruct() 这个方法,如果没有,会判断当前对象是否有关联对象。 如果有关联对象,在 dealloc 的内部实现会调用 _object_remove_assocations() 函数,通过名称看出是做了对象相关关联对象的移除。 如果没有关联对象,调用 clearDeallocating() 函数,结束 dealloc 的调用流程。
objc_destructInstance() 的实现
dearDeallocating() 的实现
弱引用管理
一个 weak 变量是怎样被添加到弱引用表当中的
objc_initWeak() 的调用栈
当一个对象被释放或者说废弃之后,weak 变量是怎样处理的
相关问题
说一下弱引用是怎么实现的?
weak 实现原理的概括 Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。 weak 的实现原理可以概括一下三步: 初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。 添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。 释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
关于系统是怎样把一个 weak 变量添加到它对应的弱引用表当中的?
一个被声明为 __weak 对象指针,经过编译器编译之后,会调用对应的 objc_initWeak() 方法,经过一系列的函数调用栈,最终在 weak_register_no_lock() 函数当中进行弱引用变量的添加。 添加的具体位置,是通过一个 Hash 算法来进行位置查找的,如果说我们查找的对应位置当中已经有了当前对象所对应的一个弱引用数组,我们就把新的弱引用变量添加到这个数组当中,如果没有的话,创建一个弱引用数组,然后把第 0 个位置添加上最新的 weak 指针,后面的都初始化为 0,或者说 nil。
两个问题大同小异
当一个对象被释放或者说废弃之后,weak 变量是怎样处理的?
当一个对象被废弃之后,weak 变量为什么会被自动置为 nil ?
当对象被废弃之后,dealloc() 方法的内部实现当中,会调用清除弱引用的方法,通过 Hash 算法来查找被废弃对象在弱引用表当中的位置,来提取它所对应的弱引用指针的列表数组,然后进行 for 循环变量,把没有 weak 指针都置为 nil。
自动释放池
相关问题
在 viewDidLoad 方法中,我们通过 [NSMutableArray array] 方法创 建了一个可变数组对象,然后在控制台打印它的值。 请思考,这个 array 对象的内存是在什么时机释放的呢?
在当次 runloop 将要结束的时候调用 AutoreleasePoolPage::pop()。 实际上,在每次 runloop 的循环过程中,都会在它即将结束的时候,对前一次创建的 AutoreleasePool 进行 pop 操作,同时会 push 进来一个新的 AutoreleasePool,所以说,我们在 viewDidLoad 当中所创建的 array 对象,是在当次 runloop 将要结束的时候调用 AutoreleasePoolPage 的 pop 方法,把对应的 array 对象调用它的 release 函数或者说方法,然后对它进行释放。
AutorealeasePool 的实现原理是怎样的?
以栈为结点,通过双向链表形式组合而成的一个数据结构。
AutoreleasePool 为何可以嵌套使用?
多层嵌套调用就是多次插入哨兵对象。 比如说,我们每次进行一个 Add AutoreleasePool 代码块的创建的时候,系统就会进行一个哨兵对象的插入,然后,完成这个新的 AutoreleasePool 的创建。 那么,这个新的 AutoreleasePool 的创建,实际上是创建了一个新的 Page,假如说这个 Page 当前 Page 没有满的话,就不用创建这个 Page。 所以说,我们所说的新创建的一个 AutoreleasePool,对应的在底层实现就是插入一个哨兵对象。 故而,当然是可以直接多层嵌套调用的。
AutoreleasePool 的一个使用场景
在 for 循环中 alloc 图片数据等内存消耗较大的场景手动插入 AutoreleasePool。 在 for 循环中,alloc 出大量的图片数据,图片数据对内存消耗非常大,我们就需要在 for 循环内部创建一个 AutoreleasePool,每一次 for 循环都进行一次内存的释放,来降低内存的峰值,防止一些内存消耗过大所导致的问题。
编译器会将 @autoreleasepool{} 改写为: void *ctx = objc_autoreleasePoolPush(); {} 中所添加的代码 objc_autoreleasePoolPop(ctx);
第一段是 objc_autoreleasePoolPush() 这样的一个函数调用,函数的参数是没有的,返回值是一个无类型的指针。 中间是花括号当中所添加的代码。 之后,会调用 objc_autoreleasePoolPop(ctx) 函数,函数的入参是前一个函数 objc_autoreleasePoolPush() 的返回结果。
这个函数内部会调用 C++ 类的一个方法,这个类叫 objc_autoreleasePoolPage,是一个很关键的数据结构,调用它里面的一个 push 方法,这个方法的参数和返回值和前面 objc_autoreleasePoolPush() 函数的参数和返回值是一样的。
objc_autoreleasePoolPush() 的内部实现
如果说当前的 next 指针,是指向这个位置,此时,如果说发生了一次 AutoreleasePool 的 push 操作,那么会把当前 next 的位置置为 nil,实际上我们称它为哨兵对象,然后将 next 指针指向下一个可入栈的位置,这个就是 AutoreleasePoolPage 的 push 方法的实现过程。 实际上,每次进行一个 AutoreleasePoolPage 的代码块的创建,相当于是不断的在这个栈当中插入哨兵对象。
AtuoreleasePoolPage 当中 push 方法的内部实现
objc_autoreleasePoolPop 的实现
这个函数,最终也会调用 objc_autoreleasePoolPage 当中的 pop 函数,它的参数和 objc_autoreleasePoolPop 的参数是一致的。 一次 pop 操作实际上相当于一次批量的 pop 操作。 如何理解呢? 在 AutoreleasePool,这个花括号当中所包含的所有对象,都会添加到自动释放池当中,当进行 pop 之后,在花括号当中所有的对象,都会被发送一次 release 消息。 所以我们把它解释为一次批量的 pop 操作。
AutoreleasePoolPage::pop
根据传入的哨兵对象找到相应的位置 给上次 push 操作之后添加的对象依次发送 release 消息 回退 next 指针到正确位置
根据传入的哨兵对象找到相应的位置 给上次 push 操作之后添加的对象依次发送 release 消息 回退 next 指针到正确位置 pop 流程的简单说明: 在进行 pop 操作的时候,会根据传入的哨兵对象来找到 pop 的最终位置。 这个所谓的哨兵对象,是 autoreleasePool::push 这个函数的返回值,实际上返回的是这个栈当中对应的哨兵位置。 pop 操作实际上和 push 操作是一一对应的,那么,我们要 pop 回哪个位置,实际上是要 pop 回它所指定的前一个哨兵对象,然后给上一次 push 操作之后添加的对象依次发送 release 消息。 之后,回退 next 指针到正确的位置。 图析: 假设现在这个指针是指向图中左侧所示的位置,然后,调用一次 autorelease pop 操作,那么,要给红括号所包含的这些对象依次发送 release 消息,假如说,发送 release 之后,这些对象就会从当前的这个栈当中被清除掉,清除之后,会把 next 指针指向一个正确的位置。 这个就是 pop 的整个过程。
AutoreleasePoolPage::pop
什么是自动释放池?/ 自动释放池的数据结构是怎样的?
是以栈为节点通过双向链表的形式结合而成的 是和线程一一对应的
双向链表
栈是向下增长的,所以,下方是高地址,上方是低地址,下方是栈底,上方是栈顶,对栈的操作有出栈和入栈。 栈这个结构有一个特点:后入先出。后入栈的对象会最先出栈。
栈
C++ 的一个类 这个类的组成结构主要有如图的四个成员变量。 第一个是 id* 类型的 next,实际上就是指向栈当中可填充的下一个位置。 之后是两个 AutoreleasePoolPage 类型的成员变量,分别为 parent 和 child,也就是双向链表中的父指针和孩子指针。 最后一个成员变量是 pthread_t 类型的,所以我们可以说 AutoreleasePool 是和线程一一对应的。就是从这个结构当中的 thread 成员变量体现出来的。
最下方是 AutoreleasePoolPage 自身占用的内存,上方那些,可以用来存储花括号中间填充的 Autorelease 对象。 同样的,这个栈是向下增长的,所以下方是高地址,上方是低地址。 栈底和栈顶位置如图,有一个 next 指针,也就是 AutoreleasePoolPage 这个结构当中的一个成员变量指向当前栈的一个空位置。 比如说,现在再进行一次入栈操作,那么可以添加到 next 指针所指向的位置。
AtuoreleasePoolPage 的结构
假设调用了一个对象的 autorelease,那么首先会判断当前 next 指针是否指向了栈顶,如果没有,直接把这个对象添加到当前这个栈的 next 位置,然后结束流程。 如若当前 next 已经位于栈顶,这个时候当前的 AutoreleasePoolPage 就没有办法再装新的 autorelease 对象了,于是需要增加一个栈结点,然后拼接到链表上,之后在新的栈上去添加对象,最终结束流程。
[obj autorelease] 系统内部的实现流程
假设此时 next 指针指向途中左侧所示位置,然后,这个位置如果产生了一个新的对象,是由于调用了这个 obj 的 autorelease,那把这个对象添加到 next 之后,next 指针就会移动到新的位置,再次添加对象的时候,就可以添加到新的 next 位置上面。
[aobj autorelease] 的运行过程
循环引用
分类
主要可分为三种类型: 自循环引用 相互循环引用 多循环引用
自循环引用
相互循环引用
多循环引用
对循环引用的理解
循环引用在面试过程中的考点
代理
Block
NSTimer
大环引用
相关问题
如何破除循环引用?
需要针对不同种类的循环引用,分别制定相应的破除方案。 主要的破除思路有两个: • 避免产生循环引用 • 在合适的时机手动断环 比如说我们在使用代理的过程当中,两个对象,一个是强引用,一个是弱引用,避免产生相互循环引用。 这两点就是解开循环引用的思路和方案。
破除循环引用可以使用的具体解决方案有哪些?
• __weak • __block • __unsafe_unretained 一般,我们会使用 __weak ,大家在使用代理的时候呢,也会用到,再比如说在使用 block 的时候,我们也会用到。 第二个解决方案呢,是 __block,通过 __block 来解除循环引用,一般使用在 block 方面产生的循环引用问题。 还有一种呢,就是 __unsafe_unretained 这种方式来破除循环引用,因为由这个关键字所修饰的对象呢也没有增加引用计数,它和 __weak 在效果上是等效的。
__weak 破解
假如说左侧的对象 A 和右侧的对象 B 都有一个成员变量 id 类型的 obj,当我们把对象 A 当中的 obj 声明为 __weak 关键字的话,就可以破除相互循环引用。 对象 B 会强持有 A,而 A 是弱引用了 B,此时,就规避了相互循环引用的问题。 此处采取的解决思路就是避免产生循环引用。
__block 破解
需注意__block 在 ARC 和 MRC 下的区别
关于 __block 破解有一个易错点,也是面试官经常会考察的点。 比如说面试官问你解除循环引用都可以用哪些技术? 如果说提到了 __block 破解的话,我们还需要注意 __block 在 ARC 下和 MRC 下的一个区别。 如果说是在 MRC 下的话,__block 所修饰的对象不会增加引用计数,可以避免循环引用,但是如果在 ARC 下的话,__block 修饰的对象会被强引用,是无法避免循环引用的,此时,我们可以通过手动破除循环引用的方式来解除循环引用。 这个也是 __block 用来破除循环引用很大的一个特点。需注意,__block 破解循环引用在 MRC 和 ARC 下两种不同场景它所解除循环引用的一个特点,这也是面试过程中的一个易错点。
__unsafe_unretained 破解
修饰对象不会增加其引用计数,避免了循环引用。 如果被修饰对象在某一时机被释放,会产生悬垂指针! __unsafe_unretained 关键字所修饰的对象不会增加其引用计数,也就是避免了循环引用。 如果说被修饰对象在某一时机被释放,我们再继续通过这个指针去访问原对象的话,会由于悬垂指针的原因导致内存泄漏。 所以一般不建议用 __unsafe_unretained 去解除循环引用,因为它会产生一些后续可能发生的不可预见的问题。
你在平时开发过程中是否有遇到过循环引用? 你又是怎样解决你所遇到的循环引用问题的?
在日常开发中我们遇到了 NSTimer 使用场景下造成了循环引用问题。 问题的解决,是通过创建一个中间对象,令中间对象持有两个弱引用变量,分别为 NSTimer 和原对象,在 NSTimer 当中直接分派的回调是在中间对象中实现的,然后在中间对象实现的回调方法中,对它所持有的 target 进行值的判断,如果说当前值存在,直接把 NSTimer 回调给原对象,如果说当前对象已经被释放,那么我们把 NSTimer 设置为无效状态并置为 nil,就可以解除当前线程的 runloop 对 NSTimer 的强引用,以及 NSTimer 对中间对象的强引用。
一般问及循环引用问题时往往是一个开放性的问题。
Block 相关循环引用问题
NSTimer 循环引用相关问题
原因分析
假如说我们有一个实际的场景,比如说有一个页面,在这个页面当中假设有一个 banner 广告栏,需要在每一秒钟滚动一次,播放下一个动画或者说下一个广告,一般情况下,我们把这个 banner 广告栏的 UI 对象作为 VC 的一个成员变量,由 VC 对它进行强持有,由于 banner 滚动广告栏需要每隔一秒钟进行一次滚动播放,就涉及到定时器的使用,所以我们需要在这个对象当中去添加一个成员变量 NSTimer,当我们添加了 NSTimer 之后呢,为它分配一个相应的定时回调事件之后,NSTimer 会对它的 Target 进行一个强引用,这个时候就产生了一个相互循环引用的问题。
解决思路
错误思路: 把这个对象对 NSTimer 的强引用换成弱引用。 错误原因: NSTimer 当被分派之后,实际上会被当前的 runloop 进行一个强引用,如果说我们这个对象或者说 NSTimer 是在主线程当中创建的,那么就由主线程的 runloop 持有 NSTimer,所以即使这个位置我们通过弱引用来指向 NSTimer,但是由于主线程的 runloop 常驻内存,然后通过对 NSTimer 的强引用,再通过 NSTimer 对对象的强引用,仍然对这个对象产生了一个强引用,那么此时,即使 VC 页面退出,VC 的引用去掉的话,这个当前 banner 滚动广告栏呢由于被 runloop 间接的强引用持有了,这个对象也不会被释放,此时就产生了内存泄漏。
错误思路
正确思路
解决方案: 关于 NSTimer 是有重复定时器和非重复定时器的区分,假如说我们创建的这个 NSTimer 是一个非重复的定时器,一般情况下,我们会在这个定时器的回调方法当中去调用 NSTimer 的 invalid 方法,同时将这个 Timer 置为 nil,通过这种方法,我们可以把 runloop 对 NSTimer 的强引用解除掉,同时 NSTimer 也解除了对对象的强引用,我们可以把这个循环引用给破除掉。 假如说当前的 NSTimer 是一个重复多次回调的一个定时器的话,我们就不能在定时器的回调方法中做这个 invalid 和 timer 置 nil 的操作。那么此时,我们该怎样破除这个循环引用呢? 左侧是 runloop 对 NSTimer 的强引用,右侧是 VC 对对象的强引用,我们可以在 NSTimer 和对象之间添加一个中间对象,然后由 NSTimer 对实行一个强引用,同时中间对象分别对 NSTimer 和 banner 广告栏的这个对象做一个弱引用,此时,对于一个重复的定时器而言,当当前 VC 或者说页面退出之后呢,VC 就释放了对 banner 广告栏的一个强引用,当下次定时器的回调事件回来的时候我们可以在中间对象当中中,Timer 的回调方法里面去进行判断当前这个中间对象所持有的弱引用对象是否被释放废弃掉了,实际上我们就判断中间对象中所持有的 weak 变量是否为 nil。 实际上这种解决方案也是利用了一个对象被释放之后它的 weak 指针会自动置为 nil 这个特点来解决这个问题。 在这个实现当中,如果说我们判断当前中间对象所指向的 banner 广告栏对象已经被释放的话,我们可以在这个这个 NSTimer 在中间对象的回调方法当中去把这个 NSTimer 给无效并且置为 nil,就可以实现去除 runloop 对 NSTimer 的强引用,以及 NSTimer 对中间对象的强引用。 这样的话,NSTimer 和中间对象就都可以得到内存的释放,达到破除循环引用的目的。
解决代码
NSTimer+WeakTimer.h #import <Foundation/Foundation.h> @interface NSTimer (WeakTimer) + (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats; @end NSTimer+WeakTimer.m #import "NSTimer+WeakTimer.h" @interface TimerWeakObject : NSObject @property (nonatomic, weak) id target; @property (nonatomic, assign) SEL selector; @property (nonatomic, weak) NSTimer *timer; - (void)fire:(NSTimer *)timer; @end @implementation TimerWeakObject - (void)fire:(NSTimer *)timer { if (self.target) { if ([self.target respondsToSelector:self.selector]) { [self.target performSelector:self.selector withObject:timer.userInfo]; } } else{ [self.timer invalidate]; } } @end @implementation NSTimer (WeakTimer) + (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats { TimerWeakObject *object = [[TimerWeakObject alloc] init]; object.target = aTarget; object.selector = aSelector; object.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:object selector:@selector(fire:) userInfo:userInfo repeats:repeats]; return object.timer; } @end
相关问题
如果说一个对象有 weak 指针指向它,当这个对象 dealloc 或者说废弃之后,它的 weak 指针为何会被自动置为 nil ?
因为在 dealloc 的内部实现当中,是有做关于它相关的弱引用指针自动置为 nil 的操作的。
相关问题
我们通过关联对象的技术为一个类添加了一些实例变量,那么我们在这个对象的 dealloc 方法当中,是否有必要对它的关联对象进行移除操作呢?
不需要。 在系统的 dealloc 内部实现中,会自动判断当前对象是否又关联对象,如果有的话,系统内部就帮助我们把相关的关联对象一并移除掉。
iOS 系统是怎样对内存进行管理的?
iOS 系统针对不同的场景,会提供不同的内存管理方案。 分别是: TaggedPointer NONPOINTER_ISA 散列表 对于一些小对象,如 NSNumber 等,采用的内存管理方案是 TaggedPointer。 对于 64 位架构下的 iOS 应用程序,采用的是 NONPOINTER_ISA 内存管理方案。 散列表是一个很复杂的数据结构,其中包括了引用计数表和弱引用表。
简述 NONPOINTER_ISA 内存管理方案的含义
在 64 位架构下,isa 这个指针本身是占 64 个比特位的,而实际上有 32 位或者 40 位就够用了,剩余的 这些比特位是浪费的,苹果为了提高内存的利用率,在 isa 剩余的这些比特位当中,存储了一些内存管理方面相关的数据内容。 所以这个叫非指针型的 isa。
isa 指针类型的确定
64 个比特位中,第一位是一个叫 indexed 的标识位。 如果这个位置是 0 代表的是该 isa 指针是一个纯的 isa 指针,它里面的内容就直接代表了当前对象的类对象的地址。 如果是 1 的话,就表示这个 isa 指针里面存储的不仅是它的类对象的地址,还有一些内存管理方面的数据。也就是非指针型的 isa(NONPOINTER_ISA)。
是否需要外挂一个 sidetable?
第 44 个标识位,是指当前这个 isa 指针中,如果存储的引用计数已经达到上限的话,那么需要外挂一个 sidetable 数据结构去存储相关的引用计数内容(即散列表)。
额外的引用计数
第 45 位到 63 位,这几位代表的是 extra_rc,也就是额外的引用计数。当我们的引用计数在一个很小的值的范围之内,就会存到 isa 指针当中,而不是由单独的引用计数表去存储它的引用计数。
散列表
散列表方案在源码当中,是通过 SideTables() 结构来实现的。 
SideTables() 结构是什么?
这个结构下,挂了很多个 SideTable 数据结构,这些数据结构在不同架构上有不同的个数。 例如,在非嵌入式系统当中,SideTable 这个表一共有 64 个。 SideTables() 实际上是一个哈希表,我们可以通过一个对象指针来找到它的弱引用表或者说引用计数表在哪一张具体的 SideTable 当中。
SideTable 的结构
SideTable 这个结构中,就包含了一下三个元素: 自旋锁 引用计数表 弱引用表 
自旋锁(Spinlock_t)
引用计数表 (RefcountMap)
弱引用表(weak_table_t)
散列表内存管理方案涉及的数据结构
为什么不是一个 SideTable,而是多个?
假如说只有一个长 SideTable,相当于我们在内存中分配的所有对象 的引用计数或者说弱引用的存储都放在一张大表里。这个时候,如果 说我们要操作某一个对象的引用计数值进行修改,比如进行加 1 或 减 1 操作,由于所有的对象可能是在不同的线程当中去分配创建的, 包括调用它们的 release、retain 等方法,也可能是在不同的线程当 中进行操作的。那么这个时候,我们如果对这张表进行操作,需要进 行加锁处理才能保证数据的访问安全,这个过程中就存在了一个效率 的问题。 比如说,用户的内存空间一共有 4 GB,那么我们可能分配出成千上 百万个对象,如果说每一个对象在对它进行内存引用计数改变的时候, 都操作这张表,那么很显然就会有效率问题。 如果说现在有一个对象在操作这张表,那么下一个对象就要等前一个 对象操作完了,把锁释放之后,才能操作这张表。很明显,成千上百 万个对象都操作这张表的话,自然会存在效率问题。
分离锁技术方案
系统为解决只一个 SideTable 的话存在的效率问题,而引入的技术 方案。 我们可以把内存对象对应的引用计数表拆分成多个部分。 比如,拆成 8 个,此时需要对 8 个表进行加锁。 假设某一个对象 A 在一个表里,另一对象 B 在另一张表里,那么当 A 和 B 同时进行引用计数操作的时候,可以并发操作。但是如果按 照一张表的情况,它们就需要顺序操作,显然,引入分离锁的方案 可以提高访问效率。
怎样实现快速分流
 比如说,给定值是对象的内存地址,目标值是数组(SideTables 这个结构当中)的下标索引。 比如说,现在给你一个对象的内存指针地址,我们通过一个哈希函数把这个指针作为这个函数的参数,然后经过这个函数的运算就可以得出一个数组的下标索引值。 那这个哈希函数对于这个 SideTables 这个具体情况来讲,实际上表达式如上图,通过对象的内存地址来和 SideTables 这个数组的个数来进行取余运算,这样,就可以计算出一个对象指针所对应的引用计数表或者说弱引用表在具体哪一张 SideTable 中。
实际上指的是我们如何通过一个对象的指针 快速定位到它属于哪一张 SideTable 表。
为什么要通过 Hash 查找?
为了提高查找效率。 比如说,我们存储的时候是通过这个 Hash 函数进行存储的,比如说这个数组个数是 8,内存地址我们这里假设是 1,那么取余就是 1,我们就把这个对象存储到数组对应的第一个位置,当我们去访问这个对象的时候,也不需要去根据数组进行遍历来比较指针值,而是通过这个函数来进行一次运算。 例如,同样一个对象还是 1,然后和 8 取余就是 1,那么我们可以直接到第一个索引位置中去取出我们想要的内容。 在这个过程中,不涉及遍历操作,其查找效率自然比较高。 内存地址的分布,实际上是一个均匀分布,我们可以称这个函数为均匀散列函数。
SideTables 的本质
SideTables 的本质是一张 Hash 表,这张 Hash 当中有多张具体的 SideTable 存储不同对象的引用计数表和弱引用表。 
Hash 表
 如图,左侧是一个对象,这个对象指针可以作为一个 key 经过 Hash 函数的运算,计算出一个值,来决定它所对应的 SideTable 是哪张,或者说在数组的位置(索引)是哪个。