让我们来聊聊Swift开发中那些让人头疼的内存泄漏问题。作为iOS开发者,你可能经常遇到应用运行一段时间后内存不断增长,最终导致性能下降甚至崩溃的情况。今天我们就来深入分析Swift中常见的内存泄漏场景,并提供实用的解决方案。

一、循环引用导致的泄漏

这是Swift中最常见的泄漏类型,主要发生在类实例之间互相强引用时。让我们看一个典型例子:

class Person {
    let name: String
    var apartment: Apartment?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name)被释放了")
    }
}

class Apartment {
    let unit: String
    var tenant: Person?
    
    init(unit: String) {
        self.unit = unit
    }
    
    deinit {
        print("公寓\(unit)被释放了")
    }
}

// 创建实例
var john: Person? = Person(name: "John")
var unit4A: Apartment? = Apartment(unit: "4A")

// 建立互相引用
john?.apartment = unit4A
unit4A?.tenant = john

// 即使设置为nil,也不会释放内存
john = nil
unit4A = nil

在这个例子中,Person和Apartment互相持有对方的强引用,形成了引用循环。即使我们将john和unit4A设为nil,这两个实例也不会被释放,因为它们的引用计数永远不会降到0。

解决方案是使用weak或unowned关键字打破循环引用:

class Apartment {
    let unit: String
    weak var tenant: Person?  // 使用weak打破循环
    
    init(unit: String) {
        self.unit = unit
    }
}

二、闭包引起的循环引用

闭包是Swift中的一等公民,但它们也容易造成内存泄漏。看这个例子:

class HTMLElement {
    let name: String
    let text: String?
    
    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    
    deinit {
        print("\(name)被释放了")
    }
}

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello")
print(paragraph!.asHTML())
paragraph = nil  // 不会被释放

这里asHTML闭包捕获了self,而闭包又被self持有,形成了循环引用。解决方案是使用捕获列表:

lazy var asHTML: () -> String = { [weak self] in
    guard let self = self else { return "" }
    if let text = self.text {
        return "<\(self.name)>\(text)</\(self.name)>"
    } else {
        return "<\(self.name) />"
    }
}

三、定时器未正确释放

Timer是另一个常见的内存泄漏源:

class ViewController: UIViewController {
    var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        timer = Timer.scheduledTimer(timeInterval: 1.0, 
                                   target: self, 
                                   selector: #selector(update), 
                                   userInfo: nil, 
                                   repeats: true)
    }
    
    @objc func update() {
        print("定时器触发")
    }
    
    deinit {
        print("ViewController被释放")
    }
}

这个ViewController永远不会被释放,因为Timer保持了对它的强引用。解决方案是:

  1. 使用weak self:
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
    self?.update()
}
  1. 在适当的时候手动invalidate:
deinit {
    timer?.invalidate()
}

四、委托模式中的强引用

委托模式使用不当也会导致内存泄漏:

protocol DataLoaderDelegate: AnyObject {
    func dataLoaded(data: [String])
}

class DataLoader {
    var delegate: DataLoaderDelegate?  // 应该是weak
    
    func loadData() {
        // 模拟数据加载
        delegate?.dataLoaded(data: ["数据1", "数据2"])
    }
}

class ViewController: UIViewController, DataLoaderDelegate {
    let loader = DataLoader()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        loader.delegate = self
        loader.loadData()
    }
    
    func dataLoaded(data: [String]) {
        print("数据加载完成: \(data)")
    }
}

这里DataLoader强引用了ViewController,而ViewController又强引用了DataLoader,形成了循环。解决方案是将delegate声明为weak:

weak var delegate: DataLoaderDelegate?

五、观察者未及时移除

KVO和NotificationCenter的观察者如果不及时移除也会导致泄漏:

class Observer: NSObject {
    override init() {
        super.init()
        NotificationCenter.default.addObserver(self, 
                                             selector: #selector(handleNotification), 
                                             name: Notification.Name("Test"), 
                                             object: nil)
    }
    
    @objc func handleNotification() {
        print("收到通知")
    }
    
    deinit {
        print("Observer被释放")
    }
}

var observer: Observer? = Observer()
observer = nil  // 不会被释放,因为NotificationCenter还持有它

解决方案是在deinit中移除观察者:

deinit {
    NotificationCenter.default.removeObserver(self)
    print("Observer被释放")
}

六、总结与最佳实践

通过以上例子,我们可以总结出一些避免内存泄漏的最佳实践:

  1. 对于可能形成循环引用的属性,使用weak或unowned修饰
  2. 在闭包中使用捕获列表[weak self]或[unowned self]
  3. 定时器、观察者等资源要及时释放
  4. 委托模式中delegate应该声明为weak
  5. 使用工具检测内存泄漏,如Xcode的Memory Graph Debugger

记住,良好的内存管理不仅能提升应用性能,还能带来更好的用户体验。在开发过程中,要养成定期检查内存使用情况的习惯,及时发现并修复潜在的内存泄漏问题。