一、ARC机制深度剖析

ARC(Automatic Reference Counting)是苹果为Swift和Objective-C提供的自动引用计数系统。理解ARC的底层实现机制,是写出高性能iOS代码的前提。

1.1 ARC的工作原理

ARC并非垃圾回收,它在编译期插入retain和release调用,通过引用计数管理对象生命周期。这意味着每个对象的内存管理都有确定性的开销。

ARC插入的引用计数操作

  • 强引用赋值:objc_storeStrong() — 先retain新值,再release旧值,最后store新值
  • 局部变量初始化:默认strong,作用域结束时release
  • weak/unowned:不改变引用计数,引用失效时自动置nil(weak)或抛异常(unowned)
  • Autorelease Pool:在 drain 时批量release,缓解即时release压力

1.2 引用计数开销分析

一次强引用赋值在ARC下执行三个操作。来看一段实际的汇编验证:

class HeavyObject {
    var data: [Byte] = Array(repeating: 0, count: 1024 * 1024) // 1MB
}

func testARC() {
    var obj1: HeavyObject? = HeavyObject()  // alloc + retain
    var obj2 = obj1                        // objc_storeStrong: retain obj1 → release old → store
    obj2 = nil                             // release obj2
    obj1 = nil                             // release obj1 → dealloc
}
// 编译后 objc_storeStrong 汇编(简化):
// MOV    QWORD PTR [rbp-0x10], rdi   ; store new value
// CMP    QWORD PTR [rbp-0x10], 0x0  ; check if nil
// JZ     .LBB0_3                   ; jump to release
// CALL   swift_retain               ; objc_storeStrong
// MOV    RAX, QWORD PTR [rbp-0x18] ; load old value
// CALL   swift_release              ; release old

每次引用赋值都有 retain/release 开销,在高频路径(如列表滚动、动画更新)上这个开销会被放大。

二、循环引用检测与解决

2.1 循环引用的四种典型场景

// 场景1:强引用闭包(最常见)
class ViewController {
    var callback: (() -> Void)?

    func setup() {
        // 闭包捕获 self(强引用)→ 循环引用
        callback = { [weak self] in
            self?.doSomething()  // self 被闭包持有,self 持有闭包
        }
    }
}

// 场景2:Delegate强引用
class NetworkManager {
    var delegate: NetworkDelegate? // 如果delegate也强引用self → 循环引用
}

// 场景3:Timer强引用
class MyViewController {
    var timer: Timer?
    func startTimer() {
        // Timer持有target,target的deinit不会调用
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            self?.updateUI()
        }
    }
}

// 场景4:GCD异步逃逸闭包
class DataService {
    func fetchData(completion: @escaping (Data?) -> Void) {
        DispatchQueue.global().async {
            // completion 闭包逃逸,捕获 self
            let data = self.loadData() // self → completion → 循环
            completion(data)
        }
    }
}

2.2 循环引用检测工具链

  • Instruments → Leaks:运行应用,执行可疑操作,检测"Leaked"和"Abandoned"对象
  • Memory Graph(Xcode):Debug Memory Graph 按钮,用白色双圈图标定位循环引用链
  • Heap Graph分析:在Memory Graph中,右键对象 → "Path to Retain Cycle",找到循环引用路径
  • MLeaksFinder(微信开源):通过swizzle dealloc检测ViewController是否泄漏

2.3 最佳实践:@weakify/@strongify

// 使用@weakify/@strongify宏(libextobjc)
@import extobjc;

- (void)setup {
    // 在异步操作前同时声明weak和strong
    @weakify(self);
    self.operation = ^{
        @strongify(self);
        if (!self) return;

        // self 在整个闭包内有效,中间任何时刻变nil都不影响后续执行
        [self.networkClient requestDataWithCompletion:^(id data) {
            @strongify(self);
            if (!self) return;
            [self updateUIWithData:data];
        }];
    };
}

// Swift等价实现(手写)
func setup() {
    operation = { [weak self] in
        guard let self = self else { return }
        // 局部强引用贯穿整个闭包,避免重复guard
        networkClient.requestData { [weak self] data in
            guard let self = self else { return }
            self.updateUI(with: data)
        }
    }
}

三、图片内存优化策略

3.1 图片解码与内存占用

UIImage(imageNamed:) 和 imageWithContentsOfFile: 的一个关键区别:前者会在主线程同步解码图片,后者则按需解码。高分辨率图片的解码会消耗大量内存和CPU。

// 场景:加载一张 4032x3024 的 HEIC 图片(约3MB文件)
// 解码后内存占用:4032 × 3024 × 4 bytes(RGBA) ≈ 48MB

// 错误示范:在主线程解码大图
let image = UIImage(named: "large_photo.jpg")
self.imageView.image = image  // 触发隐式解码,阻塞主线程

