OC学习03:内存管理

张建 lol

前言

本文主要介绍 内存的五大区函数栈内存管理

内存五大区

在iOS中,内存主要分为 栈区、堆区、全局区、常量区、代码区 五个区域,如下图所示:

栈区

定义

  • 栈是 系统数据结构,其对应的 进程或者线程是唯一的

  • 栈是 向低地址扩展 的数据结构

  • 栈是一块 连续的内存区域,遵循 先进后出(FILO) 原则

  • 栈区一般在 运行时 分配

存储

栈区是由 编译器自动分配并释放的,主要用来存储

  • 局部变量

  • 函数的参数,例如函数的隐藏参数(id self, SEL _cmd)

优缺点

  • 优点:因为栈是由 编译器自动分配并释放 的,不会产生内存碎片,所以 快速高效

  • 确定:栈的 内存大小有限制,数据不灵活

    • iOS主线程大小是1MB
    • 其他线程是 512KB
    • MAC 只有 8MB

以上内存大小的说明,在Threading Programming Guide 中有相关说明,如下图:

堆区

定义

  • 堆是 向高地址扩展 的数据结构

  • 堆是 不连续的内存区域,类似于 链表结构(便于增删,不便于查询),遵循 先进先出(FIFO)原则

  • 堆的 地址空间 在iOS中是是动态的

  • 堆区的分配一般是以在 运行时分配

存储

堆区是 由程序员动态分配和释放 的,如果程序员不释放,程序结束后,可能由操作系统回收,主要用于存放:

  • OC 中使用 allocnew 开辟空间创建 对象 ,或者 block经过copy后

  • C 语言中使用 malloc、calloc、realloc 分配的空间,需要 free 释放

  • 一般一个 new/alloc 就要对应一个 release,在 ARC 下编译器会自动在合适位置为 OC 对象添加 release 操作,会在当前 线程Runloop退出或休眠时销毁这些对象MRC 则需程序员手动释放。

优缺点

  • 优点:灵活方便,数据适应面广泛

  • 缺点:需 手动管理、速度慢,容易产生内存碎片

当需要访问堆中数据时,一般需要 先通过对象读取到栈区的指针地址,然后通过 指针地址访问堆区

全局区(静态区,即.bss & .data)

全局区是 编译时分配 的内存空间,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放,主要存放:

  • 未初始化的全局变量和静态变量,即BSS区(.bss)

  • 已初始化的全局变量和静态变量,即DATA区(.data)

其中,全局变量 是指变量值可以在 运行时被动态修改,而 静态变量static 修饰的变量,包含 静态局部变量静态全局变量

常量区(即.rodata)

常量区是 编译时分配 的内存空间,在 程序结束后由系统释放,主要存放:

  • 已经使用了的,且没有指向的 字符串常量

字符串常量因为可能在程序中被多次使用,所以在程序运行之前就会提前分配内存

代码区(即.text)

代码区是 由编译时分配,主要用于存放 程序运行时的代码,代码会被编译成 二进制存进内存

内存五大区验证

运行下面的一段代码,看看变量在内存中是如何分配的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int a = 10; // 全局区(已初始化的全局变量)
char * b; // 全局区(未初始化的全局变量)
- (void)test{
NSInteger i = 123; // 栈区(局部变量)
NSLog(@"i的内存地址:%p", &i);

NSString *string = @"ZJ"; // 常量区(字符串常量)
NSLog(@"string的内存地址:%p", string);
NSLog(@"&string的内存地址:%p", &string);

NSObject *obj = [[NSObject alloc] init]; // 堆区(alloc对象)
NSLog(@"obj的内存地址:%p", obj);
NSLog(@"&obj的内存地址:%p", &obj);
}

运行结果如下:

1
2
3
4
5
2022-03-11 14:34:25.438913+0800 内存五大区[70321:4340509] i的内存地址:0x16f6f5a18
2022-03-11 14:34:25.438976+0800 内存五大区[70321:4340509] string的内存地址:0x100710098
2022-03-11 14:34:25.438997+0800 内存五大区[70321:4340509] &string的内存地址:0x16f6f5a10
2022-03-11 14:34:25.439014+0800 内存五大区[70321:4340509] obj的内存地址:0x280fa0bc0
2022-03-11 14:34:25.439031+0800 内存五大区[70321:4340509] &obj的内存地址:0x16f6f5a08
  • 对于 局部变量i, 存放在栈区

  • 对于 字符串对象string,分别打印了 string得对象地址string对象的指针地址

    • string的 对象地址 是是存放在 常量区
    • string 对象的指针地址,是存放在 栈区
  • 对于 alloc创建的对象obj,分别打印了 obj得对象地址obj对象的指针地址

    • obj的 对象地址 是存放在 堆区
    • obj 对象的指针地址 是存放在 栈区

