MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift中的弱引用与无主引用对比

2021-10-246.7k 阅读

一、内存管理基础

在深入探讨Swift中的弱引用与无主引用之前,先来回顾一下内存管理的基础知识。在程序运行过程中,内存的分配与释放至关重要。当对象不再被使用时,如果不及时释放其占用的内存,就会导致内存泄漏,进而影响程序的性能和稳定性。

在Swift中,采用自动引用计数(ARC,Automatic Reference Counting)机制来管理内存。ARC会自动跟踪和管理类实例的引用数量。当一个类实例的引用计数变为0时,ARC会自动释放该实例占用的内存。例如:

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

var reference1: Person?
reference1 = Person(name: "John")
// 输出: John is being initialized

reference1 = nil
// 输出: John is being deinitialized

在上述代码中,Person类实例reference1被创建时,其引用计数为1。当reference1被赋值为nil时,其引用计数变为0,ARC自动调用deinit方法释放内存。

二、强引用循环问题

虽然ARC大大简化了内存管理,但在某些情况下,仍然可能出现问题,其中最典型的就是强引用循环。当两个或多个对象相互持有强引用时,就会形成强引用循环,导致对象的引用计数永远不会变为0,从而造成内存泄漏。

例如,假设有两个类ClassAClassB

class ClassA {
    let name: String
    var classB: ClassB?
    init(name: String) {
        self.name = name
        print("\(name) of ClassA is being initialized")
    }
    deinit {
        print("\(name) of ClassA is being deinitialized")
    }
}

class ClassB {
    let name: String
    var classA: ClassA?
    init(name: String) {
        self.name = name
        print("\(name) of ClassB is being initialized")
    }
    deinit {
        print("\(name) of ClassB is being deinitialized")
    }
}

var a: ClassA? = ClassA(name: "A")
var b: ClassB? = ClassB(name: "B")

a?.classB = b
b?.classA = a

a = nil
b = nil

在这段代码中,ClassA实例a持有ClassB实例b的强引用,而ClassB实例b又持有ClassA实例a的强引用,形成了强引用循环。当ab被赋值为nil时,它们的引用计数并不会变为0,因为它们相互持有对方的强引用,所以deinit方法不会被调用,导致内存泄漏。

三、弱引用(Weak References)

3.1 弱引用的定义与特性

为了解决强引用循环问题,Swift引入了弱引用。弱引用是一种不保持对象强引用的引用类型,它不会增加对象的引用计数。当对象的引用计数变为0并被释放时,指向该对象的所有弱引用都会自动被设置为nil。这一特性使得弱引用特别适合用于解决对象之间可能出现的强引用循环问题,同时又不需要持有对象的强引用。

在Swift中,使用weak关键字来声明弱引用。例如,对上述ClassAClassB的代码进行修改:

class ClassA {
    let name: String
    weak var classB: ClassB?
    init(name: String) {
        self.name = name
        print("\(name) of ClassA is being initialized")
    }
    deinit {
        print("\(name) of ClassA is being deinitialized")
    }
}

class ClassB {
    let name: String
    var classA: ClassA?
    init(name: String) {
        self.name = name
        print("\(name) of ClassB is being initialized")
    }
    deinit {
        print("\(name) of ClassB is being deinitialized")
    }
}

var a: ClassA? = ClassA(name: "A")
var b: ClassB? = ClassB(name: "B")

a?.classB = b
b?.classA = a

a = nil
// 输出: A of ClassA is being deinitialized
b = nil
// 输出: B of ClassB is being deinitialized

在修改后的代码中,ClassA中的classB属性被声明为弱引用。这样,当a被赋值为nil时,ClassA实例的引用计数变为0,ARC会释放ClassA实例,同时ba的引用也不再是强引用,ClassA实例的deinit方法被调用。随后,当b被赋值为nil时,ClassB实例的引用计数也变为0,ClassB实例的deinit方法也被调用,成功解决了强引用循环问题。

3.2 弱引用的使用场景

  1. 视图控制器之间的关系:在iOS开发中,视图控制器之间的父子关系或嵌套关系较为常见。例如,一个父视图控制器持有子视图控制器的强引用,而子视图控制器可能需要引用父视图控制器。如果子视图控制器对父视图控制器使用强引用,就可能形成强引用循环。此时,子视图控制器对父视图控制器使用弱引用是一个很好的解决方案。
class ParentViewController: UIViewController {
    var childViewController: ChildViewController?
    override func viewDidLoad() {
        super.viewDidLoad()
        let child = ChildViewController()
        childViewController = child
        addChild(child)
        view.addSubview(child.view)
        child.didMove(toParent: self)
    }
}

