【iOS】OC底层系列十七 - 内存管理-MRC

面试题1:讲一下你对 iOS 内存管理的理解(MRC和ARC)。

面试题2:ARC 都帮我们做了什么?(ARC是LLVM编译器(自动添加retain和release)和Runtime系统(自动清空弱引用表)相互协作的一个结果)。

面试题3:weak指针的实现原理(弱引用存放在哈希表中,当调用对象的dealloc方法时,会清空当前对象的弱引用表)。

面试题4:autorelease对象在什么时机会被调用release?(Runloop某次循环中或休眠前)

面试题5:方法里有局部对象, 出了方法后会立即释放吗?(如果添加了autorelease不会立即释放,出方法后会在Runloop的某次循环中或休眠前释放;如果是ARC,出方法后会立即释放)

一、引用计数

在iOS中,使用引用计数来管理OC对象的内存。

一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间。

调用retain方法会让OC对象的引用计数+1,调用release方法会让OC对象的引用计数-1。

内存管理的经验总结:

  • 调用allocnewcopymutableCopy方法会返回一个对象,在不需要这个对象时,要调用release或者autorelease来释放它。
  • 想拥有某个对象,就让它的引用计数+1。
  • 不想再拥有某个对象,就让它的引用计数-1。

在Xcode中,默认需要手动关闭ARC:

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
@implementation DBPerson

- (void)dealloc {
[super dealloc];

NSLog(@"%s", __func__);
}

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
DBPerson *person = [[DBPerson alloc] init];
NSLog(@"%zd", person.retainCount);
[person release];
NSLog(@"%zd", person.retainCount);
}
return 0;
}

/*
输出:
1
-[DBPerson dealloc]
0
*/

对象在调用release后,如果retainCount为0就会调用dealloc方法,最后被释放。

手动调用release比较繁琐,而且很容易产生内存泄漏(该释放的对象没有释放),因此也可以使用autorelease,让对象自动在恰当的时机自动释放(参考本章节的第四部分)。

1
DBPerson *person = [[[DBPerson alloc] init] autorelease];

基本数据类型不需要内存管理。更多的MRC操作请查阅相关资料。

引用计数是怎么存储的?

在64bit中,引用计数可以直接存储在优化过的isa指针中(参考前面介绍isa结构体的章节),也可能存储在SideTable类中:

1
2
3
4
5
struct SideTable {
spinlock_t slock;
RefcountMap refcnts; // 引用计数表
weak_table_t weak_table; // 弱引用表
};

refcnts是一个存放着对象引用计数的散列表(key是当前对象,value是引用计数的值)。

二、深浅拷贝

copy的目的:产生一个副本对象,跟源对象互不影响(修改了源对象,不影响副本对象;修改了副本对象,不影响源对象)。

iOS提供了2个copy方法:

  • copy:不可变拷贝,产生不可变副本。
  • mutableCopy:可变拷贝,产生可变副本。

深拷贝:内容拷贝,产生新的对象。

浅拷贝:指针拷贝,没有产生新的对象。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NSString *str1 = [[NSString alloc] initWithFormat:@"abc"];
NSString *str2 = [str1 copy]; // 这里相当于是对str1的引用计数+1
NSMutableString *str3 = [str1 mutableCopy];
NSLog(@"\nstr1:%p(%@)-%@\nstr2:%p(%@)-%@\nstr3:%p(%@)-%@", str1, [str1 class], str1, str2, [str2 class], str2, str3, [str3 class], str3);
/*
输出:
str1:0xe6f5e86a6acd7c57(NSTaggedPointerString)-abc
str2:0xe6f5e86a6acd7c57(NSTaggedPointerString)-abc
str3:0x100634410(__NSCFString)-abc
*/

NSMutableString *str4 = [[NSMutableString alloc] initWithFormat:@"abc"];
NSString *str5 = [str4 copy];
NSMutableString *str6 = [str4 mutableCopy];

NSLog(@"\nstr4:%p(%@)-%@\nstr5:%p(%@)-%@\nstr6:%p(%@)-%@", str4, [str4 class], str4, str5, [str5 class], str5, str6, [str6 class], str6);
/*
输出:
str4:0x10073f050(__NSCFString)-abc
str5:0xe6f5e86a6acd7c57(NSTaggedPointerString)-abc
str6:0x100741b90(__NSCFString)-abc
*/

平时开发中,我们习惯把声明字符串属性时使用copy修饰,为什么呢?

1
@property (nonatomic, copy) NSString *name;

上面的属性name使用copy修饰后,setter方法内部其实是这样的:

