一、内存管理方式
iOS 中主要通过引用计数(Reference Counting) 来管理对象的内存。开发者可以通过两种方式操作引用计数:
- MRC(Manual Reference Counting) :手动管理,需要开发者调用
retain、release、autorelease等方法。 - ARC(Automatic Reference Counting) :自动引用计数,由编译器在编译时自动插入内存管理代码,是现在的主流方式。
二、引用计数原理
-
每个对象内部都有一个引用计数器,表示当前有多少个地方持有该对象。
-
当引用计数变为 0 时,对象会被立即销毁,内存被回收。
-
操作对应关系:
- 引用计数 +1:
alloc/new/copy/mutableCopy(产生对象时引用计数为 1),retain(MRC)或强引用(ARC) - 引用计数 -1:
release(MRC)或强引用超出作用域/置 nil(ARC) - 延迟释放:
autorelease将对象放入自动释放池,池子被 drain 时统一 release
- 引用计数 +1:
三、ARC 规则
ARC 下编译器会在合适的位置自动插入 retain/release 代码,开发者必须遵循所有权修饰符:
__strong:默认,强引用,持有对象,引用计数 +1__weak:弱引用,不持有对象,对象销毁时自动置为 nil__unsafe_unretained:类似 weak,但不会自动置 nil,不安全__autoreleasing:用于传递间接指针,常用于 NSError 等参数传递
四、循环引用及解决方案
循环引用是内存泄漏的主要原因,即两个或多个对象相互强引用,导致无法释放。
常见场景及解决方法:
- 父子关系:父对子用强引用,子对父用弱引用(
weak)。 - Block 循环引用:Block 内部直接或间接使用了 self,且 Block 被 self 持有。解决方案是在 Block 外部使用
__weak typeof(self) weakSelf = self;,Block 内部使用 weakSelf;如果 Block 内需要 strongSelf 保证执行期间不被释放,可以在 Block 开头加__strong typeof(weakSelf) strongSelf = weakSelf;。 - Delegate:delegate 属性一般用
weak,避免循环引用。 - NSTimer:timer 会强引用 target,容易造成循环引用。解决方案:在合适的时机(如 viewWillDisappear)主动 invalidate timer,或者使用中间代理对象
五、自动释放池(Autorelease Pool)
- 用于存放标记为
autorelease的对象,通常在主线程的 RunLoop 每个循环开始前创建自动释放池,结束后销毁,从而释放池中对象。 - 手动创建自动释放池:
@autoreleasepool { ... },适用于循环中创建大量临时对象的情况,可以及时释放内存,避免峰值过高。
六、内存警告处理
- 当系统内存紧张时,会向 App 发送内存警告通知。UIViewController 会收到
didReceiveMemoryWarning,App Delegate 会调用applicationDidReceiveMemoryWarning。 - 常见做法:释放可重建的缓存对象、图片资源,清理无用数据。
七、优化技巧
- 避免使用过多的单例,除非确实需要全局共享。
- 尽量使用轻量级对象,如使用
struct代替简单的类。 - 图片加载使用适当的方法,如
imageNamed:有缓存,适合反复使用的小图;imageWithContentsOfFile:无缓存,适合一次性大图。 - 使用 Instruments 的 Leaks 和 Allocations 工具检测内存泄漏和内存分配。
八、@autoreleasepool 实现原理
1. 先一句话说明 @autoreleasepool 的作用
@autoreleasepool是 Objective-C 中用于管理自动释放对象的语法结构,它包裹的代码块中产生的自动释放对象(通过autorelease方法添加的对象)会在块结束时收到release消息,从而及时释放内存,避免内存峰值。
2. 底层实现原理(核心部分)
2.1 基于 AutoreleasePoolPage 的栈结构
- 自动释放池的底层由 C++ 类
AutoreleasePoolPage实现,它是一个双向链表节点,每个线程(Thread)拥有自己的自动释放池栈。 AutoreleasePoolPage内部有一个next指针,指向下一个可存放对象的内存位置,还有一个parent和child指针,用于链接多个 page(当当前 page 存满时,会创建新的 page)。- 每个
AutoreleasePoolPage的大小通常是 4096 字节(一页内存),除了存储对象指针,还包含一些元数据。
2.2 哨兵对象(POOL_SENTINEL)
- 当遇到
@autoreleasepool {时,编译器会在当前线程的自动释放池栈中压入一个哨兵对象(nil 或特殊标记),作为这个 pool 的边界。 - 所有在该块内调用
autorelease的对象,其指针会被依次追加到当前 page 中next指向的位置。 - 当执行到
}时,会向池中的所有对象发送release消息,一直释放到上一个哨兵对象的位置,然后销毁这个池(弹出栈顶的哨兵及之后添加的对象)。
2.3 嵌套实现
- 多个
@autoreleasepool嵌套时,每次进入都会压入一个新的哨兵,退出时弹出到对应的哨兵,因此嵌套池是“后进先出”的栈式管理。
2.4 与 RunLoop 的关联
- 主线程的 RunLoop 默认会在每次事件循环(如触摸、定时器)开始时创建自动释放池,事件结束后销毁。这保证了大多数临时对象能及时释放,也解释了为什么我们通常不需要手动创建 pool。
3. 代码层面如何体现
- 调用
[obj autorelease]时,实际会调用AutoreleasePoolPage::autorelease(obj),将对象指针添加到当前线程的自动释放池中。 - 当 pool 销毁时,会调用
AutoreleasePoolPage::pop(哨兵地址),遍历从当前 page 到哨兵之间的所有对象,并发送release。
4. 实际使用场景与优化
- 避免内存峰值:在循环中大量创建临时对象时(例如读取图片数据),用
@autoreleasepool包裹循环体,可以使每次迭代产生的对象及时释放,防止内存暴涨。 - 子线程中的自动释放池:如果子线程不开启 RunLoop,则需要手动添加
@autoreleasepool来管理自动释放对象。
5. 可能的追问及应对
- 问:
AutoreleasePoolPage的具体结构是怎样的?
答:它是一个 C++ 类,包含magic(校验用)、next(指向下一个空闲位置)、thread(绑定的线程)、parent/child(链表指针)以及一个对象指针数组(用于存储 autorelease 的对象)。 - 问:为什么自动释放池能够嵌套?
答:因为每个池用哨兵标记边界,栈式存储,出池时根据哨兵定位到正确的释放范围,所以嵌套是安全的。 - 问:ARC 下 autorelease 对象什么时候会被释放?
答:ARC 下编译器会自动插入autorelease调用,对象最终由最近的自动释放池在释放时处理。如果在主线程且没有手动创建池,则由 RunLoop 创建的池在事件循环结束时释放。
总结回答话术
@autoreleasepool的实现本质是一个基于栈的双向链表结构。每个线程都有一个自动释放池栈,每个池通过压入一个哨兵对象作为边界。当对象调用autorelease时,它的指针被添加到当前栈顶。当 pool 作用域结束时,会从栈顶向下一路释放对象,直到遇到哨兵。这种设计支持嵌套,并且和 RunLoop 紧密配合,让开发者能方便地控制内存。在写循环或子线程时,我们手动添加@autoreleasepool可以及时回收内存,避免峰值。”
这样的回答既涵盖了原理,也联系了实际使用,能够体现对内存管理的深入理解。