前言
内存管理是 iOS 开发中最核心的知识点之一,理解透彻的内存管理机制不仅能帮助我们写出高质量的代码,还能有效避免内存泄漏、野指针等常见问题。本文将从底层原理到实际应用,全面剖析 iOS 的内存管理机制。
一、内存管理的演进历程
1.1 MRC 时代(Manual Reference Counting)
在 iOS 5 之前,开发者需要手动管理对象的生命周期:
objc
// MRC 时代的内存管理
NSObject *obj = [[NSObject alloc] init]; // retainCount = 1
[obj retain]; // retainCount = 2
[obj release]; // retainCount = 1
[obj release]; // retainCount = 0,对象被销毁
黄金法则:谁创建(alloc/new/copy/mutableCopy),谁释放(release)。
1.2 ARC 时代(Automatic Reference Counting)
iOS 5 引入 ARC 后,编译器自动在适当位置插入 retain/release 代码:
swift
// ARC 时代 - 编译器自动管理
func createObject() {
let obj = MyClass() // 编译器插入 retain
// 使用 obj
} // 函数结束,编译器插入 release
⚠️ 重要提示:ARC 不是垃圾回收(GC),它是编译时特性,不会带来运行时开销。
二、引用计数的底层实现
2.1 isa 指针与 SideTable
在 64 位系统中,苹果对 isa 指针进行了优化,采用了 Non-pointer isa 结构:
yaml
┌─────────────────────────────────────────────────────────────────┐
│ isa 指针结构(64位) │
├─────────────────────────────────────────────────────────────────┤
│ 0 │ indexed │ 0: 纯指针 1: 优化的isa │
│ 1 │ has_assoc │ 是否有关联对象 │
│ 2 │ has_cxx_dtor │ 是否有C++析构函数 │
│ 3-35 │ shiftcls │ 类指针(33位) │
│ 36-41 │ magic │ 用于调试 │
│ 42 │ weakly_ref │ 是否有弱引用 │
│ 43 │ deallocating │ 是否正在释放 │
│ 44 │ has_sidetable│ 引用计数是否存储在SideTable │
│ 45-63 │ extra_rc │ 额外的引用计数(19位) │
└─────────────────────────────────────────────────────────────────┘
2.2 SideTable 结构
当引用计数超出 isa 的存储范围时,会使用 SideTable:
cpp
struct SideTable {
spinlock_t slock; // 自旋锁,保证线程安全
RefcountMap refcnts; // 引用计数表(哈希表)
weak_table_t weak_table; // 弱引用表
};
系统维护了一个 SideTables 哈希表,通过对象地址快速定位到对应的 SideTable:
cpp
// 获取对象的引用计数
static inline RefcountMap::iterator
getRefcountMap(objc_object *obj) {
SideTable& table = SideTables()[obj];
return table.refcnts.find(obj);
}
2.3 retain 和 release 的源码分析
cpp
// objc_object::retain() 简化实现
id objc_object::retain() {
// 1. TaggedPointer 直接返回
if (isTaggedPointer()) return (id)this;
// 2. 尝试在 isa 的 extra_rc 中增加引用计数
if (fastpath(!ISA()->hasCustomRR())) {
if (fastpath(bits.extra_rc++ < RC_HALF)) {
return (id)this;
}
}
// 3. extra_rc 溢出,转移到 SideTable
return sidetable_retain();
}
三、四种引用类型详解
3.1 Strong(强引用)
swift
class Person {
var name: String
var apartment: Apartment? // 强引用
init(name: String) {
self.name = name
print("\(name) is initialized")
}
deinit {
print("\(name) is deinitialized")
}
}
3.2 Weak(弱引用)
弱引用不会增加引用计数,对象释放时自动置为 nil:
swift
class Apartment {
let unit: String
weak var tenant: Person? // 弱引用,避免循环引用
init(unit: String) {
self.unit = unit
}
}
弱引用的底层实现:
cpp
// weak_table_t 结构
struct weak_table_t {
weak_entry_t *weak_entries; // 弱引用入口数组
size_t num_entries; // 弱引用数量
uintptr_t mask; // 哈希掩码
uintptr_t max_hash_displacement; // 最大哈希偏移
};
// 当对象被释放时,清理所有弱引用
void weak_clear_no_lock(weak_table_t *weak_table, id referent) {
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) return;
// 将所有指向该对象的弱引用置为 nil
weak_referrer_t *referrers = entry->referrers;
for (size_t i = 0; i < entry->num_refs; i++) {
*referrers[i] = nil;
}
weak_entry_remove(weak_table, entry);
}
3.3 Unowned(无主引用)
swift
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
}
class CreditCard {
let number: UInt64
unowned let customer: Customer // 无主引用
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
}
| 特性 | weak | unowned |
|---|---|---|
| 引用计数 | 不增加 | 不增加 |
| 对象释放时 | 自动置 nil | 不处理(悬垂指针) |
| 声明类型 | Optional | Non-optional |
| 性能 | 略低(需维护weak表) | 较高 |
| 安全性 | 安全 | 需保证生命周期 |
3.4 闭包中的引用
swift
class HTMLElement {
let name: String
let text: String?
// ❌ 循环引用
lazy var asHTML: () -> String = {
return "<\(self.name)>\(self.text ?? "")</\(self.name)>"
}
// ✅ 使用捕获列表打破循环
lazy var asHTMLFixed: () -> String = { [weak self] in
guard let self = self else { return "" }
return "<\(self.name)>\(self.text ?? "")</\(self.name)>"
}
// ✅ 或使用 unowned(确保闭包执行时 self 存在)
lazy var asHTMLUnowned: () -> String = { [unowned self] in
return "<\(self.name)>\(self.text ?? "")</\(self.name)>"
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
}
四、常见内存问题与解决方案
4.1 循环引用
场景一:Delegate 模式
swift
// ❌ 错误示例
protocol DownloadDelegate: AnyObject { // 注意这里必须用 AnyObject
func downloadDidComplete()
}
class DownloadManager {
var delegate: DownloadDelegate? // ❌ 强引用导致循环
}
// ✅ 正确示例
class DownloadManager {
weak var delegate: DownloadDelegate? // ✅ 弱引用
}
场景二:闭包捕获
swift
class NetworkManager {
var completionHandler: (() -> Void)?
func fetchData() {
// ❌ 循环引用
completionHandler = {
self.handleData()
}
// ✅ 解决方案1:weak
completionHandler = { [weak self] in
self?.handleData()
}
// ✅ 解决方案2:在不需要时置空
defer { completionHandler = nil }
}
func handleData() {
print("Handle data")
}
}
场景三:Timer
swift
class TimerHolder {
var timer: Timer?
func startTimer() {
// ❌ Timer 对 target 强引用
timer = Timer.scheduledTimer(
timeInterval: 1.0,
target: self,
selector: #selector(tick),
userInfo: nil,
repeats: true
)
// ✅ 解决方案:使用 block API
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.tick()
}
}
@objc func tick() {
print("Tick")
}
deinit {
timer?.invalidate()
print("TimerHolder deinit")
}
}
4.2 内存泄漏检测
使用 Instruments - Leaks
markdown
步骤:
1. Xcode -> Product -> Profile (⌘I)
2. 选择 Leaks
3. 运行并操作 App
4. 查看泄漏点和调用栈
使用 Debug Memory Graph
swift
// 在特定点触发内存警告,观察对象是否正确释放
#if DEBUG
extension UIViewController {
func checkMemoryLeak() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
if self != nil {
print("⚠️ 可能存在内存泄漏: \(type(of: self!))")
}
}
}
}
#endif
自定义泄漏检测工具
swift
class LeakDetector {
static let shared = LeakDetector()
private var trackedObjects: [ObjectIdentifier: WeakBox<AnyObject>] = [:]
private let queue = DispatchQueue(label: "com.app.leakdetector")
struct WeakBox<T: AnyObject> {
weak var value: T?
let className: String
}
func track(_ object: AnyObject, file: String = #file, line: Int = #line) {
let id = ObjectIdentifier(object)
let className = String(describing: type(of: object))
queue.async {
self.trackedObjects[id] = WeakBox(value: object, className: className)
print("📍 Tracking: \(className) at \(file):\(line)")
}
}
func checkLeaks() {
queue.async {
for (id, box) in self.trackedObjects {
if box.value != nil {
print("⚠️ Potential leak: \(box.className)")
} else {
self.trackedObjects.removeValue(forKey: id)
}
}
}
}
}
五、Autorelease Pool 深度解析
5.1 工作原理
scss
┌──────────────────────────────────────────────────────────────────┐
│ Autorelease Pool 结构 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Page 1 │──>│ Page 2 │──>│ Page 3 │ │
│ │ (4096 B) │ │ (4096 B) │ │ (4096 B) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ obj1 │ │ obj5 │ │ obj9 │ │
│ │ obj2 │ │ obj6 │ │ obj10 │ │
│ │ obj3 │ │ obj7 │ │ ... │ │
│ │ obj4 │ │ obj8 │ │ │ │
│ │ SENTINEL │ │ │ │ │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ ▲ │
│ │ │
│ hotPage │
│ (当前页) │
│ │
└──────────────────────────────────────────────────────────────────┘
5.2 源码分析
cpp
class AutoreleasePoolPage {
static size_t const SIZE = PAGE_MAX_SIZE; // 4096 bytes
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic;
id *next; // 下一个可存放对象的位置
pthread_t const thread; // 所属线程
AutoreleasePoolPage *parent; // 父节点
AutoreleasePoolPage *child; // 子节点
uint32_t depth; // 深度
// 添加对象到 pool
static inline id *autoreleaseFast(id obj) {
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
}
return autoreleaseFullPage(obj, page);
}
// Pool 的 pop 操作
static inline void pop(void *token) {
AutoreleasePoolPage *page = pageForPointer(token);
id *stop = (id *)token;
// 释放对象
page->releaseUntil(stop);
// 删除空页
if (page->child) {
page->child->kill();
page->child = nil;
}
}
};
5.3 主线程 RunLoop 与 Autorelease Pool
yaml
┌──────────────────────────────────────────────────────────────────┐
│ RunLoop 与 AutoreleasePool │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Main RunLoop │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────┼─────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Entry │ │ Before │ │ Exit │ │
│ │ (Push) │ │ Waiting │ │ (Pop) │ │
│ │ Order: │ │ (Pop + │ │ Order: │ │
│ │ 高优先 │ │ Push) │ │ 低优先 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 时机说明: │
│ 1. kCFRunLoopEntry: 创建 AutoreleasePool (push) │
│ 2. kCFRunLoopBeforeWaiting: 释放旧pool (pop),创建新pool (push) │
│ 3. kCFRunLoopExit: 释放 AutoreleasePool (pop) │
│ │
└──────────────────────────────────────────────────────────────────┘
5.4 手动使用 Autorelease Pool
swift
// 场景:大量临时对象的循环
func processLargeData() {
for i in 0..<100000 {
// ❌ 不使用 autoreleasepool,临时对象会累积
let data = createTemporaryData(index: i)
process(data)
}
for i in 0..<100000 {
// ✅ 使用 autoreleasepool,每次迭代后释放临时对象
autoreleasepool {
let data = createTemporaryData(index: i)
process(data)
}
}
// ✅ 更优化的方案:批量处理
let batchSize = 1000
for batch in stride(from: 0, to: 100000, by: batchSize) {
autoreleasepool {
for i in batch..<min(batch + batchSize, 100000) {
let data = createTemporaryData(index: i)
process(data)
}
}
}
}
六、Tagged Pointer 优化
6.1 什么是 Tagged Pointer
对于小对象(如小的 NSNumber、NSDate),苹果使用 Tagged Pointer 直接在指针中存储数据:
scss
┌──────────────────────────────────────────────────────────────────┐
│ Tagged Pointer 结构 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 普通对象指针: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 64位地址指向堆中的对象 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 堆中的对象 │ │
│ │ ┌──────┬──────────┬──────────┬─────────────────────┐ │ │
│ │ │ isa │ refCount │ 其他信息 │ 实际数据 │ │ │
│ │ └──────┴──────────┴──────────┴─────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Tagged Pointer: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1 │ 类型标记(3位) │ 数据值(60位) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↑ │
│ 标记位(表明这是Tagged Pointer) │
│ │
└──────────────────────────────────────────────────────────────────┘
6.2 判断 Tagged Pointer
swift
// 通过内存地址判断(仅供理解,实际开发中不需要关心)
func isTaggedPointer(_ obj: AnyObject) -> Bool {
let pointer = Unmanaged.passUnretained(obj).toOpaque()
let value = UInt(bitPattern: pointer)
// 在 arm64 上,最高位为 1 表示 Tagged Pointer
// 在 x86_64 上,最低位为 1 表示 Tagged Pointer
#if arch(arm64)
return (value >> 63) == 1
#else
return (value & 1) == 1
#endif
}
6.3 性能优势
swift
// Tagged Pointer 的优势演示
func performanceTest() {
let iterations = 1_000_000
// 小数字 - 使用 Tagged Pointer
let start1 = CFAbsoluteTimeGetCurrent()
for _ in 0..<iterations {
let num = NSNumber(value: 42) // Tagged Pointer
_ = num.intValue
}
let time1 = CFAbsoluteTimeGetCurrent() - start1
// 大数字 - 使用普通对象
let start2 = CFAbsoluteTimeGetCurrent()
for _ in 0..<iterations {
let num = NSNumber(value: Int64.max) // 普通对象
_ = num.int64Value
}
let time2 = CFAbsoluteTimeGetCurrent() - start2
print("Tagged Pointer: \(time1)s") // 明显更快
print("普通对象: \(time2)s")
}
七、实战:内存优化最佳实践
7.1 图片内存优化
swift
class ImageLoader {
// 使用 NSCache 自动管理内存
private let cache = NSCache<NSString, UIImage>()
init() {
// 设置缓存限制
cache.countLimit = 100
cache.totalCostLimit = 50 * 1024 * 1024 // 50MB
// 监听内存警告
NotificationCenter.default.addObserver(
self,
selector: #selector(handleMemoryWarning),
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil
)
}
// 下采样加载大图
func loadDownsampledImage(at url: URL, targetSize: CGSize) -> UIImage? {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, imageSourceOptions) else {
return nil
}
let maxDimension = max(targetSize.width, targetSize.height) * UIScreen.main.scale
let downsampledOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimension
] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampledOptions) else {
return nil
}
return UIImage(cgImage: downsampledImage)
}
@objc private func handleMemoryWarning() {
cache.removeAllObjects()
}
}
7.2 大数据处理
swift
class DataProcessor {
// 分批处理大数组,避免内存峰值
func processBatched<T>(_ array: [T], batchSize: Int = 1000, handler: ([T]) -> Void) {
let totalCount = array.count
var processedCount = 0
while processedCount < totalCount {
autoreleasepool {
let endIndex = min(processedCount + batchSize, totalCount)
let batch = Array(array[processedCount..<endIndex])
handler(batch)
processedCount = endIndex
}
}
}
// 使用流式读取大文件
func processLargeFile(at url: URL, lineHandler: (String) -> Void) {
guard let fileHandle = try? FileHandle(forReadingFrom: url) else { return }
defer { try? fileHandle.close() }
let bufferSize = 4096
var buffer = Data()
while autoreleasepool(invoking: {
guard let chunk = try? fileHandle.read(upToCount: bufferSize), !chunk.isEmpty else {
return false
}
buffer.append(chunk)
while let range = buffer.range(of: Data("\n".utf8)) {
let lineData = buffer.subdata(in: 0..<range.lowerBound)
if let line = String(data: lineData, encoding: .utf8) {
lineHandler(line)
}
buffer.removeSubrange(0..<range.upperBound)
}
return true
}) {}
// 处理最后一行
if let lastLine = String(data: buffer, encoding: .utf8), !lastLine.isEmpty {
lineHandler(lastLine)
}
}
}
7.3 ViewController 内存管理
swift
class BaseViewController: UIViewController {
// 所有需要取消的任务
private var cancellables = Set<AnyCancellable>()
private var tasks = [Task<Void, Never>]()
deinit {
// 取消所有订阅
cancellables.removeAll()
// 取消所有 Task
tasks.forEach { $0.cancel() }
print("\(type(of: self)) deinit")
}
// 安全地添加通知观察者
func observe(_ name: Notification.Name, handler: @escaping (Notification) -> Void) {
NotificationCenter.default.publisher(for: name)
.sink { [weak self] notification in
guard self != nil else { return }
handler(notification)
}
.store(in: &cancellables)
}
// 安全地执行异步任务
func performTask(_ operation: @escaping () async -> Void) {
let task = Task { [weak self] in
guard self != nil else { return }
await operation()
}
tasks.append(task)
}
}
八、调试技巧
8.1 LLDB 命令
bash
# 查看对象引用计数
(lldb) p CFGetRetainCount(obj as CFTypeRef)
# 查看对象的弱引用
(lldb) p _objc_rootRetainCount(obj)
# 查看所有内存分配
(lldb) memory history <address>
# 查看 Autorelease Pool 中的对象
(lldb) po [NSAutoreleasePool showPools]
# 查看对象的 isa 信息
(lldb) p/x (uintptr_t)object_getClass(obj)
8.2 环境变量
在 Scheme 的 Environment Variables 中添加:
ini
MallocStackLogging = 1 # 记录内存分配堆栈
MallocStackLoggingNoCompact = 1 # 不压缩堆栈信息
OBJC_DEBUG_POOL_ALLOCATION = YES # 调试 Autorelease Pool
NSZombieEnabled = YES # 检测野指针
8.3 自定义内存追踪
swift
#if DEBUG
class MemoryTracker {
static let shared = MemoryTracker()
private var allocations: [String: Int] = [:]
private let queue = DispatchQueue(label: "memory.tracker")
func trackAlloc(_ className: String) {
queue.async {
self.allocations[className, default: 0] += 1
}
}
func trackDealloc(_ className: String) {
queue.async {
self.allocations[className, default: 0] -= 1
}
}
func report() {
queue.async {
print("=== Memory Report ===")
for (className, count) in self.allocations where count > 0 {
print("\(className): \(count) instances")
}
print("====================")
}
}
}
// 使用方式
class TrackedObject {
init() {
MemoryTracker.shared.trackAlloc(String(describing: Self.self))
}
deinit {
MemoryTracker.shared.trackDealloc(String(describing: Self.self))
}
}
#endif
总结
iOS 内存管理是一个深度话题,本文从以下几个方面进行了详细解析:
- 引用计数原理:从 MRC 到 ARC 的演进,以及底层 SideTable 的实现
- 四种引用类型:strong、weak、unowned 的区别和适用场景
- 循环引用:常见场景和解决方案
- Autorelease Pool:工作原理和使用时机
- Tagged Pointer:小对象优化机制
- 实战优化:图片处理、大数据处理等场景的最佳实践
- 调试技巧:常用的调试命令和工具