Skip to content

内存管理

面试点

内存布局,内存管理方案,数据结构,ARC&MRC,引用计数,弱引用,自动释放池,循环引用

内存布局

  • stack: 方法调用
  • heap: 通过 alloc 等分配的对象
  • bss; 未初始化的全局变量等
  • data: 已初始化的全局变量等
  • text: 程序代码

内存管理方案

1. TaggedPointer

Tagged Pointer 是一种内存优化技术,主要用于存储小对象,例如小整数、短字符串等。通常情况下,对象指针指向对象的内存地址,但对于 Tagged Pointer,指针本身就包含了对象的数据,而不是指向堆上的内存。这种方法可以节省内存分配和释放的开销,同时提高访问速度。

工作原理:

  • Tagged Pointer 通过将数据直接嵌入指针中来避免堆内存分配。
  • 当系统检测到一个值可以被表示为 Tagged Pointer 时,它会将该值直接存储在指针中。
  • 特殊的位模式被用来区分普通指针和 Tagged Pointer。

示例:

在 Objective-C 中,某些小对象(如 NSNumber 和 NSString 的某些实例)可能会使用 Tagged Pointer。例如,小的整数和短的字符串可以直接存储在指针本身中。

2. NONPOINTER_ISA (非指针类型的 ISA)

在 Objective-C 运行时,每个对象都有一个 isa 指针,指向对象的类。为了提高性能和减少内存开销,Apple 引入了 Non-pointer ISA 技术。

Non-pointer ISA 的特点:

  • 传统的 isa 指针只是一个指向类对象的指针,但 Non-pointer ISA 使用位字段(bitfields)来存储额外的信息。
  • 除了指向类对象外,Non-pointer ISA 还包含其他信息,如引用计数、对象是否已经被标记为垃圾收集等。
  • 通过这种方式,一个 isa 指针可以存储更多信息,从而提高内存使用效率和访问速度。

示例:

struct {
    uintptr_t nonpointer        : 1;
    uintptr_t has_assoc         : 1;
    uintptr_t has_cxx_dtor      : 1;
    uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
    uintptr_t magic             : 6;
    uintptr_t weakly_referenced : 1;
    uintptr_t deallocating      : 1;
    uintptr_t has_sidetable_rc  : 1;
    uintptr_t extra_rc          : 19;
} bits;

3. 散列表

散列表是一种用于 Objective-C 运行时内存管理的结构,用于优化引用计数和弱引用的处理。

散列表的组成:

  • Side Table:Objective-C 使用 Side Table 来管理额外的引用计数信息。当对象的引用计数过高而无法存储在 Non-pointer ISA 中时,会使用 Side Table。
  • Weak Table:存储弱引用信息。当对象被弱引用时,Weak Table 记录这些引用以便在对象被释放时通知所有弱引用。
  • Per-thread Recycler List:用于管理每个线程的本地回收列表,以提高多线程环境下的性能。

工作原理:

  • 当对象的引用计数增加或减少时,可能会访问 Side Table 来更新引用计数。
  • 当对象被弱引用时,Weak Table 会记录这些引用,以便在对象被释放时将弱引用设置为 nil。
  • Per-thread Recycler List 可以减少线程间的锁争用,提高多线程性能。
struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
};

数据结构

1. Spinlock_t

  • Spinlock_t 是 “忙等” 的锁。
  • 适用于轻量访问。

2. RefcountMap

引用计数表通过 hash map 是现实的,插入和获取都是通过 hash 函数实现的,效率很高,避免了 for 循环遍历。

3. weak_table_t

MRC & ARC

1. 手动引用计数

alloc, retain, release, retainCount, autorelease, dealloc

2. 自动引用计数

  • ARC 是 LLVM 和 runtime 协作的结果。
  • ARC 中禁止手动调用 retain/release/retainCount/dealloc
  • ARC 中新增 weak, strong 属性关键字

引用计数管理

1. alloc

经过一系列调用,最终调用了C函数calloc。

此时并没有设置引用计数为1。

2. retain

我们在 retain 操作的时候,系统是怎么查找他的引用计数的?

经过两次 hash 查找,然后+1 操作。

3. release

操作和 retain 刚好相反

4. retainCount

5. dealloc

弱引用管理


在 iOS(Swift 和 Objective-C)中,weak 主要用于 解决循环引用,并且由 Objective-C 运行时(Runtime)和 ARC(自动引用计数) 管理。它的实现原理涉及 弱引用表(Weak Table)自动置 nil 机制。