函数栈

  • 函数栈 又称为 栈区,在内存中从高地址往低地址分配,与堆区相对,具体图示请看上面

  • 栈帧 是指 函数(运行中且未完成)占用的一块独立的连续内存区域

  • 应用中新创建的 每个线程都有专用的栈空间,栈可以在线程期间自由使用,而线程中有千千万万的函数调用,这些函数 共享 进程的这个 栈空间每个函数所使用的栈空间是一个栈帧,所有栈帧就组成了这个线程完成的栈

  • 函数调用是发生在栈上 的,每个 函数的相关信息(例如局部变量、调用记录等)都 存储在一个栈帧 中,每执行一次 函数调用,就会生成一个与其相关的栈帧,然后将其 栈帧压入函数栈,而当函数 执行结束,则将此函数对应的 栈帧出栈并释放掉

如下图所示,是经典图- ARM的栈帧布局方式

  • 其中 main stack frame 为调用函数的栈帧

  • func1 stack frame 为当前 当前函数(被调用者)的栈帧

  • 栈底 地址,栈向下增长

  • FP 就是 栈基址,它指向函数的 栈帧起始地址

  • SP 则是函数的 栈指针,它指向 栈顶 的位置

  • ARM压栈顺序 很是规则(也比较容易被黑客攻破),依次为 当前函数指针PC返回指针LR栈指针SP栈基址FP传入参数个数及指针本地变量临时变量。如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数

  • ARM 也可以 用栈基址和栈指针明确标示栈帧的位置,栈指针SP一直移动,ARM的特点是,两个栈空间内的地址(SP+FP)前面,必然有两个代码地址(PC+LP)明确标示着调用函数位置内的某个地址

堆栈溢出

一般情况下应用程序是不需要考虑堆和栈的大小的,但是事实上堆和栈不是无上限的,过多的递归会导致栈溢出过多的alloc变量会导致堆溢出

所以 预防堆栈溢出 的方法:

  • 避免层次过深递归 调用

  • 不要使用过多的局部变量,控制局部变量的大小

  • 避免分配 占用空间 太大的对象,并 及时释放

  • 实在不行,适当的情景下 调用系统API修改线程的堆栈大小

栈帧示例

描述下面代码的栈帧变化

栈帧程序示例

1
2
3
4
5
6
7
8
9
10
11
int Add(int x,int y) {
int z = 0;
z = x + y;
return z;
}

int main() {
int a = 10;
int b = 20;
int ret = Add(a, b);
}

程序执行时,栈区中栈帧的变化如下图所示:

内存管理

概述

  • 在iOS中开发中,我们或多或少都听说过内存管理。iOS的内存管理一般指的是OC对象的内存管理,因为OC对象分配在堆内存,堆内存需要程序员自己去动态分配和回收;基础数据类型(非OC对象)则分配在栈内存中,超过作用域就会由系统检测回收。如果我们在开发过程中,对内存管理得不到位,就有可能造成内存泄露。

  • 我们通常讲的内存管理,实际上从发展的角度来说,分为两个阶段:MRC和ARC

    • MRC指的是 手动内存管理,在开发过程中 需要开发者手动去编写内存管理的代码

    • ARC指的是 自动内存管理,在此内存管理模式下由 LLVM编译器和OC运行时库生成相应内存管理的代码

引用计数

  • OC 中,使用 引用计数进行内存管理

  • 每个对象都有一个与其相对应的引用计数器,当持有一个对象,这个对象的引用计数就会递增;当这个对象的某个持有被释放,这个对象的引用计数就会递减。当这个对象的引用计数变为0,那么这个对象就会被系统回收。

  • 当一个对象使用完没有释放,此时其引用计数永远大于1。该对象就会一直占用其分配在堆内存的空间,就会导致内存泄露。内存泄露到一定程度有可能导致内存溢出,进而导致程序崩溃。

MRC

  1. 简介

