二.iOS内存管理

11 阅读7分钟

内存管理(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
对比项MRCARC
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)
    → 在当前 Pagenext 位置插入 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

  • 对于 NSNumberNSDate、小 NSString 等对象,值直接编码在指针中。
  • 判断:最低位为 1(arm64 下 0x3 标记)。
  • 优点:无需 malloc 分配堆内存、无需 retain/release、访问更快。
  • 注意:isEqual: 可以比较,== 不一定(不同 Tagged Pointer 编码)。

内存泄漏场景排查

4 种常见循环引用链
场景原因修复
Block 属性VC 持有 Block,Block 捕获 self[weak self]
NSTimerTarget 强引用 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_unretainedweak:自动置 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 机制

动手实践

  1. 用断点验证 weak 对象在 dealloc 后自动置 nil
  2. 在 @autoreleasepool 嵌套中打印哨兵地址
  3. 实现一个简单的内存泄漏检测器

高级(P1)— 建立内存治理体系

  • 能设计内存监控和预警体系(水位/峰值/OOM 关联)
  • 理解 CF 桥接与 __bridge/__bridge_retained/__bridge_transfer
  • 能设计并推动稳定性门禁(内存回归检测)
  • 了解 VM Tracker / malloc stack logging 等高级调试工具
  • 能为团队沉淀内存治理规范文档

实战项目

  1. 为 App 搭建内存水位监控 Dashboard
  2. 设计循环引用自动检测规则(基于 Malloc Scribble + 堆栈分析)
  3. 在 CI 中集成 OOM 回归检测