1. weak 的特点

  • 不会增加对象的引用计数(retain count)
  • 对象销毁时,所有 weak 引用会自动置为 nil(防止野指针)。
  • 存储在全局的 weak table(弱引用表)中,用于跟踪 weak 引用。

2. weak 的底层实现

当你在 Swift 或 Objective-C 中声明一个 weak 变量时,底层会执行以下操作:

(1)注册到全局 Weak Table

weak 变量不会直接存储对象地址,而是通过 Objective-C 运行时弱引用表(Weak Table) 进行管理。

  • Weak Table 是一个 哈希表hash map),键是对象的地址(object pointer),值是所有指向它的 weak 指针列表。
  • 每次有新的 weak 变量指向对象,都会在 Weak Table 里添加一个条目。

示例代码

class Person {
    var name: String
    init(name: String) { self.name = name }
    deinit { print("\(name) deallocated") }
}

class Student {
    weak var teacher: Person?
}

var p: Person? = Person(name: "Alice")
var s = Student()
s.teacher = p  // `p` 对象被 `weak` 持有

print(s.teacher?.name) // Alice

p = nil  // `Person` 对象被释放
print(s.teacher?.name) // nil  (自动置 `nil`)

底层流程 1. 创建 Person 对象 p,在 Heap(堆)上分配内存,引用计数为 1。 2. s.teacher 通过 weak 持有 p,不会增加 p 的引用计数,而是 在 Weak Table 里注册该关系。 3. p = nil 使 Person 计数变 0,对象被释放,Weak Table 里所有指向 pweak 变量被自动置 nil


(2)对象释放时,自动置 nil

weak 变量指向的对象被销毁时: - Weak Table 里所有指向它的 weak 引用会被遍历。 - 每个 weak 指针都会被置 nil,防止访问野指针。

源码层面 - objc_storeWeak() 负责写入 weak 变量。 - objc_destroyWeak() 负责对象销毁时清除 weak 记录并置 nil

代码示例

var p: Person? = Person(name: "Bob")
weak var weakP = p

print(weakP) // Bob

p = nil  // 触发 `objc_destroyWeak`
print(weakP) // nil  (防止访问已销毁对象)

3. weak vs unowned