1
2
3
4
5
6
- (void)setName:(NSString *)name {
if (_name != name) {
[_name release];
_name = [name copy]; // 如果使用retain修饰,就是[name retain];
}
}

不可变或可变对象进行copy返回的是不可变对象,所以字符串使用copy修饰后,外面传入的对象和成员变量指向的是同一个内存地址,这样也能保证当前属性存储的数据可维护性更高。

面试题:下面代码能否正常运行?为什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface DBPerson : NSObject

@property (copy, nonatomic) NSMutableArray *data;

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
DBPerson *person = [[DBPerson alloc] init];
person.data = [NSMutableArray array];
[person.data addObject:@"1"];
[person.data addObject:@"2"];
}
return 0;
}

上面的代码编译时期没有问题,但是运行时会崩溃报错,因为使用copy修饰的属性,在setter方法内部会让成员变量指向copy返回的对象,由于返回的是不可变对象(copy后返回的是NSArray类型),所以调用NSMutableArray的方法会报错。

Foundation框架中类似NSString的还有NSDictionary、NSMutableDictionary、NSArray、NSMutableArray、NSData、NSMutableData、NSSet、NSMutableSet等等。建议声明属性时,如果是字符串就使用copy,其他对象类型就使用strong

自定义对象不能直接调用copymutableCopy把源对象的属性都拷贝一份呢?

不可以,需要遵守NSCopying协议,并实现copyWithZone:协议,或遵守NSMutableCopying并实现mutableCopyWithZone:协议。

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
38
@interface DBPerson : NSObject <NSCopying>

@property (assign, nonatomic) int age;
@property (assign, nonatomic) double weight;

@end

@implementation DBPerson

- (id)copyWithZone:(NSZone *)zone {
// 使用分配的空间创建一个新的对象
DBPerson *person = [[DBPerson allocWithZone:zone] init];
// 把需要copy的属性值赋给新对象的属性
person.age = self.age;
return person;
}

- (NSString *)description {
return [NSString stringWithFormat:@"age = %d, weight = %f", self.age, self.weight];
}

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
DBPerson *person1 = [[DBPerson alloc] init];
person1.age = 10;
person1.weight = 20;

DBPerson *person2 = [person1 copy];
person2.age = 30;

NSLog(@"%@", person1); // 输出:age = 10, weight = 20.000000
NSLog(@"%@", person2); // 输出:age = 30, weight = 0.000000
}
return 0;
}

三、weak指针

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
@interface DBPerson : NSObject

@end

@implementation DBPerson

- (void)dealloc {
NSLog(@"%s", __func__);
}

@end

- (void)viewDidLoad {
[super viewDidLoad];

__strong DBPerson *person1;
__weak DBPerson *person2;
__unsafe_unretained DBPerson *person3;

NSLog(@"111");
{
DBPerson *person = [[DBPerson alloc] init];

person1 = person;
}
NSLog(@"222 - %@", person1);
}

/*
输出:
111
222 - <DBPerson: 0x6000034d8370>
-[DBPerson dealloc]
*/

使用__strong修饰的强指针person1person进行了强引用,所以person对象在离开viewDidLoad方法前不会被释放。

如果使用__weak修饰的弱指针person2person进行弱引用,在person离开作用域前会被释放,person2也会被置为nil。最终的输出结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSLog(@"111");
{
DBPerson *person = [[DBPerson alloc] init];

person2 = person;
}
NSLog(@"222 - %@", person2);

/*
输出:
111
-[MJPerson dealloc]
222 - (null)
*/

如果使用__unsafe_unretained修饰的不安全弱指针person3person进行弱引用,在person离开作用域前会被释放,person3不会被置为nil,因此释放后访问person3会报野指针错误(person3指向的对象已经被释放,但还是指向那块内存地址)。

1
2
3
4
5
6
7
8
9
10
11
12
13
NSLog(@"111");
{
DBPerson *person = [[DBPerson alloc] init];

person3 = person;
}
NSLog(@"222 - %@", person3);

/*
输出:
111
-[MJPerson dealloc]
*/

_weak__unsafe_unretained安全,所以建议使用__weak

__weak修饰的指针是怎么做到当对象销毁时__weak指针被置为nil的?

当一个对象要释放时,会自动调用dealloc__weak指针是在对象执行dealloc后被置为nil的),接下来的调用轨迹是:

1
2
3
4
5
dealloc
_objc_rootDealloc
rootDealloc
object_dispose
objc_destructInstance、free

