内存管理总结

内存管理总结

前言

这里主要是对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,本文发布在戴超的技术博客,转载请注明出处

Loading Disqus comments...
Table of Contents