1. 概述
What —— 什么是内存管理?
内存管理是程序设计中常见的资源管理(resource management)的一部分,主要功能是控制应用程序中对象的生命周期,确保它们在需要时存在,而不再需要时能够及时释放内存。
Why —— 为什么要进行内存管理?
内存管理是确保应用程序高效运行并避免资源浪费的关键环节,具体表现为:
- 防止内存泄漏(leak memory):如果应用程序未能及时释放不再使用的内存,从而导致内存占用不断增加,最终可能致使应用程序崩溃。良好的内存管理可以防止这种情况的发生。
- 避免悬挂指针(dangling pointers):悬挂指针是指某块内存已被释放,但其指针仍然指向该内存,这可能会导致应用程序在后续访问时崩溃或产生未定义的行为。良好的内存管理可以确保指针在适当的时机被置为nil,从而避免这种情况。
- 提高程序性能:在iOS开发中,设备的内存资源有限,良好的内存管理能够确保应用程序只使用必要的内存资源,精细控制内存的使用,从而保证设备的响应速度以及程序的平稳运行。
How —— 怎样进行内存管理?
在Objective-C中,内存管理的核心概念是“引用计数”(Reference Counting)。每个对象在创建时会有一个引用计数,当有对象引用它时,引用计数增加;当引用被移除时,引用计数减少。当引用计数为零时,表示该对象没有被任何地方使用,其所占用的内存可以被释放。
Objective-C的基本内存管理模型有以下3种:
- 垃圾收集(Garbage Collection):
系统的垃圾收集器能够在后台定期检查哪些对象不再被使用,并自动回收这些对象的内存。但iOS运行环境并不支持垃圾收集,且这项功能在macOS 10.8后被完全废弃,故不做专门介绍。
- 手工引用计数(Manual Reference Counting, MRC):
开发者手动管理对象的引用计数。
- 自动引用计数(Automatic Reference Counting, ARC):
编译器自动管理对象的引用计数。
上图为Objective-C中的内存分布,其中,堆区内存是动态分配的,由开发者或运行时系统负责管理。每个堆区对象都有一个引用计数,对象的指针会存储在栈区,引用计数增减影响的是对象在堆区的内存释放,而不是栈区的内存。
2. 手工引用计数
2.1 MRC简介
手工引用计数(MRC)是Objective-C中早期的内存管理机制。在MRC中,开发者必须手动管理对象的引用计数,通过显式调用’retain’、’release’和’autorelease’方法来控制对象的生命周期。尽管MRC在现代开发中已经不如ARC常用,但如果需要支持一些不能够迁移到ARC上运行的代码,还是需要理解其工作原理,并且这对掌握Objective-C的内存管理非常重要。
在MRC中,每个对象都有一个与之关联的引用计数。当对象被创建或被另一个对象引用时,引用计数增加;当不再需要该对象时,引用计数减少;当引用计数减为零时,系统会自动调用对象的’dealloc’方法,释放该对象的内存。
2.2 MRC中的内存管理操作
- retain: ’retain’方法将对象的引用计数增加1,这意味着对象有一个新的持有者或引用,表明它仍在使用中。
- release: ’release’方法将对象的引用计数减少1。
可能会编写这样一个方法:先使用alloc创建一个对象,然后将它作为方法调用的结果返回。
这样会有一个问题:尽管方法不再使用这个对象,但是并不能释放它,因为需要将这个对象作为方法的返回值。
NSAutoreleasePool类创建的目的就是希望能够解决这个问题,自动释放池可以帮助追踪需要延迟一些时间释放的对象。
- autorelease: ’autorelease’方法将对象添加到当前的自动释放池中,当池被销毁(池执行到末尾,或给池发送了 ’drain’消息)时,对象会自动收到 ’release’消息,通常用于临时对象的管理。
2.3 常见的内存管理模式
-
所有权模式(Ownership Patterns)
谁分配,谁释放:对象的创建者负责释放对象,如果通过’alloc’、’new’、’copy’或’mutablecopy’创建了一个对象,那么就有责任在不再需要它时调用’release’。
传递所有权:当将对象传递给其他对象时,可能需要调用’retain’以确保它在新环境中不被释放,或者由接收方决定是否保留引用。
-
Autorelease Pool模式
将对象放入自动释放池中,系统会在适当的时候自动释放。
PS:任何由’alloc’、’new’、’copy’或’mutablecopy’创建的对象都不会被自动进入自动释放池,这种情况下可以说你拥有这个对象,需要在使用完这些对象后负责释放这些对象的内存。可以主动给这些对象发送’release’消息,或者发送 ’autorelease’消息将对象加入到自动释放池中。
2.4 Objective-C的内存管理规则
-
当使用’alloc’、’new’、’copy’或’mutablecopy’方法创建一个对象时,该对象的引用计数为1。当不再使用该对象时,应该向该对象发送一条’release’或’autorelease’消息。这样,该对象将在其使用寿命结束时被销毁。
-
当通过其他方法获得一个对象时,假设该对象的引用计数为1,而且已经被设置为自动释放,那么不需要执行任何操作来确保该对象得到清理。如果打算在一段时间内拥有该对象,则需要保留它并确保在操作完成时释放它。
-
如果保留了某个对象,就需要(最终)释放或自动释放该对象。必须保持’retain’方法和’release’方法的使用次数相等。
获得途径 | 临时对象 | 拥有对象 |
alloc/new/copy/mutablecopy | 不再使用时释放对象 | 在dealloc方法中释放对象 |
其他方法 | 不需要执行任何操作 | 获得对象时保留,在在dealloc方法中释放对象 |
2.5 MRC的优缺点
✅优点:
- 提供了对内存管理的完全控制
- 对于特定性能需求,可以进行更细粒度的优化
❌缺点:
- 开发者需要花费额外的精力管理内存,增加了代码的复杂性
- 易于出错,增加了出现内存泄漏、悬挂指针等问题的风险
3. 自动引用计数
3.1 ARC简介
自动引用计数(ARC)是Objective-C内存管理中的一种现代化机制,旨在简化内存管理操作,降低手动管理内存时出错的风险。其核心思想是ARC会在编译时自动为代码插入合适的内存管理指令,如’retain’、’release’和’autorelease’,从而确保对象的生命周期被正确管理,减轻开发者的负担。
3.2 ARC中的内存管理操作
在ARC下,开发者不再显式调用’retain’和’release’,而是通过属性和关键字来控制对象的内存管理。
- 强引用(Strong References):
默认情况下,ARC将对象的引用视为强引用,确保对象在被引用期间不会被释放。
- 弱引用(Weak References):
使用’__weak’修饰符声明的对象引用不会增加引用计数。
- 无主引用(Unowned References):
使用’__unsafe_unretained’或’__unowned’修饰符,这些引用不会增加引用计数,但在对象被释放时,其引用不会被置为’nil’,可能导致悬挂指针问题。
- __autoreleasing:
表示对象将在当前的自动释放池中释放,常见于错误处理或返回值传递。
3.3 循环引用问题
循环引用(Retain cycle)是指在内存管理中,两个或多个对象相互持有对方的强引用,导致它们的引用计数都无法降为零,从而无法释放内存。是一个经典的内存泄漏问题。
这里给出图示来解释和解决循环引用问题:
解决后的代码示例如下:
在代码块(Block)中如果引入了self对象也可能会出现循环引用问题:self对象持有代码块的强引用,块持有self对象的强引用。
解决方法:在代码块外使用@weakify(self),解除强引用;在代码块内使用@strongify(self)确保在块执行期间self不会被释放。
3.4 使用ARC时需注意的场景
- 批量创建临时对象时:
在循环中创建大量临时对象时,可能需要手动管理释放,以避免过高的内存峰值。
- 多线程环境下:
在子线程中,系统不会自动创建自动释放池,因此需要手动创建。
3.5 ARC的优缺点
✅优点:
- 自动化管理:ARC负责插入内存管理方法的调用,开发者无需手动管理对象的引用计数
- 提高安全性:通过减少手动操作,ARC降低了出现内存泄漏和悬挂指针的风险
- 代码简明化:无需手动调用'retain'、'release'和'autorelease',代码更加简洁易读
❌缺点:
- 灵活性降低:在某些性能敏感的场景下,ARC可能会插入不必要的内存管理代码,影响性能
4. 常见内存管理问题
- 内存泄漏
解决方案:
1)检查是否出现循环引用、存在未释放的对象等
2)使用XCode工具:XCode -> Open Developer Tool -> Instruments -> Leaks
- 僵尸对象:指已经被释放的对象仍然被访问,导致未定义的行为或应用崩溃
解决方案:
1)检查’release’操作和’weak’、’unsafe_unretained’指针,确保不会访问释放后的对象
2)使用XCode工具:XCode -> Open Developer Tool -> Instruments -> Zombies
- 循环引用
解决方案:
1)避免相互持有的对象之间使用强引用
2)使用XCode工具:XCode -> Open Developer Tool -> Instruments -> Allocations
- 高内存占用:通常由未及时释放的大量对象或频繁创建的临时对象引起
解决方案:
1)识别内存占用较高的对象,检查它们是否可以被更早释放。
2)使用autorelease pool控制大量临时对象的生命周期,避免内存峰值。
3)使用XCode工具:XCode -> Open Developer Tool -> Instruments -> Allocations
5. 总结
实际工程应用中,一般直接用ARC就好了,更加快捷、安全。但ARC底层原理——引用计数,是与MRC相通的,因此了解相关知识,对于掌握iOS的内存管理技术大有裨益。