特性 weak unowned
引用计数 不增加 不增加
可为空(Optional) 是(必须 var weak var x: Type? 否(非 Optional
对象销毁时 自动置 nil 访问已销毁对象会 Crash
适用场景 delegate,防止循环引用 父子关系,child → parent

示例

class Person {
    var name: String
    init(name: String) { self.name = name }
    deinit { print("\(name) deallocated") }
}

class Student {
    unowned var teacher: Person  // `teacher` 不能是 `nil`

    init(teacher: Person) {
        self.teacher = teacher
    }
}

var t: Person? = Person(name: "Alice")
var s = Student(teacher: t!)  // 需要强制解包

t = nil  // Person 被释放
print(s.teacher.name) // ❌ Crash!(野指针)
  • weak 适用于可能变 nil 的对象(如 delegate)。
  • unowned 适用于不会变 nil 的情况(如 child 持有 parent)。

4. weak 的优化

Apple 对 weak 进行了优化: 1. 高效的 Weak Table 结构 - 维护 object → weak references 的映射关系,快速查找和释放。 - Weak Table 采用 稀疏哈希表(Sparse Hash Map),减少内存占用。

  1. 优化 weak 变量的置 nil 过程
  2. 只更新 Weak Table 中的 weak 指针,而不是遍历所有对象。

5. weak 相关的 API

  • objc_initWeak():初始化 weak 变量并注册到 Weak Table
  • objc_storeWeak():将 weak 变量写入 Weak Table
  • objc_destroyWeak():对象销毁时,从 Weak Table 移除 weak 引用并置 nil

总结

  1. weak 不会增加对象的引用计数,而是存储在 Weak Table 中。
  2. 对象释放时,所有 weak 指针会被置 nil,避免访问野指针。
  3. weak 适用于 可能变 nil 的对象,如 delegate,而 unowned 适用于不会变 nil 的对象。
  4. Apple 通过 Weak Table 结构和优化的 nil 置空机制,确保 weak 的性能和安全性。

🚀 你在具体什么场景遇到 weak 相关的问题?要不要结合你的项目分析下? 😃


autoreleasepool 在 iOS(Swift & Objective-C)中的作用和原理

在 iOS 和 macOS 开发中,autoreleasepool 主要用于 手动管理临时对象的内存释放,优化内存使用,避免内存峰值过高。


1. 什么是 autoreleasepool

  • autoreleasepool自动释放池,用于管理 autorelease 过的对象。
  • autorelease 过的对象不会立即释放,而是等到 自动释放池销毁时 统一释放。
  • 在大量创建临时对象时,可以手动使用 autoreleasepool 来加速释放,优化内存占用。

2. autoreleasepool 在 Swift 中的使用

Swift 通过 autoreleasepool {} 语法提供了 @autoreleasepool 机制:

for i in 0..<100000 {
    autoreleasepool {
        let image = UIImage(named: "large_image") // 加载大图片
        // 这里 `image` 在 `autoreleasepool` 作用域结束后会被释放
    }
}

工作流程

  1. autoreleasepool {} 进入作用域,创建一个新的 自动释放池
  2. 作用域内的 autorelease 对象被加入该 autoreleasepool
  3. 作用域结束,autoreleasepool 释放,所有对象的引用计数 -1,如果没有其他强引用,则内存释放。

3. autoreleasepool 在 Objective-C 中的使用

在 Objective-C 中,使用 @autoreleasepool {}

for (int i = 0; i < 100000; i++) {
    @autoreleasepool {
        UIImage *image = [UIImage imageNamed:@"large_image"];
        // `image` 作用域结束后自动释放
    }
}

MRC(手动引用计数) 中:

NSString *str = [[NSString alloc] initWithFormat:@"Hello"];
[str autorelease]; // `str` 会在 `autoreleasepool` 销毁时释放

4. autoreleasepool 何时使用?

  1. 大量创建临时对象(避免短时间内占用大量内存) swift for _ in 0..<1000000 { autoreleasepool { let data = NSData(contentsOfFile: "large_file") // 作用域结束后,`data` 立即释放 } }
  2. 手动管理大对象释放(如图片、音频等) swift func loadImages() { for i in 0..<1000 { autoreleasepool { let image = UIImage(contentsOfFile: "image_\(i).png") processImage(image) } // 作用域结束后 `image` 立即释放 } }
  3. 避免 RunLoop 期间内存激增
  4. UIKitrunLoop 在 UI 事件循环结束时自动调用 autoreleasepool
  5. 但在 for 循环、dispatchQueue后台任务 中可能不会触发自动释放池,需要手动加 autoreleasepool

5. autoreleasepool 的底层实现

autoreleasepool 底层由 Objective-C Runtime 维护的 Autorelease Pool Page 机制支持。

  • objc_autoreleasePoolPush():创建 autoreleasepool,入栈。
  • objc_autoreleasePoolPop():销毁 autoreleasepool,出栈,释放对象。

底层结构 autoreleasepool 其实是 多个 autorelease 过的对象组成的栈

[ pool1 ] ---> [ Object A ]
               [ Object B ]
[ pool2 ] ---> [ Object C ]
  • pool2pop 时,Object C 释放。
  • pool1pop 时,Object AObject B 释放。

6. autoreleasepool vs defer

Swift 的 defer 也是延迟执行的机制,但作用不同:

对比项 autoreleasepool {} defer
作用 释放 autorelease 对象 代码块在作用域结束时执行
触发时机 autoreleasepool 退出时 defer 作用域结束时
应用场景 内存管理 关闭文件、解锁资源

示例

defer {
    print("defer 执行")
}
autoreleasepool {
    print("autoreleasepool 执行")
}
print("结束")

输出

autoreleasepool 执行
结束
defer 执行
  • autoreleasepool 里的代码 立即执行,作用域结束时释放对象。
  • defer 最后执行,适用于资源清理。

7. autoreleasepool 相关面试问题

💡 Q1: 为什么 autoreleasepool 在 iOS 需要手动使用?
👉 iOS 由 RunLoop 自动管理 autoreleasepool,但在 循环创建大量对象子线程 时需要手动创建。

💡 Q2: autoreleasepool 什么时候清空?
👉 RunLoop 每次循环结束时清空,或者 autoreleasepool 作用域结束时清空。

💡 Q3: autoreleasepool 作用于 @discardableResult 吗?
👉 作用于所有 autorelease 对象,但 @discardableResult 只影响返回值,不影响内存管理。


8. 总结

特性 描述
作用 延迟释放 autorelease 对象,减少内存峰值
适用场景 循环创建临时对象、大型数据处理、后台线程
自动 vs 手动 主线程 RunLoop 自动管理,子线程需要手动创建
底层机制 使用 objc_autoreleasePoolPush()objc_autoreleasePoolPop() 管理 Autorelease Pool Page

🚀 你在什么场景遇到 autoreleasepool 相关问题?可以一起优化代码! 😃