内存管理总结
内存管理总结
前言
这里主要是对iOS内存管理做下总结,主要从内存布局、内存管理方案、数据结构、ARC & MRC、引用计数管理、弱引用管理、自动释放池、循环引用这些方面来入手。
内存布局
关于iOS程序内存布局,如下图所示
- stack:方法调用,程序运行时存放局部变量
- heap:通过alloc等分配的对象
- bss:未初始化的全局变量等
- data:已初始化的全局变量等
- text:程序代码
内存管理方案
可根据场景区分:
- TaggedPointer:如NSNumber等小对象
- NONPOINTER_ISA:在64位架构下的程序,非指针型的isa
- 散列表:是一个复杂的数据结构,包含了引用计数表、弱引用表
NONPOINTER_ISA
arm64架构,64个bit位具体的含义:
- indexed:标志位
- 0:代表isa指针是一个的纯的isa指针,里面的内容就代表了当前对象的类对象的地址
- 1:代表isa指针里面存储的,不仅是类对象的地址,还有一些内存管理方面的数据
- has_assoc:表示当前对象是否有关联对象,0没有,1有
- has_cxx_dtor:表示当前对象是否有使用到c++相关的内容
- shiftcls:表示当前对象的类对象的指针地址
- weakly_referenced:标识了当前对象是否存在弱引用指针
- deallocating:表示当前对象是否正在进行deallocat操作
- has_sidetable_rc:是指当前isa指针当中,如果所存储的引用计数已经达到了上限,需要外挂一个sidetable的数据结构去存储引用计数内容
- extra_rc:表示额外的引用计数(存储相关的引用计数值),当引用计数在一个很小的范围之内,就会存储到isa指针当中
散列表方式
SideTables()结构:SideTables本质是一个Hash表
Q:为什么不是一个SideTable?
如果只存在一个SideTable,则所有的对象都存在一起,
当我们在多线程的环境下,去操作某一个对象的引用计数,
就需要进行加锁处理,才能保证数据的访问安全;
这其中就存在效率问题。
解决方案:分离锁的技术方案
把内存对象的引用计数表分拆成多个部分,
这样可以对分拆后不同的表分别加锁,
这样可以并发操作不同表上的对象的引用计数
Q:怎样实现快速分流?(如何根据对象指针,快速定位到在哪张SideTable表中)
通过Hash查找(可提高查找效率),得到对应的SideTable表
Key(对象指针) —(Hash函数)—>Value(SideTable)
数据结构
SideTable
- Spinlock_t:自旋锁
- RefcountMap:引用计数表
- weak_table_t:弱引用表
自旋锁Spinlock_t
-
Spinlock_t是”忙等”的锁。
-
适用于轻量访问
引用计数表RefcountMap
是用Hash表来实现的,是为了提高查找效率,插入、修改都是通过同一个Hash算法来实现的,从而避免了循环遍历
Hash查找 ptr—DisguisedPtr(objc_object)—>size_t
size_t:表示对象的引用计数值,unsign long类型
-
weakly_referenced:标识了当前对象是否存在弱引用指针
-
deallocating:表示当前对象是否正在进行deallocat操作
-
RC:存储了对象实际的引用计数值(计算数值时,需要向右偏移2位)
弱引用表weak_table_t
也是用Hash表来实现的
Hash查找 Key(对象指针)—Hash函数—>Value(weak_entry_t)
- weak_entry_t:实际上是一个结构体数组
MRC & ARC
MRC:手动引用计数
涉及的方法
方法名 | 含义 |
---|---|
alloc | 分配一个对象的内存空间 |
retain | 对象的引用计数+1 |
release | 对象的引用计数-1 |
retainCount | 获取当前对象的引用计数值 |
autorelease | 若调用了对象的autorelease,则在autoreleasepool结束时,会调用对象的release方法 |
dealloc | 显示调用,来释放或废弃父类的成员变量 |
ARC:自动引用计数
- ARC是LLVM(编译器)和Runtime协作管理内存(如weak指针是如何在对象释放的时候自动设置为nil?涉及了runtime的协作)
- ARC中禁止手动调用retain/release/retainCount/dealloc
- ARC中新增weak、strong属性关键字
引用计数管理
- 从alloc、retain、release、retainCount、dealloc的实现原理来讲解
alloc实现
是经过一系列调用,最终调用了C函数calloc。此时并没有设置引用计数为1。
retain实现
代码片段
SideTable& table = Sidetas()[this];
size_t& refcntStorage = table.refcnts[this];
refcntStorage += SIDE_TABLE_RC_ONE; // 加1操作-加的是偏移量;SIDE_TABLE_RC_ONE宏定义
release实现
代码片段
SideTable& table = Sidetas()[this];
RefcountMap::iterator it = table.refcnts.find(this);
it->second -= SIDE_TABLE_RC_ONE;
retainCount实现
代码片段
SideTable& table = Sidetas()[this];
size_t refcnt_result = 1; // 局部变量
RefcountMap::iterator it = table.refcnts.find(this);
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
dealloc实现原理(重点)
dealloc方法内部流程:
- nonpointer_isa:非指针型的isa;里面部分字节存储了引用计数,如超出,则会在sidetable中存储引用计数
- has_cxx_dtor: 当前对象是否存在c++语法的使用,是否使用了ARC管理
object_dispose()实现原理
objc_destructInstance()实现原理
clearDeallocating()实现原理
弱引用管理
- 添加weak变量
- 注意:获取到弱引用对象的数组,是通过弱引用对象进行hash算法计算,得出弱引用对象的位置
- 清除weak变量,同时设置指向为nil
dealloc() - … - weak_clear_no_lock()
// 通过hash算法,获得弱引用对象的数组,在遍历设置为nil
自动释放池
我们从其实现原理来分析,编译器会将@autoreleasepool{}
改写为:
void *ctx = objc_autoreleasePoolPush();
{}中的代码
objc_autoreleasePoolPop(ctx);
- objc_autoreleasePoolPush
void* objc_autoreleasrPoolPush(void)
转化为c++方法调用
void* AutoreleasePoolPage::push(void)
- objc_autoreleasePoolPop
void objc_autoreleasrPoolPop(void * ctx)
转化为c++方法调用
AutoreleasePoolPage::pop(void* ctx)
- 一次pop实际上相当于一次批量的pop操作:会对池中所有的对象都会发送一次release消息
自动释放池的数据结构
- 以
栈
为结点的双向链表
的组合形式 - 是和
线程
一一对应的
结点:AutoreleasePoolPage
id* next;
AutoreleasePoolPage* const parent;
AutoreleasePoolPage* child;
pthread_t const thread;
- AutoreleasePoolPage::push
- AutoreleasePoolPage::pop
- 根据传入的哨兵对象找到对应位置
- 给上次push操作之后添加的对象依次发送release消息
- 回退next指针到正确位置
Q:AutoreleasePool的实现原理?
自动释放池的数据结构以`栈`为结点的`双向链表`的组合形式;
编译器会将@autoreleasepool{}改写为:
void *ctx = objc_autoreleasePoolPush();
{}中的代码
objc_autoreleasePoolPop(ctx);
在PoolPush中,会插入一个哨兵对象,PoolPop中根据哨兵对象,将之后产生的对象依次做release操作。
Q:什么时候会释放局部变量?
在当次runloop将要结束的时候调用AutoreleasePool::pop()
Q:AutoewleasePool为何可以嵌套使用?
多层嵌套就是多次插入哨兵对象
- 场景应用:
在for循环中alloc图片等内存消耗较大的场景手动插入autoreleasePool
循环引用
存在三种循环引用:
- 自循环引用
- 相互循环引用
- 多相互引用
- 常考点:
- 代理
Block
NSTimer
- 大环引用
(Block和NSTimer是重点)
Q:如何破除循环引用?思路:
- 避免产生循环引用
- 在合适的时机手动断环
Q:具体的解决方案都有哪些?
- __weak
- __block
- __unsafe_unretained (没有增加引用计数)
__block破解 注意点
- 在
MRC
下,__block修饰对象不会增加其引用计数,避免
了循环引用。 - 在
ARC
下,__block修饰对象会被强引用,无法避免
循环引用,需手动断环
。
__unsafe_unretained破解
- 修饰对象不会增加其引用计数,
避免
了循环引用。 - 如果被修饰对象在某一时机被释放,会产生
悬垂指针
!,导致内存泄漏
悬垂指针:指针所指向的对象已经被释放或者回收了,
但是指向该对象的指针没有作任何的修改,仍旧指向已经回收的内存地址。
此类指针称为垂悬指针。
循环引用示例
- Block的使用示例,__block修饰对象,需手动断环。请浏览Block知识点总结
-
NSTimer使用示例
- NSTimer的循环引用问题(相互循环引用)
- 场景:页面Banner循环滚动显示
- 错误思路:对象弱引用Timer;因为NSTimer被分派之后,会被当前线程的RunLoop给强引用,则RunLoop也会对
对象
进行强引用,对象也不会得到释放,即产生内存泄漏- 不产生循环事件,则在执行方法中调用Timer的invalidate,设置Timer=nil,来破除循环引用
- 若存在循环事件,就无法得知结束,就不能通过invalidate和设置nil来解决循环引用
- 解决方案:
(中间对象在回调事件中判断持有的对象还是否存在(利用一个对象被释放后,weak指针对象会置为nil的特点))
总结
- 什么是ARC?
- 为什么weak指针指向的对象在废弃之后会被自动置为ni?(hash查找,数组遍历置为nil)
- 苹果是如何实现 AutoreleasePool的?(数据结构)
- 什么是循环引用?你遇到过哪些循环引用,是怎样解决的?(NSTimer举例)
参考文章
最后
如果对大家有帮助,请github上follow和star,本文发布在戴超的技术博客,转载请注明出处