全称 Manual Reference Counting,管理通过使用 retain, release, 以及 autorelease 的消息发送来实现。

  • retain: 持有(拥有)对象,对象引用数加 1
  • release: 释放对象,对象引用数减 1
  • autorelease: 通知系统,在 @autoreleasepool 代码块结束时,对对象调用 release
  1. 管理原则
  • 自己创建的对象,自己获得拥有权

在苹果规定中,使用 alloc/new/copy/mutableCopy 创建返回的对象归调用者所有,例如以下

1
2
3
4
/* NSMutableArray类对象A */
NSMutableArray *array = [[NSMutableArray alloc] init];
NSLog(@"%p", array);
[array release]; // 释放

由于对象 Aalloc 生成,符合苹果规定,指针变量(*array) 指向并持有 [[NSMutableArray alloc] init]创建的 对象A,引用计数器会加 1。另外,array在使用完 对象A 后需要对其进行释放。当调用 release 后,释放了其对对象A的引用,计数器减1。对象A此时引用计数值为零,所以对象A被回收。不能访问已经被回收的对象,会发生崩溃。

  • 别人创建的对象,可以通过 retain 来获得拥有权
1
2
3
4
5
6
7
8
9
// 例如已有 fooArray, 通过 array 方法获得其引用
NSArray *bar = [fooArray array];

// 不可以直接调用 release,因为没有拥有权
// [bar release];

// 需要先 retain 来获得拥有权,然后才能释放
[bar retain];
[bar release];
  • 你所拥有的对象不再需要使用时,必须将其释放

  • 不能释放你不拥有的对象

  1. retain
  • retain和属性

我们可以通过属性来保存对象,如果一个属性为强引用,我们就可以通过属性的实例变量和存取方法来对某个对象进行操作,例如某个属性的setter方法如下:

1
2
3
4
5
6
7
8
9
10
- (void)setPerson:(Person *)person {

[person retain];

[_person release];

_person = person;

}

我们通过 retain新值release旧值,再给实例变量更新值。

需要注意的一点是:需要先retain新值,再release旧值。因为如果新旧值是同一个对象的话,先release就有可能导致该对象被系统回收,再去retain就没有任何意义了。例如下面这个例子:

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
#import "ViewController.h"
#import "Person.h"

@interface ViewController ()
@property (nonatomic, strong)Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];

// 实例变量持有Person类对象(P对象)。这样赋值不会调用set方法
_person = [[Person alloc] init];

// 调用set方法
self.person = _person;

}

- (void)setPerson:(Person *)person {
// release释放对P对象的引用,P对象引用计数值变为零,则P对象被系统回收
[_person release];

// 由于P对象已经被回收,再去retain就容易出问题
[person retain];

_person = person;
}

@end

由于P对象被回收,对应其所分配的内存被置于 可用内存池 中。如果该内存未被覆写,那么P对象依然有效;如果内存被覆写,那么实例变量_person就会指向一个被覆写的未知对象的指针,那么实例变量就变成一个 悬挂指针

  • retain和数组

如果我们把一个对象加入到一个数组中,那么该数组的addObject方法会对该对象调用retain方法。例如以下代码:

1
2
3
4
5
6
7
8
// person获得并持有P对象,P对象引用计数为1
Person *person = [[Person alloc] init]; // Person类对象生成的P对象

NSMutableArray *array = [NSMutableArray array];

// person被加入到数组,对象P引用计数值为2
[array addObject:person];

此时,对象P被person和array两个变量同时持有。

  1. release
  • 自己持有的对象自己释放

当我们持有一个对象,如果在不需要继续使用该对象,我们需要对其进行释放(release)。例如以下代码:

1
2
3
4
5
6
7
8
// array获得并持有NSArray类对象
NSArray *array = [[NSArray alloc] init];

// 当不再需要使用该对象时,需要释放
[array release];

// obj获得但不持有该对象
id obj = [NSArray array];
  • 非自己持有的对象不要释放

当我们不持有某个对象,却对该对象进行释放,应用程序就会崩溃。

1
2
3
4
5
6
7
8
// 获得并持有A对象
Person *p = [[Person alloc] init]; // Person类对象A

// 对象A引用计数为零,所以对象A被回收
[p release];

// 释放非自己持有的对象
[p release];

另外,我们也不能访问某个已经被释放的对象,该对象所占的堆空间如果被覆写就会发生崩溃的情况。

  1. autorelease

