OC学习03:内存管理
前言
本文主要介绍 内存的五大区
、函数栈
和 内存管理
内存五大区
在iOS中,内存主要分为 栈区、堆区、全局区、常量区、代码区
五个区域,如下图所示:
栈区
定义
栈是
系统数据结构
,其对应的进程或者线程是唯一的
栈是
向低地址扩展
的数据结构栈是一块
连续的内存区域
,遵循先进后出(FILO)
原则栈区一般在
运行时
分配
存储
栈区是由 编译器自动分配并释放
的,主要用来存储
局部变量
函数的参数
,例如函数的隐藏参数(id self, SEL _cmd)
优缺点
优点:因为栈是由
编译器自动分配并释放
的,不会产生内存碎片,所以快速高效
确定:栈的
内存大小有限制,数据不灵活
iOS主线程大小是1MB
- 其他线程是
512KB
MAC
只有8MB
以上内存大小的说明,在Threading Programming Guide 中有相关说明,如下图:
堆区
定义
堆是
向高地址扩展
的数据结构堆是
不连续的内存区域
,类似于链表结构
(便于增删,不便于查询),遵循先进先出(FIFO)
原则堆的
地址空间
在iOS中是是动态的堆区的分配一般是以在
运行时分配
存储
堆区是 由程序员动态分配和释放
的,如果程序员不释放,程序结束后,可能由操作系统回收,主要用于存放:
OC
中使用alloc
、new
开辟空间创建对象
,或者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 | int a = 10; // 全局区(已初始化的全局变量) |
运行结果如下:
1 | 2022-03-11 14:34:25.438913+0800 内存五大区[70321:4340509] i的内存地址:0x16f6f5a18 |
对于
局部变量i
, 存放在栈区对于
字符串对象string
,分别打印了string得对象地址
和string对象的指针地址
- string的
对象地址
是是存放在常量区
- string
对象的指针地址
,是存放在栈区
- string的
对于
alloc创建的对象obj
,分别打印了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 | int Add(int x,int y) { |
程序执行时,栈区中栈帧的变化如下图所示:
内存管理
概述
在iOS中开发中,我们或多或少都听说过内存管理。iOS的内存管理一般指的是OC对象的内存管理,因为OC对象分配在堆内存,堆内存需要程序员自己去动态分配和回收;基础数据类型(非OC对象)则分配在栈内存中,超过作用域就会由系统检测回收。如果我们在开发过程中,对内存管理得不到位,就有可能造成内存泄露。
我们通常讲的内存管理,实际上从发展的角度来说,分为两个阶段:
MRC和ARC
。MRC指的是
手动内存管理
,在开发过程中需要开发者手动去编写内存管理的代码
;ARC指的是
自动内存管理
,在此内存管理模式下由LLVM编译器和OC运行时库生成相应内存管理的代码
。
引用计数
在
OC
中,使用引用计数
来进行内存管理
。每个对象都有一个与其相对应的引用计数器,当持有一个对象,这个对象的引用计数就会递增;当这个对象的某个持有被释放,这个对象的引用计数就会递减。当这个对象的引用计数变为0,那么这个对象就会被系统回收。
当一个对象使用完没有释放,此时其引用计数永远大于1。该对象就会一直占用其分配在堆内存的空间,就会导致内存泄露。内存泄露到一定程度有可能导致内存溢出,进而导致程序崩溃。
MRC
- 简介
全称 Manual Reference Counting,管理通过使用 retain, release
, 以及 autorelease
的消息发送来实现。
- retain: 持有(拥有)对象,对象引用数加 1
- release: 释放对象,对象引用数减 1
- autorelease: 通知系统,在
@autoreleasepool
代码块结束时,对对象调用release
- 管理原则
- 自己创建的对象,自己获得拥有权
在苹果规定中,使用 alloc/new/copy/mutableCopy
创建返回的对象归调用者所有,例如以下
1 | /* NSMutableArray类对象A */ |
由于对象 A
由 alloc
生成,符合苹果规定,指针变量(*array) 指向并持有 [[NSMutableArray alloc] init]创建的 对象A
,引用计数器会加 1
。另外,array在使用完 对象A
后需要对其进行释放。当调用 release
后,释放了其对对象A的引用,计数器减1。对象A此时引用计数值为零,所以对象A被回收。不能访问已经被回收的对象,会发生崩溃。
- 别人创建的对象,可以通过
retain
来获得拥有权
1 | // 例如已有 fooArray, 通过 array 方法获得其引用 |
你所拥有的对象不再需要使用时,必须将其释放
不能释放你不拥有的对象
- retain
- retain和属性
我们可以通过属性来保存对象,如果一个属性为强引用,我们就可以通过属性的实例变量和存取方法来对某个对象进行操作,例如某个属性的setter方法如下:
1 | - (void)setPerson:(Person *)person { |
我们通过 retain新值
,release旧值
,再给实例变量更新值。
需要注意的一点是:需要先retain新值,再release旧值。因为如果新旧值是同一个对象的话,先release就有可能导致该对象被系统回收,再去retain就没有任何意义了。例如下面这个例子:
1 | #import "ViewController.h" |
由于P对象被回收,对应其所分配的内存被置于 可用内存池
中。如果该内存未被覆写,那么P对象依然有效;如果内存被覆写,那么实例变量_person就会指向一个被覆写的未知对象的指针,那么实例变量就变成一个 悬挂指针
。
- retain和数组
如果我们把一个对象加入到一个数组中,那么该数组的addObject方法会对该对象调用retain方法。例如以下代码:
1 | // person获得并持有P对象,P对象引用计数为1 |
此时,对象P被person和array两个变量同时持有。
- release
- 自己持有的对象自己释放
当我们持有一个对象,如果在不需要继续使用该对象,我们需要对其进行释放(release)。例如以下代码:
1 | // array获得并持有NSArray类对象 |
- 非自己持有的对象不要释放
当我们不持有某个对象,却对该对象进行释放,应用程序就会崩溃。
1 | // 获得并持有A对象 |
另外,我们也不能访问某个已经被释放的对象,该对象所占的堆空间如果被覆写就会发生崩溃的情况。
- autorelease
autorelease
指的是自动释放,当一个对象收到 autorelease
的时候,该 对象就会被注册到当前处于栈顶的自动释放池(autorelease pool)
。如果没有主动生成自动释放池,则当前自动释放池对应的是 主运行循环的自动释放池
。在当前线程的RunLoop进入 休眠前
,就会对被注册到该自动释放池的所有对象进行一次 release
操作。
autorelease和release的区别是:
release:是马上释放对某个对象的强引用;
autorelease:是延迟释放某个对象的生命周期。
1 | { |
在外部调用,从方法名person知道,创建的对象由p指针变量获得但不持有。在函数内部,person获得并持有了Person类对象,所返回的person对象的引用计数加1。换句话说,调用者需要额外处理这多出来的一个持有操作。另外,我们不能在函数内部调用release,不然对象还没返回就已经被系统回收。这时候使用autorelease就能很好地解决这个问题。
只要把要返回的对象调用autorelease方法,注册到自动释放池就能延长person对象的生命周期,使其在 autorelease pool销毁(drain)前依然能够存活。
另外,person对象在返回时调用了 autorelease方法
。该对象已经在自动释放池中,我们可以直接使用对象p,无须再通过[p retain]访问;不过,如果要用实例变量持有该对象,则需要对变量p进行一次retain操作,实例变量使用完该对象需要释放该对象。
- autorelease pool
autorelease pool
和RunLoop(运行循环)
当应用程序启动,系统默认会 开启一条线程
,该线程就是 主线程
。主线程有一个与之对应的自动释放池。每条线程都包含一个与其对应的自动释放池
,当某条线程被终止的时候,对应该线程的自动释放池会被销毁。同时,处于该自动释放池的对象将会进行一次 release
操作。
,例如我们常见的 ARC
下的 main.h
文件:
1 | int main(int argc, char * argv[]) { |
该自动释放池用来释放在主线程下注册到该自动释放池的对象。
需要注意的是,当我们 开启一条子线程
,并且在该线程 开启RunLoop
的时候,需要为其增加一个autorelease pool
,这样有助于保证内存的安全。
- autorelease pool和降低内存峰值
当我们执行一些复杂的操作,特别是如果这些复杂的操作要被循环执行,那么中间会免不了会产生一些临时变量。当被加到主线程自动释放池的对象越来越来多,却没有得到及时释放,就会导致内存溢出。这个时候,我们可以手动添加自动释放池来解决这个问题。如以下例子所示:
1 | for (int i = 0; i < largeNumber; i++) { |
如上述例子所示,我们执行的循环次数是一个非常大的数字。并且调用personWithComplexOperation方法的过程中会产生许多临时对象,所产生的临时对象有可能会被注册到自动释放池中。我们通过手动生成一个自动释放池,并且在每次循环结束前把该自动释放池的对象执行release操作释放掉,这样就能有效地降低内存的峰值了。
ARC
- 概述
Automatic Reference Counting
,自动引用计数
,即ARC
,WWDC2011
和iOS5
所引入的最大的变革和最激动人心的变化。ARC是新的LLVM 3.0编译器的一项特性,使用ARC,可以说举解决了广大iOS开发者所憎恨的手动内存管理的麻烦。此处的
A
就是automatic
。其实ARC
只是比MRC
多了一步,就是在编译时编译器自动帮开发者添加 retain, release 以及 autorelease 的调用
,底层的内存管理机制还是和MRC
一样。在
ARC
模式下,我们通常在对象变量的声明里用属性标记符
来指引ARC
机制来管理我们的对象变量,它们是:strong, retain, weak, copy, assign
。默认标记是strong
- 标记符的区别
- strong: 顾名思义,就是
强引用
,对应MRC
下的retain
,即引用数加 1 - retain: 同
strong
- weak: 弱引用,不增加引用数,引用的对象被释放后变为
nil
- copy: 对对象进行
copy
后再赋值,因此对象必须遵循NSCopying
协议。如:
1 | @property(copy)Foo *foo; |
- assign: 一般用于原始数据类型(primitive type)的赋值。可以用于对象,效果相当于
weak
,可是有一个坑是当对象被释放后,assign
属性的变量不会变成nil
,而是成为野指针
(dangling pointer),因此不建议使用在对象上。
借助以上的属性标记符,我们可以在对象声明的时候集中制定它们的内存管理策略,清晰明了。
- ARC的判断原则
ARC判断一个对象是否需要释放不是通过引用计数来进行判断的,而是通过强指针来进行判断的。那么什么是强指针?
强指针
- 默认所有对象的指针变量都是强指针
- 被
__strong
修饰的指针
1 | Person *p1 = [[Person alloc] init]; |
弱指针
- 被
__weak
修饰的指针
- 被
1 | __weak Person *p = [[Person alloc] init]; |
ARC如何通过强指针来判断?
- 只要还有一个强指针变量指向对象,对象就会保持在内存中
- ARC的使用
1 | int main(int argc, const char * argv[]) { |
- ARC的注意点
- 不允许调用对象的
release方法
- 不允许调用
autorelease方法
- 重写父类的dealloc方法时,不能再调用
[super dealloc]
;
- ARC下单对象内存管理
- 局部变量释放对象随之被释放
1 | int main(int argc, const char * argv[]) { |
- 清空指针对象随之被释放
1 | int main(int argc, const char * argv[]) { |
- 默认清空所有指针都是强指针
1 | int main(int argc, const char * argv[]) { |
弱指针需要明确说明
- 注意: 千万不要使用弱指针保存新创建的对象
1 | int main(int argc, const char * argv[]) { |
打印结果:
1 | 2020-07-31 18:02:51.021697+0800 iOS-OC之ARC[2134:984503] (null) |
- ARC下多对象内存管理
ARC和MRC一样, 想拥有某个对象必须用强指针保存对象, 但是不需要在dealloc方法中release
1 | @interface Person : NSObject |
自动释放池
- 概述
AutoreleasePool
(自动释放池)是OC
中的一种内存自动回收机制
。当向一个对象发送
autorelease
消息时,会将对象加入到自动释放池,这个对象不会立即释放,而是等到runloop休眠或超出autoreleasepool作用域
之后进行释放
。
- MRC 下使用自动释放池
在MRC环境中使用自动释放池需要用到 NSAutoreleasePool
对象,其生命周期就相当于C语言变量的作用域。对于所有调用过 autorelease方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。用源代码表示如下:
1 | // MRC环境下的测试: |
ARC
下使用自动释放池
ARC环境不能使用NSAutoreleasePool类也不能调用autorelease方法,代替它们实现对象自动释放的是 @autoreleasepool块
。
1 | // ARC环境下的测试: |
ARC 下 AutoReleasePool 内部实现
使用@autoreleasepool{}
我们在main函数中写入自动释放池相关的测试代码如下:
1 | int main(int argc, const char * argv[]) { |
为了探究释放池的底层实现,我们在终端使用 clang -rewrite-objc +
文件名命令将上述OC代码转化为 C++
源码:
1 | int main(int argc, const char * argv[]) { |
在经过编译器 clang
命令转化后,我们看到的所谓的 @autoreleasePool块
,其实对应着__AtAutoreleasePool的结构体
。
分析结构体 __AtAutoreleasePool
的具体实现
在源码中找到 __AtAutoreleasePool结构体
的实现代码,具体如下:
1 | extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void); |
__AtAutoreleasePool结构体包含了:构造函数、析构函数和一个对象
;
构造函数内部调用:objc_autoreleasePoolPush()
方法,返回对象atautoreleasepoolobj
析构函数内部调用:objc_autoreleasePoolPop()
方法,传入对象atautoreleasepoolobj
- 分析main函数中 __autoreleasepool结构体实例的生命周期是这样的:
__autoreleasepool是一个自动变量,其构造函数是在程序执行到声明这个对象的位置时调用的,而其析构函数则是在程序执行到离开这个对象的作用域时调用。所以,我们可以将上面main函数的代码简化如下:
1 | int main(int argc, const char * argv[]) { |
objc_autoreleasePoolPush
与objc_autoreleasePoolPop
进一步观察自动释放池构造函数与析构函数的实现,其实它们都只是对AutoreleasePoolPage
对应静态方法push
和pop
的封装:
1 | void *objc_autoreleasePoolPush(void) { |
理解 AutoreleasePoolPage
AutoreleasePoolPage
是一个 C++
中的类,打开Runtime的源码工程,在NSObject.mm文件中可以找到它的定义,摘取其中的关键代码如下:
1 | class AutoreleasePoolPage { |
AutoreleasePoolPage
中拥有 parent 和 child 指针
,分别指向上一个和下一个 page
;当前一个page的空间被占满(每个AutorelePoolPage的大小为4096字节)时,就会新建一个AutorelePoolPage对象并连接到链表中,后来的Autorelease对象也会添加到新的page中;
另外,当next== begin()时,表示AutoreleasePoolPage为空;当next == end(),表示AutoreleasePoolPage已满。
- 理解
哨兵对象(POOL_BOUNDARY)的作用
,而它的作用事实上也就是为了起到一个标识的作用
。
每当自动释放池初始化调用 objc_autoreleasePoolPush
方法时,总会通过 AutoreleasePoolPage
的 push
方法,将 POOL_BOUNDARY
放到当前 page
的栈顶,并且返回这个对象 atautoreleasepoolobj
;
而在自动释放池释放调用 objc_autoreleasePoolPop
方法时,又会将 atautoreleasepoolobj对象
以参数传入,这样自动释放池就会向释放池中对象发送release消息,直至找到第一个边界对象为止。
理解 objc_autoreleasePoolPush
方法
经过前面的分析,objc_autoreleasePoolPush
最终调用的是 AutoreleasePoolPage
的 push
方法,该方法的具体实现如下:
1 | static inline void *push() { |
观察上述代码,每次调用 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 | static inline void pop(void *token) // POOL_BOUNDARY的地址 |
【总结】
【结构】:
自动释放池的压栈和出栈,通过结构体的构造函数和析构函数实现:
压栈:调用
objc_autoreleasePoolPush()
函数,内部调用的是AutoreleasePoolPage
的push()
方法,返回atautoreleasepoolobj
对象出栈:调用
objc_autoreleasePololPop()
函数,内部调用的是AutoreleasePoolPage
的pop()
方法,传入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 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { |
结合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 | - (void)viewDidLoad { |
上述代码中,obj因为离开作用域所以会被加入最近一次创建的自动释放池中,而这个释放池就是主线程上的RunLoop管理的;因为for循环在当前线程没有执行完毕,Runloop也就没有完成当前这一次的迭代,所以导致大量对象被延时释放。释放池中的对象将会在viewDidAppear方法执行前就被销毁。在此情况下,我们就有必要通过手动干预的方式及时释放不需要的对象,减少内存消耗;优化的代码如下:
1 | - (void)viewDidLoad { |
- 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.