一、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 时主动释放渲染资源