普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月26日首页

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

作者 Sheffi
2025年11月26日 11:37

前言

内存管理是 iOS 开发中最核心的知识点之一,理解透彻的内存管理机制不仅能帮助我们写出高质量的代码,还能有效避免内存泄漏、野指针等常见问题。本文将从底层原理到实际应用,全面剖析 iOS 的内存管理机制。


一、内存管理的演进历程

1.1 MRC 时代(Manual Reference Counting)

在 iOS 5 之前,开发者需要手动管理对象的生命周期:

// 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 代码:

// ARC 时代 - 编译器自动管理
func createObject() {
    let obj = MyClass()  // 编译器插入 retain
    // 使用 obj
}  // 函数结束,编译器插入 release

⚠️ 重要提示:ARC 不是垃圾回收(GC),它是编译时特性,不会带来运行时开销。


二、引用计数的底层实现

2.1 isa 指针与 SideTable

在 64 位系统中,苹果对 isa 指针进行了优化,采用了 Non-pointer isa 结构:

┌─────────────────────────────────────────────────────────────────┐
                        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:

struct SideTable {
    spinlock_t slock;           // 自旋锁,保证线程安全
    RefcountMap refcnts;        // 引用计数表(哈希表)
    weak_table_t weak_table;    // 弱引用表
};

系统维护了一个 SideTables 哈希表,通过对象地址快速定位到对应的 SideTable:

// 获取对象的引用计数
static inline RefcountMap::iterator 
getRefcountMap(objc_object *obj) {
    SideTable& table = SideTables()[obj];
    return table.refcnts.find(obj);
}

2.3 retain 和 release 的源码分析

// 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(强引用)

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:

class Apartment {
    let unit: String
    weak var tenant: Person?  // 弱引用,避免循环引用
    
    init(unit: String) {
        self.unit = unit
    }
}

弱引用的底层实现

// 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(无主引用)

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 闭包中的引用

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 模式

// ❌ 错误示例
protocol DownloadDelegate: AnyObject {  // 注意这里必须用 AnyObject
    func downloadDidComplete()
}

class DownloadManager {
    var delegate: DownloadDelegate?  // ❌ 强引用导致循环
}

// ✅ 正确示例
class DownloadManager {
    weak var delegate: DownloadDelegate?  // ✅ 弱引用
}

场景二:闭包捕获

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

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

步骤:
1. Xcode -> Product -> Profile (⌘I)
2. 选择 Leaks
3. 运行并操作 App
4. 查看泄漏点和调用栈

使用 Debug Memory Graph

// 在特定点触发内存警告,观察对象是否正确释放
#if DEBUG
extension UIViewController {
    func checkMemoryLeak() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
            if self != nil {
                print("⚠️ 可能存在内存泄漏: \(type(of: self!))")
            }
        }
    }
}
#endif

自定义泄漏检测工具

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 工作原理

┌──────────────────────────────────────────────────────────────────┐
│                    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 源码分析

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

┌──────────────────────────────────────────────────────────────────┐
                    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

// 场景:大量临时对象的循环
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 直接在指针中存储数据:

┌──────────────────────────────────────────────────────────────────┐
│                    Tagged Pointer 结构                            │
├──────────────────────────────────────────────────────────────────┤
│                                                                   │
│   普通对象指针:                                                   │
│   ┌─────────────────────────────────────────────────────────┐    │
│   │         64位地址指向堆中的对象                            │    │
│   └─────────────────────────────────────────────────────────┘    │
│                              │                                    │
│                              ▼                                    │
│   ┌─────────────────────────────────────────────────────────┐    │
│   │                    堆中的对象                             │    │
│   │  ┌──────┬──────────┬──────────┬─────────────────────┐   │    │
│   │  │ isa  │ refCount │ 其他信息  │      实际数据       │   │    │
│   │  └──────┴──────────┴──────────┴─────────────────────┘   │    │
│   └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│   Tagged Pointer:                                                │
│   ┌─────────────────────────────────────────────────────────┐    │
│   │ 1 │ 类型标记(3位) │           数据值(60位)              │    │
│   └─────────────────────────────────────────────────────────┘    │
│     ↑                                                             │
│   标记位(表明这是Tagged Pointer)                                   │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘

6.2 判断 Tagged Pointer

// 通过内存地址判断(仅供理解,实际开发中不需要关心)
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 性能优势

// 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 图片内存优化

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 大数据处理

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 内存管理

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 命令

# 查看对象引用计数
(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 中添加:

MallocStackLogging = 1          # 记录内存分配堆栈
MallocStackLoggingNoCompact = 1 # 不压缩堆栈信息
OBJC_DEBUG_POOL_ALLOCATION = YES # 调试 Autorelease Pool
NSZombieEnabled = YES           # 检测野指针

8.3 自定义内存追踪

#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. 调试技巧:常用的调试命令和工具

参考资料

❌
❌