autorelease 指的是自动释放,当一个对象收到 autorelease 的时候,该 对象就会被注册到当前处于栈顶的自动释放池(autorelease pool)。如果没有主动生成自动释放池,则当前自动释放池对应的是 主运行循环的自动释放池。在当前线程的RunLoop进入 休眠前,就会对被注册到该自动释放池的所有对象进行一次 release 操作。

autorelease和release的区别是:

  • release:是马上释放对某个对象的强引用;

  • autorelease:是延迟释放某个对象的生命周期。

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
{
// 外部调用
Person *p = [Person person];
NSLog(@"%p", p); //使用无须retain

// 持有则需要retain
[p retain];
_person = p;
[_person release];

}

// Person类内部定义
+ (id)person {

//创建的Person类对象由person获得并持有
Person *person = [[Person alloc] init];

// [person release];

// 将 person 对象放入自动释放池
[person autorelease];

return person;
}

在外部调用,从方法名person知道,创建的对象由p指针变量获得但不持有。在函数内部,person获得并持有了Person类对象,所返回的person对象的引用计数加1。换句话说,调用者需要额外处理这多出来的一个持有操作。另外,我们不能在函数内部调用release,不然对象还没返回就已经被系统回收。这时候使用autorelease就能很好地解决这个问题。

只要把要返回的对象调用autorelease方法,注册到自动释放池就能延长person对象的生命周期,使其在 autorelease pool销毁(drain)前依然能够存活。

另外,person对象在返回时调用了 autorelease方法。该对象已经在自动释放池中,我们可以直接使用对象p,无须再通过[p retain]访问;不过,如果要用实例变量持有该对象,则需要对变量p进行一次retain操作,实例变量使用完该对象需要释放该对象。

  1. autorelease pool
  • autorelease poolRunLoop(运行循环)

当应用程序启动,系统默认会 开启一条线程,该线程就是 主线程。主线程有一个与之对应的自动释放池。每条线程都包含一个与其对应的自动释放池,当某条线程被终止的时候,对应该线程的自动释放池会被销毁。同时,处于该自动释放池的对象将会进行一次 release 操作。

,例如我们常见的 ARC 下的 main.h 文件:

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

该自动释放池用来释放在主线程下注册到该自动释放池的对象。

需要注意的是,当我们 开启一条子线程,并且在该线程 开启RunLoop 的时候,需要为其增加一个autorelease pool,这样有助于保证内存的安全。

  • autorelease pool和降低内存峰值

当我们执行一些复杂的操作,特别是如果这些复杂的操作要被循环执行,那么中间会免不了会产生一些临时变量。当被加到主线程自动释放池的对象越来越来多,却没有得到及时释放,就会导致内存溢出。这个时候,我们可以手动添加自动释放池来解决这个问题。如以下例子所示:

1
2
3
4
5
6
7
8
9
10
11
12
for (int i = 0; i < largeNumber; i++) {

// 创建自动释放池
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

// 产生许多被注册到自动释放池的临时对象
id obj = [Person personWithComplexOperation];

// 释放池中对象
[pool drain];

}

如上述例子所示,我们执行的循环次数是一个非常大的数字。并且调用personWithComplexOperation方法的过程中会产生许多临时对象,所产生的临时对象有可能会被注册到自动释放池中。我们通过手动生成一个自动释放池,并且在每次循环结束前把该自动释放池的对象执行release操作释放掉,这样就能有效地降低内存的峰值了。

ARC

  1. 概述
  • Automatic Reference Counting自动引用计数,即 ARCWWDC2011iOS5 所引入的最大的变革和最激动人心的变化。ARC是新的LLVM 3.0编译器的一项特性,使用ARC,可以说举解决了广大iOS开发者所憎恨的手动内存管理的麻烦。

  • 此处的 A 就是 automatic。其实 ARC 只是比 MRC 多了一步,就是在 编译时编译器自动帮开发者添加 retain, release 以及 autorelease 的调用,底层的内存管理机制还是和 MRC 一样。

  • ARC 模式下,我们通常在对象变量的声明里用 属性标记符 来指引 ARC 机制来管理我们的对象变量,它们是:strong, retain, weak, copy, assign。默认标记是 strong

  1. 标记符的区别
  • strong: 顾名思义,就是 强引用,对应 MRC 下的 retain,即引用数加 1
  • retain: 同 strong
  • weak: 弱引用,不增加引用数,引用的对象被释放后变为 nil
  • copy: 对对象进行 copy 后再赋值,因此对象必须遵循 NSCopying 协议。如:
