上一篇文章中,介绍了内存的概述与iOS系统中内存
分布,让大家对内存的概念稍微有点“敏感”,虽然现在大部分iOS项目开发都是基于ARC的,但是对于MRC的理解也很重要。下面主要介绍基于MRC环境下的内存管理。

一、引用计数

iOS内存管理采用的引用计数器原理,那么什么是引用计数?原理是什么?为什么要使用引用计数?iOS大V@唐巧在《iOS开发进阶》一书中做了介绍。我在这里简单描述一下。

1.引用计数原理

“引用计数器”可以简单理解为对象的一个标识属性,它标识着对象被多少个对象引用(持有)。当我们创建一个对象时,它的引用计数为1,当一个新的指针指向这个对象时,引用计数加1;当指针指针不再指向(持有)这个对象时,引用计数减1。当对象的引用计数为0时,说明这个对象不再被任何指针指向,这时就可以销毁对象,回收对象所占用的内存。

引用计数原理图解如下:
countImg
当然,还有一种其他比较容易理解的解释场景:员工在办公室使用灯的情景,具体大家可以去这篇博文去看,在此不再阐述。

这样说或与有一些抽象,为了形象一下,下面用代码进行简单阐述:

:在运行这段程序时,我们需要手动关闭ARC,方法如图所示:
CancelARC

示例程序:

1
2
3
4
5
6
7
8
9
10
11
- (void)viewDidLoad {
[super viewDidLoad];

NSObject *obj1 = [[NSObject alloc] init]; //对像创建,引用计数=1
NSLog(@"Reference count = %lu",(unsigned long)[obj1 retainCount]);
NSObject *obj2 = [obj1 retain]; //obj2持有对象,引用计数加1
NSLog(@"Reference count = %lu",(unsigned long)[obj1 retainCount]);
[obj2 release]; //obj2释放,引用计数减1
NSLog(@"Reference count = %lu",(unsigned long)[obj1 retainCount]);
[obj1 release]; //obj1释放,引用计数=1,对象释放
}

运行结果:

1
2
3
2016-03-28 16:39:52.035 MemoryTest[69135:40583765] Reference count = 1
2016-03-28 16:39:52.035 MemoryTest[69135:40583765] Reference count = 2
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#   define ISA_MASK        0x00000001fffffff8ULL
# define ISA_MAGIC_MASK 0x000003fe00000001ULL
# define ISA_MAGIC_VALUE 0x000001a400000001ULL
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 30;
uintptr_t magic : 9;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};

针对上面的指针结构,下面是每个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_objectrootRetainCount() 方法。

1
2
3
- (NSUInteger)retainCount {
return ((id)self)->rootRetainCount();
}

(2)在ARC环境中。可以使用Core Foundation 库的CFGetRetainCount() 方法:

1
2
3
4
5
6
7
8
9
10
- (void)viewDidLoad {
[super viewDidLoad];

NSObject * obj = [[NSObject alloc] init];

CFTypeRef cfObjc = (__bridge CFTypeRef)obj; //转换为core foundation类型对象
NSInteger count = CFGetRetainCount(cfObjc);

NSLog(@"Reference count = %ld",(long)count);
}

还可以使用 Runtime 的 _objc_rootRetainCount(id obj) 方法:

:需要引入 <objc/runtime.h>

1
2
3
4
5
6
7
8
- (void)viewDidLoad {
[super viewDidLoad];

extern uintptr_t _objc_rootRetainCount(id obj);

NSObject * obj = [[NSObject alloc] init];
NSLog(@"%lu", _objc_rootRetainCount(obj));
}

_objc_rootRetainCount()方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
inline uintptr_t 
objc_object::rootRetainCount()
{
assert(!UseGC);
if (isTaggedPointer()) return (uintptr_t)this; //如果isa为优化指针,即使用Tagged Pointer

sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
if (bits.indexed) {
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}

sidetable_unlock();
return sidetable_retainCount(); //如果19bit不能存储引用计数,通过调用sidetable_retainCount方法获取
}

由以上程序可以看出,当对象使用 isa 优化指针时,直接返回对想引用计数;64为环境优化的 isa 无法使用时,调用 seidtable_retainCount方法。至于调用 seidtable_retainCount方法的过程,暂不做分析。