查看objc的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
void *objc_destructInstance(id obj) {
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor(); // 是否有c++析构函数
bool assoc = obj->hasAssociatedObjects(); // 是否有关联对象

// This order is important.
if (cxx) object_cxxDestruct(obj); // 清除成员变量
if (assoc) _object_remove_assocations(obj); // 移除关联对象
obj->clearDeallocating(); // 将指向当前对象的弱指针置为nil(重点)
}
return obj;
}

clearDeallocating()内部会根据当前对象的内存地址,通过哈希查找方式到SideTable中清除refcnts

四、自动释放池

示例代码(MRC):

1
2
3
4
5
6
7
int main(int argc, const char * argv[]) {
@autoreleasepool {
DBPerson *person = [[[DBPerson alloc] init] autorelease];
}
return 0;
}

上面代码中,person对象加了autorelease后,会在离开最近的大括号前自动释放,在大括号内可以安全使用person。那么person对象究竟是在什么时候释放呢?

把上面的代码转为C++:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct __AtAutoreleasePool {
__AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}

~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}

void * atautoreleasepoolobj;
};

int main(int argc, const char * argv[]) {
/* @autoreleasepool */
{
__AtAutoreleasePool __autoreleasepool;
DBPerson *person = ((DBPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((DBPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((DBPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("DBPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
}
return 0;
}

创建person对象的代码被自动释放池的构造函数和析构函数所包围(以下为简化的伪代码):

1
2
3
4
5
6
7
{
atautoreleasepoolobj = objc_autoreleasePoolPush();

DBPerson *person = [[[DBPerson alloc] init] autorelease];

objc_autoreleasePoolPop(atautoreleasepoolobj);
}

因此我们只需要研究autoreleasepool内部的逻辑就可以知道对象时如何被释放的。

自动释放池的主要底层数据结构是:__AtAutoreleasePoolAutoreleasePoolPage

调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的。

源码分析(objc源码中NSObject.mm):clang重写@autoreleasepool

1
2
3
4
5
6
7
8
9
class AutoreleasePoolPage {
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}

AutoreleasePoolPage的结构:

每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址,所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。

begin():从AutoreleasePoolPage的哪个位置开始存储autorelease对象的地址。

1
2
3
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this)); // AutoreleasePoolPage的开始地址 + AutoreleasePoolPage成员变量占用的内存大小(成员变量共占用50个字节)
}

end():返回AutoreleasePoolPage的结束地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id * end() {
return (id *) ((uint8_t *)this+SIZE); // AutoreleasePoolPage的开始地址 + AutoreleasePoolPage的内存大小
}

static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif

#define PAGE_MAX_SIZE PAGE_SIZE

#define PAGE_SIZE I386_PGBYTES

#define I386_PGBYTES 4096

child:指向下一个AutoreleasePoolPage节点,如果是最后一个节点,child指向nil。

parent:指向上一个AutoreleasePoolPage节点,如果是第一个节点,parent指向nil。

id *next:指向下一个能存放autorelease对象地址的区域。

调用objc_autoreleasePoolPush方法会将一个POOL_BOUNDARY入栈(放到AutoreleasePoolPage中),并且返回其存放的内存地址(这一步其实就是设置本次autorelease的栈底边界)。

1
#define POOL_BOUNDARY nil

只要对象调用了autorelease,就会自动把这个对象内存地址入栈(放到AutoreleasePoolPage中)。

调用objc_autoreleasePoolPop方法时传入一个POOL_BOUNDARY的内存地址(objc_autoreleasePoolPush返回的地址),会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY,本次autorelease释放结束。

可以通过以下私有函数来查看自动释放池的情况(这个是系统自带的私有函数,内部实现未暴露,我们只需要声明该函数就可以直接使用):

1
extern void _objc_autoreleasePoolPrint(void);

Runloop和Autorelease

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
// main.m
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
DBPerson *person = [[[DBPerson alloc] init] autorelease];
}

上面代码中,person对象调用了autorelease,那么person对象什么时候被释放呢?

在main函数的自动释放池结束后?不可能,只要程序不退出@autoreleasepool就不会退出,更不可能释放person对象。

其实iOS的main函数内部维护了一个主线程的Runloop,这个Runloop中默认注册了2个Observer(_wrapRunLoopWithAutoreleasePoolHandler):

  • 第1个Observer:

    • 监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
  • 第2个Observer:

    • 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()objc_autoreleasePoolPush()
    • 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

也就是说person对象什么时候释放是由Runloop控制的(在Runloop某次循环中或休眠前释放)。