1
2
3
@property(copy)Foo *foo;
...
self.foo = bar; // 相当于 self.foo = [bar copy];
  • assign: 一般用于原始数据类型(primitive type)的赋值。可以用于对象,效果相当于 weak,可是有一个坑是当对象被释放后,assign 属性的变量不会变成 nil,而是成为 野指针(dangling pointer),因此不建议使用在对象上。

借助以上的属性标记符,我们可以在对象声明的时候集中制定它们的内存管理策略,清晰明了。

  1. ARC的判断原则

ARC判断一个对象是否需要释放不是通过引用计数来进行判断的,而是通过强指针来进行判断的。那么什么是强指针?

  • 强指针

    • 默认所有对象的指针变量都是强指针
    • __strong 修饰的指针
1
2
Person *p1 = [[Person alloc] init];
__strong Person *p2 = [[Person alloc] init];
  • 弱指针

    • __weak 修饰的指针
1
__weak  Person *p = [[Person alloc] init];

ARC如何通过强指针来判断?

  • 只要还有一个强指针变量指向对象,对象就会保持在内存中
  1. ARC的使用
1
2
3
4
5
6
int main(int argc, const char * argv[]) {
// 不用写release, main函数执行完毕后p会被自动释放
Person *p = [[Person alloc] init];

return 0;
}
  1. ARC的注意点
  • 不允许调用对象的 release方法
  • 不允许调用 autorelease方法
  • 重写父类的dealloc方法时,不能再调用 [super dealloc];
  1. ARC下单对象内存管理
  • 局部变量释放对象随之被释放
1
2
3
4
5
6
7
8
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
} // 执行到这一行局部变量p释放
// 由于没有强指针指向对象, 所以对象也释放
return 0;
}

  • 清空指针对象随之被释放
1
2
3
4
5
6
7
8
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
p = nil; // 执行到这一行, 由于没有强指针指向对象, 所以对象被释放
}
return 0;
}

  • 默认清空所有指针都是强指针
1
2
3
4
5
6
7
8
9
int main(int argc, const char * argv[]) {
@autoreleasepool {
// p1和p2都是强指针
Person *p1 = [[Person alloc] init];
__strong Person *p2 = [[Person alloc] init];
}
return 0;
}

  • 弱指针需要明确说明

    • 注意: 千万不要使用弱指针保存新创建的对象
1
2
3
4
5
6
7
8
int main(int argc, const char * argv[]) {
@autoreleasepool {
// p是弱指针, 对象会被立即释放
__weak Person *p1 = [[Person alloc] init];
NSLog(@"%@",p);
}
return 0;
}

打印结果:

1
2020-07-31 18:02:51.021697+0800 iOS-OC之ARC[2134:984503] (null)
  1. ARC下多对象内存管理

ARC和MRC一样, 想拥有某个对象必须用强指针保存对象, 但是不需要在dealloc方法中release

1
2
3
4
5
6
7
@interface Person : NSObject
// MRC写法
//@property (nonatomic, retain) Dog *dog;

// ARC写法
@property (nonatomic, strong) Dog *dog;
@end

自动释放池

  1. 概述
  • AutoreleasePool(自动释放池)是 OC 中的一种 内存自动回收机制

  • 当向一个对象发送 autorelease 消息时,会将对象加入到自动释放池,这个对象不会立即释放,而是等到 runloop休眠或超出autoreleasepool作用域 之后进行 释放

  1. MRC 下使用自动释放池

在MRC环境中使用自动释放池需要用到 NSAutoreleasePool 对象,其生命周期就相当于C语言变量的作用域。对于所有调用过 autorelease方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。用源代码表示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// MRC环境下的测试:
// 第一步:生成并持有释放池NSAutoreleasePool对象;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

// 第二步:调用对象的autorelease实例方法;
id obj = [[NSObject alloc] init];
[obj autorelease];

// 第三步:废弃NSAutoreleasePool对象;
[pool drain]; // 向pool管理的所有对象发送消息,相当于[obj release]

// obj已经释放,再次调用会崩溃(Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT))
NSLog(@"打印obj:%@", obj);
  1. ARC 下使用自动释放池