class ChildViewController: UIViewController {
    weak var parentVC: ParentViewController?
    override func viewDidLoad() {
        super.viewDidLoad()
        parentVC = parent as? ParentViewController
    }
}
  1. 代理模式:代理模式是一种常用的设计模式,在Swift中广泛应用。例如,一个视图可能有一个代理对象,视图在某些事件发生时会通知代理。如果视图对代理使用强引用,而代理又持有视图的强引用,就会形成强引用循环。通过将视图对代理的引用声明为弱引用,可以避免这种情况。
protocol MyViewDelegate: AnyObject {
    func viewDidTap()
}

class MyView: UIView {
    weak var delegate: MyViewDelegate?
    override init(frame: CGRect) {
        super.init(frame: frame)
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
        addGestureRecognizer(tapGesture)
    }

    @objc func handleTap() {
        delegate?.viewDidTap()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class ViewController: UIViewController, MyViewDelegate {
    let myView = MyView()
    override func viewDidLoad() {
        super.viewDidLoad()
        myView.delegate = self
        view.addSubview(myView)
    }

    func viewDidTap() {
        print("View was tapped")
    }
}

3.3 弱引用的局限性

  1. 可选类型:由于弱引用在对象被释放时会自动设置为nil,所以弱引用必须是可选类型。这意味着在使用弱引用时,需要进行可选值的解包操作,增加了代码的复杂性。例如:
class ClassA {
    let name: String
    weak var classB: ClassB?
    init(name: String) {
        self.name = name
    }
    func accessClassB() {
        if let b = classB {
            print("Accessed ClassB: \(b.name)")
        }
    }
}

class ClassB {
    let name: String
    init(name: String) {
        self.name = name
    }
}

var a: ClassA? = ClassA(name: "A")
var b: ClassB? = ClassB(name: "B")
a?.classB = b
a?.accessClassB()
// 输出: Accessed ClassB: B

b = nil
a?.accessClassB()
// 无输出
  1. 性能开销:虽然ARC会自动管理弱引用的设置和更新,但在对象的生命周期中,每次对象的引用计数发生变化时,ARC都需要检查并更新相关的弱引用。这在一定程度上会带来性能开销,尤其是在对象引用计数频繁变化的场景下。

四、无主引用(Unowned References)

4.1 无主引用的定义与特性

无主引用也是一种用于解决强引用循环的引用类型,但它与弱引用有所不同。无主引用不会增加对象的引用计数,与弱引用类似,但无主引用在对象被释放后不会自动设置为nil。这意味着无主引用必须指向一个始终存在的对象,否则在访问已释放对象的无主引用时,会导致运行时错误。

在Swift中,使用unowned关键字来声明无主引用。例如:

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

class CreditCard {
    let number: Int
    unowned let customer: Customer
    init(number: Int, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit {
        print("Card number \(number) is being deinitialized")
    }
}

var john: Customer? = Customer(name: "John")
john?.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

john = nil
// 输出: John is being deinitialized
// 输出: Card number 1234567890123456 is being deinitialized

在上述代码中,CreditCard类的customer属性被声明为无主引用。Customer实例john持有CreditCard实例的强引用,而CreditCard实例通过无主引用指向Customer实例。由于Customer实例的生命周期长于CreditCard实例,所以使用无主引用是安全的。当john被赋值为nil时,Customer实例的引用计数变为0,deinit方法被调用,同时CreditCard实例的引用计数也变为0,deinit方法也被调用,解决了强引用循环问题。

4.2 无主引用的使用场景

  1. 相互依赖且生命周期相同:当两个对象相互依赖,且其中一个对象的生命周期必然长于另一个对象时,无主引用是一个合适的选择。例如,在一个音乐播放器应用中,可能有一个MusicPlayer类和一个Playlist类。Playlist类依赖于MusicPlayer类来播放歌曲,而MusicPlayer类可能需要引用Playlist类来获取歌曲列表。如果MusicPlayer类的生命周期长于Playlist类,可以在Playlist类中使用无主引用指向MusicPlayer类。
class MusicPlayer {
    var playlist: Playlist?
    func play() {
        if let pl = playlist {
            print("Playing playlist \(pl.name)")
        }
    }
}

class Playlist {
    let name: String
    unowned let player: MusicPlayer
    init(name: String, player: MusicPlayer) {
        self.name = name
        self.player = player
    }
}

let player = MusicPlayer()
let playlist = Playlist(name: "My Playlist", player: player)
player.playlist = playlist
player.play()
// 输出: Playing playlist My Playlist
  1. 单例与其他对象的关系:在某些情况下,单例对象可能与其他对象存在相互引用关系。由于单例对象在整个应用程序生命周期内存在,其他对象对单例对象使用无主引用可以避免强引用循环,同时又能确保始终可以访问到单例对象。
class Singleton {
    static let shared = Singleton()
    var associatedObject: AssociatedObject?
    private init() {}
}

class AssociatedObject {
    unowned let singleton: Singleton
    init(singleton: Singleton) {
        self.singleton = singleton
    }
}

let obj = AssociatedObject(singleton: Singleton.shared)
Singleton.shared.associatedObject = obj

4.3 无主引用的风险

  1. 野指针问题:由于无主引用在对象被释放后不会自动设置为nil,如果不小心释放了无主引用指向的对象,后续对该无主引用的访问就会导致野指针问题,引发运行时错误。例如:
class ClassA {
    let name: String
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) of ClassA is being deinitialized")
    }
}

class ClassB {
    unowned let classA: ClassA
    init(classA: ClassA) {
        self.classA = classA
    }
    func accessClassA() {
        print("Accessed ClassA: \(classA.name)")
    }
}

var a: ClassA? = ClassA(name: "A")
var b: ClassB? = ClassB(classA: a!)
b?.accessClassA()
// 输出: Accessed ClassA: A

a = nil
// 输出: A of ClassA is being deinitialized
b?.accessClassA()
// 运行时错误,因为classA已被释放
  1. 初始化顺序依赖:使用无主引用时,需要特别注意对象的初始化顺序。如果无主引用指向的对象在无主引用所在对象初始化之后才初始化,可能会导致未初始化的无主引用被访问,从而引发错误。例如:
class ClassA {
    let name: String
    unowned let classB: ClassB
    init(name: String) {
        self.name = name
        classB = ClassB()
    }
}

class ClassB {
    let name = "B"
}

var a: ClassA? = ClassA(name: "A")

在上述代码中,ClassA在初始化时尝试创建ClassB实例并将其赋值给无主引用classB,但此时ClassB实例尚未完全初始化,这可能导致潜在的问题。为了避免这种情况,需要仔细设计对象的初始化逻辑,确保无主引用指向的对象在使用前已经正确初始化。

五、弱引用与无主引用的对比

5.1 内存管理行为

