iOS内存管理之二:MRC(MannulReference Counting)
在上一篇文章中,介绍了内存的概述与iOS系统中内存
分布,让大家对内存的概念稍微有点“敏感”,虽然现在大部分iOS项目开发都是基于ARC的,但是对于MRC的理解也很重要。下面主要介绍基于MRC环境下的内存管理。
一、引用计数
iOS内存管理采用的引用计数器原理,那么什么是引用计数?原理是什么?为什么要使用引用计数?iOS大V@唐巧在《iOS开发进阶》一书中做了介绍。我在这里简单描述一下。
1.引用计数原理
“引用计数器”可以简单理解为对象的一个标识属性,它标识着对象被多少个对象引用(持有)。当我们创建一个对象时,它的引用计数为1,当一个新的指针指向这个对象时,引用计数加1;当指针指针不再指向(持有)这个对象时,引用计数减1。当对象的引用计数为0时,说明这个对象不再被任何指针指向,这时就可以销毁对象,回收对象所占用的内存。
引用计数原理图解如下:
当然,还有一种其他比较容易理解的解释场景:员工在办公室使用灯的情景,具体大家可以去这篇博文去看,在此不再阐述。
这样说或与有一些抽象,为了形象一下,下面用代码进行简单阐述:
注:在运行这段程序时,我们需要手动关闭ARC,方法如图所示:
示例程序:
1 | - (void)viewDidLoad { |
运行结果:
1 | 2016-03-28 16:39:52.035 MemoryTest[69135:40583765] Reference count = 1 |
2.引用计数的存储
内存管理并不是管理所有内存,任何继承了NSObject
的对象需要进行内存管理,而对于基本数据类型(int、char、short、float、double)、结构体、枚举等不用去关心内存。对象引用计数存储有两种方式:通过一个全局的散列表存储;通过isa指针存储。
(1)在32位环境下,对象的引用计数都保存在一个全局的散列表中。当对一个对象进行Retain
操作时:先获得记录引用计数的散列表;然后对表进行加锁(为了线程安全);然后读取引用计数加1,并写回散列表;最后解锁。
(2)在64为环境下,如果对象支持使用优化的isa
指针,那么对象的引用计数一般会存储在isa
指针中。否则的话,还是使用散列表来存储。
下面是在arm64
架构的设备下,64位环境中isa
指针结构:
1 |
|
针对上面的指针结构,下面是每个bit位的具体作用:
bit位 | 变量名 | 意义 |
---|---|---|
1 bit | indexed | 0 表示普通的 isa 指针,1 表示使用优化,即Tagged Pointer 存储引用计数 |
1 bit | has_assoc | 表示该对象是否包含 associated object ,如果没有,则析构(释放内存)时会更快 |
1 bit | has_cxx_dtor | 表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,则析构(释放内存)时更快 |
30 bits | shiftcls | 类的指针 |
9 bits | magic | 固定值为 0xd2,用于在调试时分辨对象是否未完成初始化。 |
1 bit | weakly_referenced | 表示该对象是否有过 weak 对象,如果没有,则析构(释放内存)时更快 |
1 bit | deallocating | 表示该对象是否正在析构 |
1 bit | has_sidetable_rc | 表示该对象的引用计数值是否过大无法存储在 isa 指针 |
19 bits | extra_jc | 表示引用计数值减一后的结果。例如,如果对象引用计数为4,则extra_jc为3 |
在64位环境中,即使使用优化的 isa
指针,也不一定存储存储引用计数,因为用19bit保存引用计数不一定够。另外,如果对象的引用计数值过大无法存储在 isa
指针中,即 has_sidetable_rc
值为1时,引用计数会存储在一个叫 SideTable
的类的属性中。
3.引用计数的获取
(1)在非ARC环境中,我们可以使用 [obj retainCount]
方法进行获取。在运行时对象会指向 objc_object
的 rootRetainCount()
方法。
1 | - (NSUInteger)retainCount { |
(2)在ARC环境中。可以使用Core Foundation 库的CFGetRetainCount()
方法:
1 | - (void)viewDidLoad { |
还可以使用 Runtime 的 _objc_rootRetainCount(id obj) 方法:
注:需要引入 <objc/runtime.h>
1 | - (void)viewDidLoad { |
_objc_rootRetainCount()
方法实现如下:
1 | inline uintptr_t |
由以上程序可以看出,当对象使用 isa
优化指针时,直接返回对想引用计数;64为环境优化的 isa
无法使用时,调用 seidtable_retainCount
方法。至于调用 seidtable_retainCount
方法的过程,暂不做分析。
4.为什么要使用引用计数
使用引用计数,主要是为了方便我们管理对象的内存。内存的基本释放原则是:谁申请,谁释放。但是当我们在对象之间传递和共享数据时,会出现以下问题:
(1)假如对象A生成了对象T,需要调用对象B的一个方法,将对象T作为参数进行传递,这样对象B就会对T持有一个引用。如果没有引用计数,则按照“谁申请谁释放”的原则进行内存管理,那么对象A就需要在对象B不需要对象T时,对T进行释放。但是对象A不知道对象B会将T持有到什么时候,这样什么时候去释放T就是一个问题,如下图所示:
解决方法之一:每次A调用完对象B的方法后,对象B将参数进行拷贝,然后对像A就可以销毁对象T。但是这会带来很多内存申请,影响性能,不宜采用。
解决方法之二:对象A生成对象T后,始终不销毁对像T,把管理权交给B,让B来销毁。但是这种方法依赖与A、B两个对象的配合,在对象A中申请,在对象B中释放,管理起来很繁琐,而且当T在多个对象之间传递时(例如对象B将T传递给对象C),很容易出错。所以这种方法也不宜采用。
于是就引入了引用计数。对象T在多个对象之间传递过程中,哪些对象对它进行持有,就把它的引用计数加1。当不再持有它时,把它的引用计数减1。当对象T的引用计数为0时,说明此时没有对象在持有它,可以释放,回收其内存。
二、内存管理关键字
Objective-C通过引用计数方式来管理内存,调用实例对象的 release
方法后,引用计数减1,当实例对象的引用计数为0时,对象自动调用 dealloc
方法,尽行内存回收。如果需要在对象释放前进行其他操作,我们可以重写对象的 dealloc
方法。
Objective-C内存管理API主要通过以下几个接口进行操作:
1.alloc,allocWithZone,new
这三个接口的作用是为对象分配内存,引用计数(retainCount)为1,并返回此实例。
1 | - (void)viewDidLoad { |
对于
allocWithZone:
方法,它的调用基本上和alloc
方法是一样的。官方文档这样描述“This method exists for historical reasons; memory zones are no longer used by Objective-C”.它的存在是由于历史原因造成的。我们只需要知道alloc
方法的实现调用了allocWithZone
方法。
2.release
使实例对象的引用计数(retainCount)减1,减到0时调用此对象的 dealloc
方法释放内存。
1 | - (void)viewDidLoad { |
3.retain
使实例对象的引用计数(retainCount)加1。
1 | - (void)viewDidLoad { |
4.copy,mutableCopy
复制一个对象实例,得到的对象实例的引用计数为1。这个对象是与上下文无关的,独立的对象。
1 | - (void)viewDidLoad { |
5.autorelease
在当前上下文的AutoreleasePool栈顶的autoreleasePool实例添加此对象,由于它的引入使Objective-C(非GC管理环境)由全手动内存管理上升到半自动化。即对于retainCount = 1的对象,在自动释放池(autoreleasePool)释放时会自动释放,不需要再去手动调用[obj release]。对于autorelease的原理深度剖析,@阳神有一篇黑幕背后的Autorelease讲解的很详细。关于autorelease的使用,后面会讲到。
三、MRC中定义setter方法
如果一个类的成员变量是另外一个如果一个类中的成员变量是另外一个类,当使用setter方法传递对象时,传递的是指针,指向的还是同一个类。如果另外一个类在传递给setter后,又在另外的地方进行了release,那么再次调用这个类成员变量的getter方法时,就会获取一个野指针,所以为了解决这种问题,需要 这样定义setter方法:
1、先判断传递近来的对象跟现有的对象是不是同一个,如果是同一个就没有必须要再进行引用;
2、如果不是同一个对象,就得要先将原有得对象进行release,在将新的对象进行引用并retain这个对象。
3、重写dealloc方法,把对象类型进行一次release。
1 |
|
四、自动释放池:Autoreleasepool
AutoreleasePool(自动释放池)使Objective-C成为内存管理半自动化语言。“自动释放池”顾名思义就是一个容器。它的基本使用如下:
1 | - (void)releaseObj { |
如上所示,包含在 pool
和 [pool release]
之间的对象,凡是调用 autorelease
方法的实例,都被加入了“池子”,即obj1和obj2两个调用性质一样。
AutoreleasePool的基本原理是这样:在声明 pool
和 [pool release]
之间生成的实例对象,每有一个实例对象调用 autorelease
方法,都会在pool
实例中添1次此实例要回收的记录以做备案。当此pool
实例dealloc时,首先会检查之前备案的所有实例,所有记录在案的实例都会依次调用它的release
方法。
调用autorelease只是使对象延迟释放,并不是立刻释放。如在上述代码中的obj2,调用了autorelease
方法,但依然未释放,还可以使用,等到[pool release]
才释放。
如果实例对象的retainCount=2,那么调用一次autorelease
方法是不能使对象在最终释放的,例如obj3,最终无法释放。如要释放,需要调用和retainCount相同次数autorelease
方法,如obj4。
如果AutoleasePool被嵌套调用,起作用的是最里层的pool,例如:
1 | - (void)releaseObj { |
起作用的是pool3。
对于如何使用AutoreleasePool优化内存,后面的篇章会讲到。
基于MRC(手动引用计数)的内存管理就暂时说到这里,下一篇会主要写基于ARC(自动引用计数)的内存管理。