ARC环境不能使用NSAutoreleasePool类也不能调用autorelease方法,代替它们实现对象自动释放的是 @autoreleasepool块

1
2
3
4
5
// ARC环境下的测试:
@autoreleasepool {
id obj = [[NSObject alloc] init];
NSLog(@"打印obj:%@", obj);
}

ARC 下 AutoReleasePool 内部实现

使用@autoreleasepool{}

我们在main函数中写入自动释放池相关的测试代码如下:

1
2
3
4
5
6
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
}
return 0;
}

为了探究释放池的底层实现,我们在终端使用 clang -rewrite-objc + 文件名命令将上述OC代码转化为 C++ 源码:

1
2
3
4
5
6
7
8
9
int main(int argc, const char * argv[]) {
/* @autoreleasepool */
{
__AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_d37e0d_mi_0);
} // 大括号对应释放池的作用域

return 0;
}

在经过编译器 clang 命令转化后,我们看到的所谓的 @autoreleasePool块,其实对应着
__AtAutoreleasePool的结构体

分析结构体 __AtAutoreleasePool 的具体实现

在源码中找到 __AtAutoreleasePool结构体 的实现代码,具体如下:

1
2
3
4
5
6
7
8
extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};

__AtAutoreleasePool结构体包含了:构造函数、析构函数和一个对象

构造函数内部调用:objc_autoreleasePoolPush() 方法,返回对象atautoreleasepoolobj

析构函数内部调用:objc_autoreleasePoolPop() 方法,传入对象atautoreleasepoolobj

  • 分析main函数中 __autoreleasepool结构体实例的生命周期是这样的:

__autoreleasepool是一个自动变量,其构造函数是在程序执行到声明这个对象的位置时调用的,而其析构函数则是在程序执行到离开这个对象的作用域时调用。所以,我们可以将上面main函数的代码简化如下:

1
2
3
4
5
6
7
8
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ {
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kb_06b822gn59df4d1zt99361xw0000gn_T_main_d39a79_mi_0);
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
  • objc_autoreleasePoolPushobjc_autoreleasePoolPop
    进一步观察自动释放池构造函数与析构函数的实现,其实它们都只是对
    AutoreleasePoolPage 对应静态方法 pushpop 的封装:
1
2
3
4
5
6
7
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}

理解 AutoreleasePoolPage

AutoreleasePoolPage 是一个 C++ 中的类,打开Runtime的源码工程,在NSObject.mm文件中可以找到它的定义,摘取其中的关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class AutoreleasePoolPage {
# define EMPTY_POOL_PLACEHOLDER ((id*)1) // 空池占位
# define POOL_BOUNDARY nil // 即哨兵对象
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
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
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic; // 校验AutoreleasePagePoolPage结构是否完整
id *next; // 指向新加入的autorelease对象的下一个位置,初始化时指向begin()
pthread_t const thread; // 当前所在线程,AutoreleasePool是和线程一一对应的
AutoreleasePoolPage * const parent; // 指向父节点page,第一个结点的parent值为nil
AutoreleasePoolPage *child; // 指向子节点page,最后一个结点的child值为nil
uint32_t const depth; // 链表深度,节点个数
uint32_t hiwat; // 数据容纳的一个上限
//......
};

AutoreleasePoolPage 中拥有 parent 和 child 指针,分别指向上一个和下一个 page;当前一个page的空间被占满(每个AutorelePoolPage的大小为4096字节)时,就会新建一个AutorelePoolPage对象并连接到链表中,后来的Autorelease对象也会添加到新的page中;
另外,当next== begin()时,表示AutoreleasePoolPage为空;当next == end(),表示AutoreleasePoolPage已满。

  • 理解 哨兵对象(POOL_BOUNDARY)的作用,而它的作用事实上也就是为了 起到一个标识的作用

每当自动释放池初始化调用 objc_autoreleasePoolPush 方法时,总会通过 AutoreleasePoolPagepush 方法,将 POOL_BOUNDARY 放到当前 page 的栈顶,并且返回这个对象 atautoreleasepoolobj

而在自动释放池释放调用 objc_autoreleasePoolPop 方法时,又会将 atautoreleasepoolobj对象 以参数传入,这样自动释放池就会向释放池中对象发送release消息,直至找到第一个边界对象为止。

理解 objc_autoreleasePoolPush 方法