  • 弱引用:不增加对象的引用计数,当对象的引用计数变为0并被释放时,指向该对象的所有弱引用都会自动被设置为nil。这使得弱引用在对象生命周期结束后不会导致野指针问题。
  • 无主引用:同样不增加对象的引用计数,但在对象被释放后不会自动设置为nil。这就要求在使用无主引用时,必须确保所指向的对象在其生命周期内始终存在,否则会导致运行时错误。

5.2 使用场景选择

  • 弱引用:适用于对象之间存在可能的强引用循环,且其中一个对象的生命周期可能短于另一个对象的情况。例如,在视图控制器之间的父子关系、代理模式等场景中,由于对象的生命周期不确定,使用弱引用可以避免强引用循环,同时又能安全地处理对象释放后的情况。
  • 无主引用:适用于对象之间相互依赖,且可以确定其中一个对象的生命周期长于另一个对象的场景。例如,在相互依赖且生命周期相同的对象关系、单例与其他对象的关系中,使用无主引用可以避免强引用循环,并且由于对象不会提前释放,不会出现野指针问题。

5.3 代码复杂性与性能

  • 弱引用:由于弱引用必须是可选类型,在使用时需要进行可选值的解包操作,这增加了代码的复杂性。同时,ARC在管理弱引用时需要额外的操作来更新弱引用,会带来一定的性能开销,尤其是在对象引用计数频繁变化的场景下。
  • 无主引用:无主引用不需要进行可选值的解包操作,代码相对简洁。但由于无主引用不会自动设置为nil,需要开发者更加小心地管理对象的生命周期,以避免野指针问题。从性能角度来看,无主引用在对象引用计数变化时不需要像弱引用那样进行额外的更新操作,性能开销相对较小。

5.4 安全性

  • 弱引用:由于在对象释放后会自动设置为nil,所以在使用弱引用时,即使对象已经被释放,也不会导致运行时错误,安全性较高。
  • 无主引用:如果不小心释放了无主引用指向的对象,后续对该无主引用的访问会导致运行时错误,安全性相对较低。因此,在使用无主引用时,需要更加谨慎地管理对象的生命周期,确保无主引用始终指向有效的对象。

综上所述,在Swift中选择弱引用还是无主引用,需要根据具体的应用场景和对象之间的关系来决定。合理地使用弱引用和无主引用,可以有效地解决强引用循环问题,提高程序的性能和稳定性。在实际开发中,要充分理解它们的特性和使用场景,以避免潜在的内存泄漏和运行时错误。