iOS的内存管理
- 内存布局
- 内存管理方案
- 数据结构
- ARC&MRC
- 引用计数
- 弱引用
- 自动释放池
- 循环引用
内存布局
- stack区
- 方法调用
- heap区 (堆区)
- alloc分配的一些对象
- bss :未初始化的全局变量
- data:已初始化的全局变量
- text:程序代码
内存管理方案
- TageedPointer
- NONPOINTER_ISA
- 64位 isa占64位,一般用30~40就够用了,所以在剩余的位中做了内存管理
- 散列表 是复杂的数据结构
- 引用计数表
- 弱引用表
内存管理的分析全部都是基于开源代码的讲解
NONPOINTER_ISA
arm64结构下是64位
- 第一位indexed
- 0代表isa只是一个纯的isa指针,里面内容指向当前的类对象地址,
- 1代表不单单是isa指针,还有一些关于内存管理的一些数据
- 第二位has_assoc
- 0代表 没有关联对象
- 1代表 有关联对象
- 第三位has_cxx_dtor 当前对象是否用到c++
- 0 没有
- 1 有
- shiftcls 4 ~ 36 33位 比特位是类对象的地址
- 当前类对象的内存地址
- magic
- weakly_referenced
- 是否有弱引用指针
- deallocating
- 是否有dealloc的操作
- has_sidetable_rc
- 如果当前的引用计数已经达到上限,需要外挂一个has_sidetable_rc一个数据结构去存储相关的引用计数内容
- extra_rc 44~63 都是 extra_rc 引用计数
- 引用计数在很小的范围内,会直接存储到isa这里,而不需要单独的引用计数表来单独存储
散列表方式
- sideTables()结构
- SideTable (数据结构)
- 64个(非嵌入式系统)
SideTable
为什么不是一个SideTable? 为什么要组成SideTables呢?
- 对象可能需要在不同线程操作创建,这个时候进行操作的时候需要进行加锁处理才能保证安全,这样存在效率问题
- 用户的内存空间需要4gb,我们可以分配成千上万的引用对象,如果每个引用对象我们都要对齐进行引用计数改变,都操作这张表就会出现效率问题。如果一个对象操作这张表,会加锁下一个对象需要在等上一个对象的锁加完以后在处理,这就造成了效率低下的问题
- 系统为了解决这个效率低下,系统给我们提供了一个分离锁的概念
- 系统为我们分配了8个这个表(64位),这样就可以并发的去修改引用解决问题。
怎么样实现快速分流?
SideTables的本质是一张Hash表。
可能有64个SideTable存储不同的引用计数表、和弱引用表
什么是hash算法
对象指针可以作为Key通过hash函数计算计算出hash表中你的value值的下标也就是索引。
hash查找的过程
给定值是对象内存地址,目标是数组下标索引。
给了一个对象的内存地址,通过哈希函数的运算,得到了一个集合的下标索引值
对象的内存地址指针 % 数组的count就可以得到这个数组中的索引
这样就查找速度快了
散列表方案设计的数据结构
- Spinlock_t
- 自旋锁 是"忙等"的锁,如果当前锁已经被其他线程获取,那么当前线程会不断的探测这个锁是否被其他线程释放,如果释放自己第一时间去获取这个锁
- 特点:适合轻量访问(这个中+1 -1 计算逻辑不复杂的情况下称之为轻量)
- 信号量
- 当前线程获取不到锁的时候,会把自己的线程阻塞休眠,等这个锁被释放的时候,会唤醒它获取这个锁
引用计数表
- 实际上是一个refcountMap(hash表或者是字典),提高查找效率,存储查找都是hsah,找到
- size_t引用技术使用64位存储的
- 第一位是否有weak
- 第二位是否deallocating
弱引用表
- weak_table_t也是一张hash表,
- 通过对象指针,hash算法的到我们的weak_entry_t的表
- weak_entry_t里面存储的就是弱引用指针。
MRC和ARC
MRC
手动引用计数,进行内存管理
- alloc 分配一个对象的内存空间
- retain 引用计数加1
- release 引用计数-1
- retainCount 当前引用计数的值
- autorelease 会在autoreleasepool结束的时候,向其每一个对象发送release消息
- dealloc 在MRC中需要显示的调用super dealloc需要废弃父类的相关成员变量
ARC
什么事ARC
全名自动引用计数
- ARC是由LLVM和Runtime协作完成
- ARC中禁止手动调用retain、release、retainCount、dealloc、
- 可以重写dealloc但是不能调用super dealloc
- ARC中新增weak、strong属性关键字
ARC和MRC的区别
- MRC是手动管理,ARC是LLVM和Runtime协作进行自动引用计数管理
- MRC可以调用一些引用计数相关的+-1操作方法,而ARC是不能的
- ARC有weak和strong
引用计数管理
实现原理分析
alloc实现
- 经过一些列调用,最终调用了C函数calloc(初始化完成会自己置0 而malloc是垃圾数据)。
- 此时并没有设置引用计数为1
retain实现
- SideTable &table = SideTables()[this] 先获取对应的SideTable
- size_t & refcntStorage = table.refcnts[this]; 获取到SideTable中的引用计数值
- refcntstorage += SIDE_TABLE_RC_ONE; 对其+1
- SIDE_TABLE_RC_ONE不是1,前两个位置是存储弱引用计数,和是否正在dealloc的,是后面62位,所以需要做一个偏移量。 所以说是4.
release实现
- SideTable &table = SideTables()[this] 先获取对应的SideTable
- RefcountMap:iterator it = table.refcnts.find(this); 获取到SideTable中的引用计数值
- it->second -= SIDE_TABLE_RC_ONE; 和retain正好相反
retainCount的实现
- SideTable &table = SideTables()[this] 先获取对应的SideTable
- size_t refcnt_result = 1; 声明一个局部变量
- RefcountMap::iterator it = table.refcnts.find(this) 获取到SideTable中的引用计数值,刚去alloc的对象其实里面是为0的
- refcnt_result += it-> second >> SIDE_TABLE_RC_SHIFT 把查找的结果做一个向右偏移的操作,然后在+1
dealloc的实现
- 调用_objc_rootDealloc
- rootDealloc
- 是否直接释放的判断标准
- nonpointer_isa 是否是非指针型的isa指针
- weakly_referenced 是否有weak指针指向它
- has_assoc 当前对象是否有关连对象
- has_cXX_dtor 是否使用了C++相关的代码,是否ARC
- has_sidetable_rc 是否是通过sidtable表来存储的。
- 以上都不是 就可以释放,如果可以就使用c函数的free函数释放
- 如果不可以就使用object_dispose函数来进行后续的处理,然后结束
object_dispose实现
- objc_destructInstance
- C函数的free
- 结束
objc_destructInstance的实现
- 是否有C++或者是否有ARC
- YES object_cxxDestruct
- NO 调用AssociatedObjects
- 如果没有C++或者ARC 就调用AssociatedObjects方法
- YES _object_remove_assocations()
- NO clearDealloccating
- _object_remove_assocations,以后还是没有关联对象那么都会调用clearDealloccating结束objc_destructInstance方法。
clearDealloccating的实现
- sidetable_clearDeallocating
- weak_clear_no_lock
- 将指向该对象的弱引用指针置为nil
- table.refcnts.erase
- 从引用计数表中擦除该对象的引用计数
弱引用管理
弱引用调用栈
一个被声明为__weak弱引用的指针,经过编译器编译以后,会调用objc_initWeak方法,经过一系列的函数调用栈,会调用weak_register_no_lock这个函数,进行弱引用变量的添加。
具体添加的位置,是通过一个hash算法来进行进行位置查找的,
如果说我们查找位置当中已经有了当前对象所有的弱引用数组,我们新的弱引用变量添加到引用数组当中,如果没有弱引用数组,那么就创建一个新的弱引用数组,然后第0个位置添加wek指针,后面的都置为0。
当清除weak变量 通知设置指向为nil, 它是如何置nil的
完整过程
- 通过被废弃对象的指针经过hash算法的到,求出弱引用数组对应的数组索引位置,然后通过这个索引返回给对象我这个弱引用数组
- 如果弱引用数组为空,不做操作
- 如果弱引用数组不为空遍历这个数组,如果数组中的指针就是被废弃的弱引用指针那么就将其置为nil。
总结
- 当一个对象被dealloc之后,dealloc内部的实现会去调用弱引用清除这个函数
- 然后函数会通过,当前对象指针查找弱引用表 ,把一个对象的所有弱引用表都拿出来,是一个数组
- 编译所有这个数组当中的弱引用指针分别置为nil
自动释放池
编译器改写
- objc_autoreleasepoolPush()和objc_autoreleasePoolPop中间是代码
- push返回值是一个无类型的指针
-
objc_autoreleasePoolPop需要这个无类型指针作为标记也就是push返回的无类型的指针
objc_autoreleasepoolPush和objc_autoreleasePoolPop的函数调用栈
- 内部会调用AutoreleasePoolPage::push方法
- AutoreleasePoolPage::pop方法
- 一次Pop当然于一次批量的Pop操作(AutoreleasePool中的对象都会放入到自动释放池中当Pop之后,AutoreleasePool中的对象的对象都会发送一次release消息所以是批量操作)
自动释放池的数据结构
- 以栈为节点通过双向链表的形式组合而成
- 适合线程一一对应的。
AtuoreleasePoolPage的数据结构
- id*next
- 当前栈的空的位置
- autoreleasePollPage * const parent;
- 链表的父指针
- autoreleasePollPage *child;
- 链表的孩子指针
- pthread_t const thread;
- 与线程一一对应的
AtuoreleasePoolPage::Push
autorelease
- 一个对象调用了autorelease方法,这个对象添加到next指针之后,next移动到新的位置,下一个调用autorelease方法的对象就会添加到这个位置
AtuoreleasePoolPage::Pop
- 根据传入的哨兵对象找到对应位置
- 给上次Push操作之后添加的对象依次发送release消息(next指针到哨兵对象之间的对象依次发送release消息)
- 回退指针搭配正确位置
双向链表的概念
栈结构概念
面试题自动释放池的实现结构是什么?
- 以栈为节点通过双向链表的形式组合而成
自动释放池的总结
下面代码什么时候释放
viewdidload{
NSMutableArray *array = [NSSmutableArray array];
}
- 在当次runloop将要结束的时候调用AutoreleasePoolPage::Pop()
- 每一次runloop循环将要结束的时候都会对前一次创建的autoreleasePool进行Pop操作,同时会Push进来一个新的autoreleasePool
- viewdidload创建的对象,是在当次runloop将要结束的时候,调用autoreleasePoolpage::Pop方法时候,把对应的array对象调用它的release方法。
多次嵌套就是多次插入哨兵对象
关于autoreleasepool为和可以嵌套调用?
实际上多次嵌套插入哨兵对象,每次进行autoreleasePool的代码块创建的时候,假如autoreleasePoolpage没有满的情况下,系统就会在当前autoreleasePoolpage栈中进行一次哨兵对象的插入,如果满了会创建一个新的autoreleasePoolpage,然后当前的autoreleasePoolpage中的child指针指向新创建的autoreleasePoolpage。
所以autoreleasepool可以多层嵌套调用
循环引用
三种循环引用:
- 自循环引用
- 相互循环引用
- 多循环引用(大环)
自循环引用
相互循环引用
多循环引用(大环)
如何破除循环引用?
- 避免产生循环引用
- 在合适的时机手动断环
具体解决方案:
- __weak
- __block
- __unsafe_unretained
__weak 破解循环引用
__block破解* 破解循环引用
- MRC __block修饰对象不会增加其引用计数,避免了循环引用
- ARC __block修饰对象会被强引用,无法避免循环引用,需手动解环
__unsafe_unretained 破解循环引用
- 修饰对象不会增加引用计数,避免了循环引用
- 如果被修饰对象在某一时机被释放,会产生悬垂指针,悬垂指针又可能会出现不可预见的问题
循环引用示例
在开发过程中,你遇到过哪些循环引用问题,你又是如何解决的?
Block的使用示例
NSTimer的循环引用问题
有一个页面有一个banner滚动栏,每3秒滚到一次。一般有一个banner对象,VC对它强持有,
banner对象的需求是每3秒滚动一次。需要添加一个NSTimer,当我向banner对象添加回调的时候,NSTimer会对banner对象施加一个强引用,这个时候就产生了相互循环引用问题。
实际上,NSTimer创建完成以后会有一个主线程RunLoop去强引用NSTimer强引用。就算是VC退出释放掉,那么banner对象也不会释放,因为主线程runLoop强引用了这个对象。
NSTimer有重复,以及非重复定时器,假如是非重复定时器,那么在NSTimer回调完成以后设置无效并置nil,那么就破解了循环引用。
假如NSTimer重复多次回调
第一种:在NSTimer和Banner之间设置一个中间对象,这个中间对象分别弱引用NSTimer和banner对象。那么当VC释放掉,banner对象也就释放掉了,NSTimer的回调会去中间对象,而中间对象只要判断banner对象为不为nil 如果为nil就直接无效化NSTimer。
考点
- 代理
- Block
- NSTimer
- 大环引用
面试题总结
如果我们对一个类添加了关联对象,那么在这个类被释放以后会清除掉这个关联对象吗?
会的,因为系统在dealloc中释放了关联对象
关于autoreleasepool为和可以嵌套调用?
实际上多次嵌套插入哨兵对象,每次进行autoreleasePool的代码块创建的时候,假如autoreleasePoolpage没有满的情况下,系统就会在当前autoreleasePoolpage栈中进行一次哨兵对象的插入,如果满了会创建一个新的autoreleasePoolpage,然后当前的autoreleasePoolpage中的child指针指向新创建的autoreleasePoolpage。
autoreleasePool的应用场景
在for循环中alloc图片数据等内存消耗较大的场景手动插入autoreleasePool。
autoreleasePool的实现原理是什么?
autoreleasePool实现原理:
- 以栈为节点,通过双向链表的形式组合而成的数据结构
什么是ARC
- ARC是由LLVM编译器以及runtime协作来为我们实现自动引用计数的管理
为什么weak指针指向的对象在废弃之后会被自动置为nil
当对象在被废弃之后,dealloc的内部方法实现当中会调用清除弱引用的方法,然后在清除弱引用的方法中会通过hash算法来查找被废弃对象在弱引用表当中的位置来提取它所对的人用引用指针的列表数组,然后进行for循环遍历,把每一个弱引用指针都置为nil
苹果是如何实现AutoreleasePool
AutoreleasePool是以栈为节点,双向链表形式来合成的数据结构。
什么是循环引用?你遇到过哪些循环引用,是怎么解决的?
- 自循环引用
- 双向循环引用
- 大环引用
比如说NStimer的循环引用。