经过前面的分析,objc_autoreleasePoolPush 最终调用的是 AutoreleasePoolPagepush 方法,该方法的具体实现如下:

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
39
40
41
42
43
44
45
46
static inline void *push() {
return autoreleaseFast(POOL_BOUNDARY);
}

static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
1. return autoreleaseNoPage(obj);
}
}

// 压栈操作:将对象加入AutoreleaseNoPage并移动栈顶的指针
id *add(id obj) {
id *ret = next;
*next = obj;
next++;
return ret;
}

// 当前hotPage已满时调用
static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());

setHotPage(page);
return page->add(obj);
}

// 当前hotpage不存在时调用
static id *autoreleaseNoPage(id obj) {
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);

if (obj != POOL_SENTINEL) {
page->add(POOL_SENTINEL);
}

return page->add(obj);
}

观察上述代码,每次调用 push 其实就是 创建一个新的AutoreleasePoolPage,在对应的AutoreleasePoolPage中插入一个 POOL_BOUNDARY ,并且返回插入的 POOL_BOUNDARY 的内存地址。自动释放池最终都会通过 page->add(obj) 方法 将对象添加到page中,而这一过程被分为三种情况:

* 当前page存在且不满,调用 `page->add(obj)` 方法将 `对象` 添加至page的栈中,即next指向的位置

* 当前page存在但是已满,调用 autoreleaseFullPage 初始化一个新的 page,调用page->add(obj)方法将对象添加至page的栈中

* 当前page不存在时,调用 autoreleaseNoPage 创建一个 hotPage,再调用page->add(obj) 方法将对象添加至page的栈中

理解 objc_autoreleasePoolPop 方法

  • AutoreleasePool 的释放调用的是 objc_autoreleasePoolPop 方法,此时需要传入 atautoreleasepoolobj 对象作为参数。

  • 同理,我们找到 objc_autoreleasePoolPop 最终调用的方法,即 AutoreleasePoolPage 的pop方法,该方法的具体实现如下

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
39
40
41
static inline void pop(void *token)   // POOL_BOUNDARY的地址
{
AutoreleasePoolPage *page;
id *stop;

page = pageForPointer(token); // 通过POOL_BOUNDARY找到对应的page
stop = (id *)token;
if (DebugPoolAllocation && *stop != POOL_SENTINEL) {
// This check is not valid with DebugPoolAllocation off
// after an autorelease with a pool page but no pool in place.
_objc_fatal("invalid or prematurely-freed autorelease pool %p; ",
token);
}

if (PrintPoolHiwat) printHiwat(); // 记录最高水位标记

page->releaseUntil(stop); // 向栈中的对象发送release消息,直到遇到第一个哨兵对象

// memory: delete empty children
// 删除空掉的节点
if (DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
}
else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}

【总结】

【结构】:

  • 自动释放池的压栈和出栈,通过结构体的构造函数和析构函数实现:

    • 压栈:调用 objc_autoreleasePoolPush() 函数,内部调用的是 AutoreleasePoolPagepush() 方法,返回 atautoreleasepoolobj 对象

    • 出栈:调用 objc_autoreleasePololPop() 函数,内部调用的是 AutoreleasePoolPagepop() 方法,传入 atautoreleasepoolobj 对象

【容量】:

  • 池页大小为4096字节,每一页都包含56字节的成员变量,但一个自动释放池中,只会压栈一个哨兵对象,占8字节

【原理】:

  • 自动释放池的本质是 __AtAutoreleasePool 结构体,包含 构造函数和析构函数

  • 结构体声明,触发构造函数,调用 objc_autoreleasePoolPush() 函数,对象压栈

    • 如果存在page,并且没有存满,调用add函数

      • 将对象压栈
    • 如果存在page,但存储已满,调用autoreleaseFullPage函数

      • 遍历链表,找到最后一个空白的子页面
      • 对其进行创建新页
      • 设置为热页面
      • 添加对象
    • 否则,不存在page,调用autoreleaseNoPage函数

      • 通过父类AutoreleasePoolPageData进行初始化
      • begin:获取对象压栈的起始位置
      • objc_thread_self:通过tls获取当前线程
      • 链接双向链表
      • 设置为热页面
      • pushExtraBoundary为YES,哨兵对象压栈
      • 对象压栈
  • 结构体出作用域,触发析构函数,调用 objc_autoreleasePoolPop() 函数,对象出栈

    • 调用popPage函数,传入stop为哨兵对象的位置
    • 当前页中对象出栈,到stop位置停止
    • 调用kill函数,销毁当前页面

