Swift 自动引用计数(ARC)(长文讲解)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目:《Spring AI 项目实战》 正在持续爆肝中,基于 Spring AI + Spring Boot 3.x + JDK 21..., 点击查看 ;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观

在 Swift 开发中,内存管理是一个既基础又复杂的主题。对于初学者而言,手动管理内存容易引发崩溃或性能问题,而中级开发者则可能因疏忽导致内存泄漏。Swift 自动引用计数(ARC) 正是为了解决这些问题而设计的机制,它通过自动跟踪对象的引用关系,确保内存高效且安全地释放。本文将从零开始,逐步解析 ARC 的原理、常见问题及解决方案,并通过案例帮助读者深入理解这一核心概念。


什么是自动引用计数(ARC)?

自动引用计数(Automatic Reference Counting, ARC) 是 Swift 的内存管理机制,其核心是通过跟踪对象被引用的次数来判断是否需要释放内存。简单来说,当某个对象没有被任何引用指向时,ARC 会自动回收该对象占用的内存空间。

引用计数的比喻

想象你和朋友在公园里玩捉迷藏。每个朋友被找到后,游戏组织者会记录他们的“参与次数”。当参与次数归零时,他们就可以离开公园回家。在 Swift 中,对象就像这些朋友,而“参与次数”就是引用计数。只要至少有一个引用指向对象,它的计数就不会归零,对象就会一直“留在公园里”。

ARC 的工作流程

  1. 创建对象时:ARC 会为该对象分配内存空间,并初始化引用计数为 1(因为至少有一个变量或常量指向它)。
  2. 增加引用时:每当一个新变量或常量指向该对象,引用计数加 1。
  3. 减少引用时:当变量或常量超出作用域或被重新赋值时,引用计数减 1。
  4. 释放内存:当引用计数降为 0 时,ARC 会立即释放该对象的内存。

强引用与循环引用问题

强引用的定义

在 Swift 中,默认情况下,变量或常量对对象的引用是“强引用”(Strong Reference)。强引用会增加对象的引用计数,且只有当所有强引用被移除后,对象才会被释放。

示例代码:

class Person {  
    let name: String  
    init(name: String) {  
        self.name = name  
        print("\(name) 被创建")  
    }  
    deinit {  
        print("\(name) 被销毁")  
    }  
}  

var alice: Person?  
alice = Person(name: "Alice")  // Alice 的引用计数为 1  
var bob = alice               // Alice 的引用计数变为 2  
alice = nil                    // Alice 的引用计数变为 1(因为 bob 仍指向它)  
bob = nil                      // Alice 的引用计数归零,触发销毁  

循环引用的产生

循环引用(Retain Cycle)是指两个或多个对象之间相互持有强引用,导致彼此的引用计数永远无法降为 0,最终内存泄漏。

示例:

class Dog {  
    let name: String  
    unowned let owner: Person  // 这里需要特殊处理,避免循环  
    init(name: String, owner: Person) {  
        self.name = name  
        self.owner = owner  
        print("\(name) 被创建")  
    }  
    deinit { print("\(name) 被销毁") }  
}  

class Person {  
    let name: String  
    var dog: Dog?  
    init(name: String) {  
        self.name = name  
        print("\(name) 被创建")  
    }  
    deinit { print("\(name) 被销毁") }  
}  

var john: Person? = Person(name: "John")  
john?.dog = Dog(name: "Buddy", owner: john!) // 这里存在循环引用  
john = nil // 试图释放 John,但 Dog 的 owner 属性持有强引用,导致两者无法销毁  

循环引用的成因分析

在上述代码中,Dogowner 属性和 Persondog 属性形成了一个闭环:

  • DogPerson 有强引用(因为 owner 是强引用)。
  • Person 通过 dog 属性对 Dog 也有强引用。
    因此,当 john 被设为 nil 后,两者引用计数仍为 1,导致内存无法释放。

如何解决循环引用?

1. 使用 weak 关键字

weak 引用不会增加对象的引用计数,并且当对象被释放后,weak 引用会自动设为 nil。它适用于“非拥有关系”,例如父类与子类、观察者与被观察者等场景。

修改后的代码:

class Dog {  
    let name: String  
    unowned let owner: Person  // 改为 unowned 或 weak  
    init(name: String, owner: Person) {  
        self.name = name  
        self.owner = owner  
    }  
    deinit { print("\(name) 被销毁") }  
}  

// 使用 weak  
class Person {  
    let name: String  
    weak var dog: Dog?        // 将 dog 改为 weak  
    init(name: String) { self.name = name }  
    deinit { print("\(name) 被销毁") }  
}  

var john: Person? = Person(name: "John")  
john?.dog = Dog(name: "Buddy", owner: john!)  
john = nil // 此时 John 和 Buddy 都会被释放  

2. 使用 unowned 关键字

unownedweak 类似,但不会自动设为 nil,且假设引用始终有效。它适用于父对象生命周期长于子对象的场景(例如视图和控制器的关系)。

示例:

class ViewController {  
    unowned let label: UILabel  
    init(label: UILabel) {  
        self.label = label // 假设 ViewController 的生命周期不短于 label  
    }  
}  

3. 解决闭包中的循环引用

闭包默认会捕获外部变量的强引用,若闭包本身被强引用持有,会形成循环。此时可用 [weak self][unowned self] 来打破循环。

示例:

class TimerManager {  
    var completion: (() -> Void)?  
    init() {  
        completion = {  
            [weak self] in  
            print("Timer 完成")  
            self?.completion = nil // 手动断开引用  
        }  
    }  
    deinit { print("TimerManager 被销毁") }  
}  

var tm: TimerManager? = TimerManager()  
tm = nil // 此时 TimerManager 可以被释放  

实战案例:构建一个安全的观察者模式

观察者模式常用于事件监听,但若处理不当,容易引发循环引用。

问题代码:

class Observable<T> {  
    private var listener: ((T) -> Void)?  
    func addListener(_ closure: @escaping (T) -> Void) {  
        listener = closure // 此处形成循环引用  
    }  
    func notify(value: T) {  
        listener?(value)  
    }  
}  

class Observer {  
    var observable: Observable<Int>?  
    init(observable: Observable<Int>) {  
        self.observable = observable  
        observable.addListener { [weak self] value in  
            print("收到值: \(value)")  
        }  
    }  
    deinit { print("Observer 被销毁") }  
}  

// 测试  
var obs: Observer? = Observer(observable: Observable<Int>())  
obs = nil // 此时 Observable 和 Observer 都会被释放  

分析与解决方案

在原始代码中,Observablelistener 闭包捕获了 Observer 的强引用,而 Observer 又持有 Observable 的强引用,形成循环。通过在闭包中使用 [weak self],成功打破循环,确保对象能够被释放。


总结

Swift 自动引用计数(ARC) 是开发者必须掌握的核心概念。通过理解强引用、弱引用(weak)、无主引用(unowned)以及闭包捕获规则,可以有效避免内存泄漏和崩溃问题。

  • 关键点回顾
    • 引用计数是 ARC 的核心机制,对象的生命周期由其引用关系决定。
    • 循环引用常见于对象间相互强引用的场景,需通过 weakunowned 解决。
    • 闭包中的循环引用需通过捕获列表(Capture List)显式管理。

掌握这些技巧后,开发者可以更自信地构建复杂的应用逻辑,同时确保程序的高效与稳定性。记住:在 Swift 中,“引用关系决定生死”,合理管理每一处引用,是写出健壮代码的关键。

最新发布