内存管理¶
面试点¶
内存布局,内存管理方案,数据结构,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 里所有指向 p
的 weak
变量被自动置 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),减少内存占用。
- 优化
weak
变量的置nil
过程 - 只更新
Weak Table
中的weak
指针,而不是遍历所有对象。
5. weak
相关的 API¶
objc_initWeak()
:初始化weak
变量并注册到Weak Table
。objc_storeWeak()
:将weak
变量写入Weak Table
。objc_destroyWeak()
:对象销毁时,从Weak Table
移除weak
引用并置nil
。
总结¶
weak
不会增加对象的引用计数,而是存储在 Weak Table 中。- 对象释放时,所有
weak
指针会被置nil
,避免访问野指针。 weak
适用于 可能变nil
的对象,如delegate
,而unowned
适用于不会变nil
的对象。- 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` 作用域结束后会被释放
}
}
工作流程¶
autoreleasepool {}
进入作用域,创建一个新的 自动释放池。- 作用域内的
autorelease
对象被加入该autoreleasepool
。 - 作用域结束,
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
何时使用?¶
- 大量创建临时对象(避免短时间内占用大量内存)
swift for _ in 0..<1000000 { autoreleasepool { let data = NSData(contentsOfFile: "large_file") // 作用域结束后,`data` 立即释放 } }
- 手动管理大对象释放(如图片、音频等)
swift func loadImages() { for i in 0..<1000 { autoreleasepool { let image = UIImage(contentsOfFile: "image_\(i).png") processImage(image) } // 作用域结束后 `image` 立即释放 } }
- 避免 RunLoop 期间内存激增
UIKit
的runLoop
在 UI 事件循环结束时自动调用autoreleasepool
。- 但在
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 ]
- 当
pool2
被pop
时,Object C
释放。 - 当
pool1
被pop
时,Object A
和Object 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
相关问题?可以一起优化代码! 😃