AutoreleasePool在主线程上的释放时机

  • 分析主线程RunLoop管理自动释放池并释放对象的详细过程,我们在如下Demo中的主线程中设置断点,并执行lldb命令:po [NSRunLoop currentRunLoop],具体效果如下:

我们看到主线程RunLoop中有两个与自动释放池相关的Observer,对应CFRunLoopActivity的类型如下:

1
2
3
4
5
6
7
8
9
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //0x1,启动Runloop循环
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5), //0xa0,即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7), //0xa0,退出RunLoop循环
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

结合RunLoop监听的事件类型,分析主线程上自动释放池的使用过程如下:

  • App启动后,苹果在主线程RunLoop里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler();

  • 第一个Observer监视的事件

    • Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。
  • 第二个Observer监视了两个事件 :

    • BeforeWaiting(准备进入休眠) 时调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;

    • Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。

  • 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop创建好的AutoreleasePool环绕着,所以不会出现内存泄漏,开发者也不必显示创建AutoreleasePool了;

  • 之后的时机

    • 程序启动到加载完成后,主线程对应的RunLoop会停下来等待用户交互

    • 用户的每一次交互都会启动一次运行循环,来处理用户所有的点击事件、触摸事件。

    • RunLoop检测到事件后,就会创建自动释放池;

    • 所有的延迟释放对象都会被添加到这个池子中;

    • 在一次完整的运行循环结束之前,会向池中所有对象发送release消息,然后自动释放池被销毁;

AutoreleasePool子线程上的释放时机

子线程默认不开启RunLoop,那么其中的延时对象该如何释放呢?其实这依然要从Thread和AutoreleasePool的关系来考虑:

  • 就是说,每一个线程都会维护自己的 Autoreleasepool栈,所以子线程虽然默认没有开启RunLoop,但是依然存在AutoreleasePool,在 子线程退出 的时候会去释放autorelease对象。

  • 前面讲到过,ARC会根据一些情况进行优化,添加__autoreleasing修饰符,其实这就相当于对需要延时释放的对象调用了autorelease方法。从源码分析的角度来看,如果子线程中没有创建AutoreleasePool ,而一旦产生了Autorelease对象,就会调用autoreleaseNoPage方法自动创建hotpage,并将对象加入到其栈中。所以,一般情况下,子线程中即使我们不手动添加自动释放池,也不会产生内存泄漏。

AutoreleasePool需要手动添加的情况

  • 尽管ARC已经做了诸多优化,但是有些情况我们必须手动创建 AutoreleasePool,而其中的延时对象将在当前释放池的作用域结束时释放。苹果文档中说明了三种情况,我们可能会需要手动添加自动释放池:

    • 编写的不是基于UI框架的程序,例如命令行工具;
    • 通过循环方式创建大量临时对象;
    • 使用非Cocoa程序创建的子线程;

而在ARC环境下的实际开发中,我们最常遇到的也是第二种情况,以下面的代码为例:

1
2
3
4
5
6
7
- (void)viewDidLoad {
[super viewDidLoad];
for (int i = 0; i < 1000000; i++) {
NSObject *obj = [[NSObject alloc] init];
NSLog(@"打印obj:%@", obj);
}
}

上述代码中,obj因为离开作用域所以会被加入最近一次创建的自动释放池中,而这个释放池就是主线程上的RunLoop管理的;因为for循环在当前线程没有执行完毕,Runloop也就没有完成当前这一次的迭代,所以导致大量对象被延时释放。释放池中的对象将会在viewDidAppear方法执行前就被销毁。在此情况下,我们就有必要通过手动干预的方式及时释放不需要的对象,减少内存消耗;优化的代码如下:

1
2
3
4
5
6
7
8
9
- (void)viewDidLoad {
[super viewDidLoad];
for (int i = 0; i < 1000000; i++) {
@autoreleasepool{
NSObject *obj = [[NSObject alloc] init];
NSLog(@"打印obj:%@", obj);
}
}
}
  • Post title:OC学习03:内存管理
  • Post author:张建
  • Create time:2023-03-02 18:51:48
  • Post link:https://redefine.ohevan.com/2023/03/02/OC/OC学习03:内存管理/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.