// 正确示范:后台解码 + 缩放到目标尺寸
func loadImageOptimized(named: String, targetSize: CGSize) -> UIImage? {
    guard let image = UIImage(named: named),
          let cgImage = image.cgImage else { return nil }

    // 计算缩放比例
    let scale = min(
        targetSize.width / CGFloat(cgImage.width),
        targetSize.height / CGFloat(cgImage.height)
    )
    guard scale < 1.0 else { return image }

    let width = CGFloat(cgImage.width) * scale
    let height = CGFloat(cgImage.height) * scale

    // 在后台队列解码(避免主线程阻塞)
    return DispatchQueue.global(qos: .userInitiated).sync {
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)

        guard let context = CGContext(
            data: nil, width: Int(width), height: Int(height),
            bitsPerComponent: 8, bytesPerRow: 0,
            space: colorSpace, bitmapInfo: bitmapInfo.rawValue
        ) else { return nil }

        context.interpolationQuality = .high
        context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))

        return context.makeImage().map { UIImage(cgImage: $0) }
    }
}

3.2 图片缓存架构

成熟的三级缓存策略:内存 → 磁盘 → 网络。实现时需注意NSMapTable对weak key的支持:

class ImageCache {
    // 内存缓存:NSCache 自动处理内存压力(后台线程回收)
    private let memoryCache = NSCache()
    // 磁盘缓存:Key-Value数据库(SQLite.swift)
    private let diskCache = DiskCache(directory: "image_cache")
    // 内存中的弱引用缓存(用于并发安全地共享同一图片对象)
    private let sharedImages = NSMapTable(
        options: .weakMemory, capacity: 50
    )

    func image(forKey key: String) -> UIImage? {
        // 1. 内存缓存
        if let image = memoryCache.object(forKey: key as NSString) {
            return image
        }
        // 2. 磁盘缓存 + 解码 + 存入内存
        if let data = diskCache.read(key: key) {
            let image = decodeImage(from: data)
            memoryCache.setObject(image, forKey: key as NSString)
            return image
        }
        return nil
    }

    func store(_ image: UIImage, forKey key: String) {
        memoryCache.setObject(image, forKey: key as NSString)
        DispatchQueue.global(qos: .background).async { [weak self] in
            guard let data = image.jpegData(compressionQuality: 0.8) else { return }
            self?.diskCache.write(data: data, key: key)
        }
    }
}

四、Autorelease Pool优化

4.1 Autorelease Pool的运行机制

Autorelease Pool 用于延迟对象的 release 调用。在 drain 时,所有标记了 autorelease 的对象会统一执行 release。这个机制在循环中创建大量临时对象时尤为重要。

4.2 循环中局部池的必要性

// 错误示范:大量临时字符串在pool drain时才释放
func processStrings(bigArray: [String]) {
    var results: [NSAttributedString] = []
    for str in bigArray { // 假设10000次循环
        // 每次循环的 attributedString都是autorelease
        let attr = NSAttributedString(string: str, attributes: [.font: UIFont.systemFont])
        results.append(attr)
    }
    // 整个循环完成后才drain pool,10000个NSAttributedString
    // 同时占用内存,可能触发memory warning
}

// 正确示范:每批创建局部Autorelease Pool
func processStringsOptimized(bigArray: [String]) {
    var results: [NSAttributedString] = []
    let batchSize = 100

    for batch in stride(from: 0, to: bigArray.count, by: batchSize) {
        autoreleasepool {
            let endIndex = min(batch + batchSize, bigArray.count)
            for i in batch..

五、线上内存监控体系

5.1 OOM(Out Of Memory)预防

iOS的jetsam机制会在内存紧张时强制杀死进程,无法被应用捕获。但我们可以通过监控memory footprint提前预警:

class MemoryMonitor {
    private var timer: Timer?

    func startMonitoring() {
        timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in
            self?.checkMemoryPressure()
        }
    }

    private func checkMemoryPressure() {
        var info = mach_task_basic_info()
        var count = mach_msg_type_number_t(MemoryLayout.size) / 4
        let result = withUnsafeMutablePointer(to: &info) {
            $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
                task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
            }
        }

        guard result == KERN_SUCCESS else { return }
        let usedBytes = info.resident_size
        let limitBytes = os_proc_available_memory() // 系统可用内存

        // 当应用占用超过系统可用内存的50%时预警
        if usedBytes > limitBytes * 0.5 {
            reportMemoryWarning(used: usedBytes, limit: limitBytes)
        }
    }

    private func reportMemoryWarning(used: UInt64, limit: UInt64) {
        // 触发内存释放策略
        ImageCache.shared.clearMemoryCache()
        NotificationCenter.default.post(name: .memoryPressureWarning, object: nil)
    }
}

extension Notification.Name {
    static let memoryPressureWarning = Notification.Name("MemoryPressureWarning")
}

5.2 大图自动降级策略

内存优化检查清单

  • 列表页图片必须设置明确的 size,禁用 ImageIO 动态解码
  • Controller dismiss 后立即释放大图引用,不要等 dealloc
  • 后台处理 JSON 序列化时,用小的 autoreleasepool 批量化
  • 音频/视频资源用 CMBlockBuffer 替代直接加载到内存
  • 监控 self.view.window == nil 时主动释放渲染资源