4.为什么要使用引用计数

使用引用计数,主要是为了方便我们管理对象的内存。内存的基本释放原则是:谁申请,谁释放。但是当我们在对象之间传递和共享数据时,会出现以下问题:
(1)假如对象A生成了对象T,需要调用对象B的一个方法,将对象T作为参数进行传递,这样对象B就会对T持有一个引用。如果没有引用计数,则按照“谁申请谁释放”的原则进行内存管理,那么对象A就需要在对象B不需要对象T时,对T进行释放。但是对象A不知道对象B会将T持有到什么时候,这样什么时候去释放T就是一个问题,如下图所示:
releaseImg

解决方法之一:每次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
2
3
4
5
6
- (void)viewDidLoad {
[super viewDidLoad];

NSObject *obj = [[NSObject alloc] init];
NSObject *obj2 = [NSObject new]; //自动初始化对象,作用同上。
}

对于 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
2
3
4
5
6
- (void)viewDidLoad {
[super viewDidLoad];

NSObject *obj = [[NSObject alloc] init];
[obj release];
}

3.retain

使实例对象的引用计数(retainCount)加1。

1
2
3
4
5
6
7
- (void)viewDidLoad {
[super viewDidLoad];

NSObject *obj = [[NSObject alloc] init];
[obj retain]; //引用计数 = 2
NSObject *anotherObj = [obj retain]; //引用计数 = 3
}

4.copy,mutableCopy

复制一个对象实例,得到的对象实例的引用计数为1。这个对象是与上下文无关的,独立的对象。

1
2
3
4
5
6
7
8
9
- (void)viewDidLoad {
[super viewDidLoad];

NSArray *arr = [[NSArray alloc] init];
NSMutableArray *mutableArr = [[NSMutableArray alloc] init];

NSArray *copyArr = [arr copy]; //独立对象,指向地址与arr不同
NSMutableArray *mutableCopyArr = [mutableArr mutableCopy];
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#import "ViewController.h"
#import "Person.h"

@interface ViewController (){
Person *_person;
}

@property (nonatomic,strong) Person *person;

-(void)setPerson:(Person *)person;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
Person *newPer = [Person new];
self.person = newPer;
}

- (void)setPerson:(Person *)person{
if (_person != person) { //判断当前成员变量和传入变量是否相同
[_person release]; //如果不相同,释放之前的对象
_person = [person retain]; //重新持有新的对象

/*
如果仅是简单的_person = person;那么当传入的per变量在某一时刻调用[person release]后,_person则会成为一个野指针。
*/

}
}

- (void)dealloc{
[super dealloc]; //使父类中一些成员变量能够释放
[_person release];
// 另有一种推荐的做法是:[person setPerson:nil];
}
@end

四、自动释放池:Autoreleasepool

AutoreleasePool(自动释放池)使Objective-C成为内存管理半自动化语言。“自动释放池”顾名思义就是一个容器。它的基本使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)releaseObj {

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

NSObject *obj1 = [[[NSObject alloc] init] autorelease];

NSObject *obj2 = [[NSObject alloc] init];
[obj2 autorelease];
NSLog(@"%lu",(unsigned long)[obj2 retainCount]); //retainCount = 1

NSObject *obj3 = [[NSObject alloc] init];
[obj3 retain];
[obj3 autorelease];

NSObject *obj4 = [[NSObject alloc] init];
[obj4 retain];
[obj4 autorelease];
[obj4 autorelease];

[pool release];
}

如上所示,包含在 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
2
3
4
5
6
7
8
9
10
11
12
- (void)releaseObj {

NSAutoreleasePool *pool1 = [[NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool2 = [[NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool3 = [[NSAutoreleasePool alloc] init];

NSObject *obj = [[[NSObject alloc] init] autorelease];

[pool3 release];
[pool2 release];
[pool1 release];
}

起作用的是pool3。

对于如何使用AutoreleasePool优化内存,后面的篇章会讲到。

基于MRC(手动引用计数)的内存管理就暂时说到这里,下一篇会主要写基于ARC(自动引用计数)的内存管理。

参考

1.《iOS开发进阶》
2.Objective-C 引用计数原理
3.Objective-C 内存管理精髓