内存管理(ARC/Weak/AutoreleasePool)
面试回答版
一句话概括
ARC(Automatic Reference Counting)是编译器自动插桩的引用计数管理机制,它在编译期将 retain/release/autorelease 操作插入合适位置,不是 GC(垃圾回收),没有后台回收线程。
ARC 基本原理
编译器插桩规则
// 编译器在编译期自动插入 retain/release
// 源代码
- (void)handleObject:(id)obj {
[obj doSomething];
self.property = obj;
}
// 编译后等效
- (void)handleObject:(id)obj {
[obj retain]; // +1:确保方法内有效
[obj doSomething];
[self.property release]; // release 旧值
[obj retain]; // +1
self->_property = obj;
[obj release]; // -1:平衡方法开头的 retain
[obj release]; // -1:平衡参数传入时的 retain
}
alloc/new/copy/mutableCopy开头的返回已 +1。retain计数 +1,release计数 -1。- ARC 下不允许手动调用
retain/release/autorelease/dealloc。
MRC vs ARC
| 对比项 | MRC | ARC |
|---|---|---|
| retain/release | 手动调用 | 编译器自动插桩 |
| dealloc | 需调 [super dealloc] | 不可手动调,自动链式调用 |
| 自动释放池 | 可选 | 编译器插入 autorelease |
| 弱引用 | 无 | __weak / __unsafe_unretained |
| 方法命名约定 | 无强制 | copy/init/new 等有规则 |
引用计数底层实现
retain / release 路径
[obj retain] → objc_retain(obj) → obj->rootRetain() → SideTable::tryRetain()
→ 引用计数表中 +1
[obj release] → objc_release(obj) → obj->rootRelease() → SideTable::tryRelease()
→ 引用计数表中 -1
→ 归零则调用 dealloc
SideTable 结构
struct SideTable {
spinlock_t slock; // 自旋锁,保护本表
RefcountMap refcnts; // 引用计数哈希表(DenseMap 实现)
weak_table_t weak_table; // 弱引用全局表
};
// 全局有 8 个 SideTable(根据对象地址哈希索引)
// 对象引用计数 = (refcnts[obj] >> 1) + 1 (extra_rc 存储)
// 低位标记:SIDE_TABLE_RC_PINNED | SIDE_TABLE_WEAKLY_REFERENCED | SIDE_TABLE_DEALLOCATING
- Tagged Pointer 对象无引用计数,直接存在指针值中。
- 优化:非指针 isa 的
extra_rc字段可存储额外引用计数,减少 SideTable 访问。
weak 实现原理
// 注册 weak 引用
id objc_storeWeak(id *location, id newObj) {
// 1. 清除 location 上的旧 weak 映射
// 2. 将 newObj 注册到 SideTable.weak_table
// 添加 weak_entry_t,内含指向 location 的指针数组
// 3. location 指向 newObj
}
// 读取 weak 引用
id objc_loadWeak(id *location) {
// 1. objc_copyWeak 返回对象(retain + autorelease)
// 2. 确保读取期间对象不释放
}
// dealloc 时自动置 nil
void objc_destroyWeak(id *location) {
// 1. 找到 SideTable
// 2. 从 weak_table 对应 entry 的指针数组移除 location
// 3. *location = nil
}
dealloc 时 weak 自动置 nil 流程:
dealloc
→ objc_destructInstance
→ objc_clear_deallocating
→ SideTable::weak_clear_no_lock
→ 遍历 weak_entry_t 中的所有 referrer
→ *referrer = nil // 全部置 nil
→ 从 weak_table 移除 entry
追问重点:weak 表什么时候会清理旧的?
objc_storeWeak在设置新值时会先清理旧地址的映射。所以weak属性在objc_storeWeak时覆盖。
autoreleasepool 机制
数据结构
autoreleasepool 基于栈结构,以 Page 为单位:
objc_autoreleasePoolPush()
→ AutoreleasePoolPage::push()
→ 在当前线程的 autoreleasepool 栈顶压入一个 POOL_BOUNDARY 哨兵
[obj autorelease]
→ AutoreleasePoolPage::autorelease(obj)
→ 在当前 Page 的 next 位置插入 obj,next++
objc_autoreleasePoolPop(哨兵令牌)
→ AutoreleasePoolPage::pop(哨兵)
→ 从栈顶逐一向后释放对象,直到遇到 POOL_BOUNDARY 哨兵
→ 回滚 next 指针
- 每个线程都有独立的 autoreleasepool 栈(
TLS线程局部存储)。 - Page 大小 4096 字节(一页内存),Page 之间双向链表连接。
- Page 满了会创建新 Page(
parent/child指针)。
autoreleasepool 与 RunLoop 的绑定
RunLoop 一次循环:
1. objc_autoreleasePoolPush() ← 压入哨兵
2. 处理事件(Source/Timer/Observer)
3. objc_autoreleasePoolPop(哨兵令牌) ← 回收本次循环产生的临时对象
嵌套场景(手动 @autoreleasepool):
@autoreleasepool { ← Push 哨兵 A
id obj = [NSObject new];
// ...
@autoreleasepool { ← Push 哨兵 B
// ...
} ← Pop 哨兵 B(释放 B 后的临时对象)
// ...
} ← Pop 哨兵 A(释放 A 后的临时对象)
Tagged Pointer
- 对于
NSNumber、NSDate、小NSString等对象,值直接编码在指针中。 - 判断:
最低位为 1(arm64 下0x3标记)。 - 优点:无需 malloc 分配堆内存、无需 retain/release、访问更快。
- 注意:
isEqual:可以比较,==不一定(不同 Tagged Pointer 编码)。
内存泄漏场景排查
4 种常见循环引用链
| 场景 | 原因 | 修复 |
|---|---|---|
| Block 属性 | VC 持有 Block,Block 捕获 self | [weak self] |
| NSTimer | Target 强引用 self,Timer 被 RunLoop 持有 | 系统 API 加到 deinit 里 invalidate,或使用 block-based Timer |
| Delegate | 强引用 delegate(应为 weak) | weak var delegate |
| 闭包网络回调 | VC -> NetworkService -> closure -> VC | [weak self] + guard let self |
deinit 验证
class MyViewController: UIViewController {
deinit {
print("[Leak] \(Self.self) deinit") // 若页面 pop 不打印则泄漏
}
}
高频追问清单
| 问题 | 关键要点 |
|---|---|
| ARC 是编译期还是运行期? | 编译期插桩 + 运行期 SideTable 管理引用计数 |
| weak 的底层数据结构? | SideTable.weak_table.weak_entry_t,内含 referrer 指针数组 |
| dealloc 时 weak 指针何时置 nil? | objc_clear_deallocating -> weak_clear_no_lock -> *referrer = nil |
| autoreleasepool 的释放时机? | 每次 RunLoop 循环结束 Pop,或手动 @autoreleasepool 结束时 |
| Tagged Pointer 的判断方式? | 指针最低有效位标记,arm64 下 objc_taggedPtr 通过掩码判断 |
__weak vs __unsafe_unretained? | weak:自动置 nil;unsafe:野指针 |
| objc_retainAutoreleasedReturnValue 是什么? | 返回值优化(caller/callee 约定,消除多余的 retain/release) |
| 为什么 __weak 变量在使用前要转为 __strong? | 防止多线程环境下对象被释放导致返回 nil |
项目落地版
场景 1:批量处理降低内存峰值
// 处理大量图片/payload 时,手动池避免峰值飙升
for (NSInteger i = 0; i < largeArray.count; i++) {
@autoreleasepool {
NSData *data = [self processItem:largeArray[i]];
[writer appendData:data];
}
}
场景 2:循环引用检测器
final class LeakDetector {
static let shared = LeakDetector()
private var livingObjects = NSHashTable<AnyObject>.weakObjects()
func track(_ object: AnyObject) {
livingObjects.add(object)
}
func untrack(_ object: AnyObject) {
livingObjects.remove(object)
}
/// 在页面 pop 后调用,检查是否有 VC 未释放
func checkForLeaks() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
guard let self else { return }
for obj in self.livingObjects.allObjects {
print("[LeakDetector] 疑似泄漏: \(type(of: obj))")
}
}
}
}
场景 3:NSTimer 安全封装
final class SafeTimer {
private weak var target: AnyObject?
private let selector: Selector
init(timeInterval: TimeInterval, target: AnyObject, selector: Selector, repeats: Bool) {
self.target = target
self.selector = selector
// 使用 block 避免 target 强持有
Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: repeats) { [weak target] _ in
_ = target?.perform(selector)
}
}
}
场景 4:内存水位监控
import MachO
func reportMemoryUsage() -> UInt64 {
var info = task_vm_info_data_t()
var count = mach_msg_type_number_t(MemoryLayout<task_vm_info>.size / 4)
let result = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
}
}
guard result == KERN_SUCCESS else { return 0 }
return info.phys_footprint / 1024 / 1024 // MB
}
场景 5:关联对象 + Weak 引用封装
// 给分类添加 weak 属性的替代方案
@interface NSObject (WeakAssociated)
@property (nonatomic, weak) id weakAssociatedObject;
@end
// 用中间持有者封装 weak 语义
@interface WeakHolder : NSObject
@property (nonatomic, weak) id object;
@end
static const void *kWeakAssociatedKey = &kWeakAssociatedKey;
@implementation NSObject (WeakAssociated)
- (void)setWeakAssociatedObject:(id)object {
WeakHolder *holder = [WeakHolder new];
holder.object = object;
objc_setAssociatedObject(self, kWeakAssociatedKey, holder, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)weakAssociatedObject {
WeakHolder *holder = objc_getAssociatedObject(self, kWeakAssociatedKey);
return holder.object;
}
@end
学习路径与优先级
初级(P0)— 掌握引用语义和使用边界
- 理解 strong/weak/copy/assign/unsafe_unretained 的区别
- 能解释 ARC 不是 GC
- 掌握 @autoreleasepool {} 基本用法
- 知道循环引用的概念,能识别 Block 中的 [weak self]
- 理解 deinit 和 dealloc 的关系
- 能使用 Instruments -> Leaks / Allocations 排查内存问题
自检:
- 写出 3 种可能产生循环引用的代码模式
- 解释为什么 IBOutlet 用 weak
中级(P0)— 能排查和治理内存问题
- 理解 weak 自动置 nil 的底层实现(SideTable + weak_table)
- 掌握 autoreleasepool Page 的 Page/哨兵/嵌套机制
- 能定位 NSTimer/CADisplayLink 泄漏并修复
- 了解 Tagged Pointer 的工作原理
- 能编写内存泄漏检测工具
- 理解 objc_retainAutoreleasedReturnValue 返回值优化
- 掌握非指针 isa 的 extra_rc 机制
动手实践:
- 用断点验证 weak 对象在 dealloc 后自动置 nil
- 在 @autoreleasepool 嵌套中打印哨兵地址
- 实现一个简单的内存泄漏检测器
高级(P1)— 建立内存治理体系
- 能设计内存监控和预警体系(水位/峰值/OOM 关联)
- 理解 CF 桥接与 __bridge/__bridge_retained/__bridge_transfer
- 能设计并推动稳定性门禁(内存回归检测)
- 了解 VM Tracker / malloc stack logging 等高级调试工具
- 能为团队沉淀内存治理规范文档
实战项目:
- 为 App 搭建内存水位监控 Dashboard
- 设计循环引用自动检测规则(基于 Malloc Scribble + 堆栈分析)
- 在 CI 中集成 OOM 回归检测