iOS 内存管理深度解析:从原理到实践

前言

内存管理是 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 内存管理是一个深度话题,本文从以下几个方面进行了详细解析:

  1. 引用计数原理:从 MRC 到 ARC 的演进,以及底层 SideTable 的实现
  2. 四种引用类型:strong、weak、unowned 的区别和适用场景
  3. 循环引用:常见场景和解决方案
  4. Autorelease Pool:工作原理和使用时机
  5. Tagged Pointer:小对象优化机制
  6. 实战优化:图片处理、大数据处理等场景的最佳实践
  7. 调试技巧:常用的调试命令和工具

参考资料

相关推荐
HouWan31 分钟前
SwiftUI 小技巧:如何读取 SwiftUI Font 的详细信息
ios·swiftui·apple
李游Leo1 小时前
HarmonyOS 7 实战开发 05:把页面做到可上线状态
ios·harmonyos
终端安全笔记6 小时前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
传奇开心果编程7 小时前
【SwiftUI入门练中学】第1课 从零开始
学习·ui·ios·swiftui·swift
丿Wayne12 小时前
iOS Token 刷新实战(Actor 避免并发重复刷新)
ios·swift·token·actor·swift并发
终端安全笔记1 天前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
大熊猫侯佩1 天前
独立 App 首发完成 iPhone Duo 展开态适配
ios·swiftui·swift
光影少年1 天前
setImmediate 和 setTimeout(0) 的区别
android·前端·react.js·ios·前端框架
2501_916008891 天前
SSE 和 gRPC 流式接口如何抓包,调试 SSE 与 gRPC 流式接口
网络协议·计算机网络·网络安全·ios·adb·https·udp
HouWan2 天前
Flutter: MediaQuery.of(context) 为什么可能拖慢页面?
android·flutter·ios