MST
星途 面试题库

面试题:Swift 内存管理与自动引用计数(ARC)的高级分析

详细描述 Swift 的自动引用计数(ARC)机制。列举并分析在 ARC 环境下可能出现的内存管理问题,如强引用循环,并给出至少两种避免强引用循环的方法及对应的代码示例。同时,探讨在一些复杂场景(如使用闭包、类继承结构复杂)下如何确保内存管理的正确性和高效性。
41.0万 热度难度
编程语言Swift

知识考点

AI 面试

面试题答案

一键面试

Swift 的自动引用计数(ARC)机制

ARC 是 Swift 中自动内存管理的核心机制。当一个类的实例不再被任何其他代码引用时,ARC 会自动释放该实例所占用的内存。

每次创建一个类的新实例时,ARC 会分配一块内存来存储该实例的信息,包括其属性值以及指向其他对象的引用。每当有一个新的强引用指向该实例时,实例的引用计数就会加 1;当指向该实例的强引用被销毁(例如变量超出作用域),引用计数就会减 1。当实例的引用计数变为 0 时,ARC 会自动释放该实例所占用的内存空间。

ARC 环境下可能出现的内存管理问题 - 强引用循环

强引用循环是指两个或多个对象通过强引用相互持有,导致它们的引用计数永远不会降为 0,从而无法被 ARC 释放,造成内存泄漏。

例如,假设有两个类 ClassAClassB

class ClassA {
    var b: ClassB?
    deinit {
        print("ClassA deinitialized")
    }
}

class ClassB {
    var a: ClassA?
    deinit {
        print("ClassB deinitialized")
    }
}

如果这样使用:

var a: ClassA? = ClassA()
var b: ClassB? = ClassB()
a?.b = b
b?.a = a
a = nil
b = nil

在上述代码中,ab 相互持有对方的强引用,即使 ab 被设为 nilClassAClassB 的实例由于强引用循环,其引用计数不会变为 0,不会被销毁,从而导致内存泄漏。

避免强引用循环的方法

  1. 使用弱引用(Weak References):弱引用不会增加对象的引用计数,当被引用的对象销毁时,弱引用会自动被设为 nil。修改上述代码如下:
class ClassA {
    var b: ClassB?
    deinit {
        print("ClassA deinitialized")
    }
}

class ClassB {
    weak var a: ClassA?
    deinit {
        print("ClassB deinitialized")
    }
}

var a: ClassA? = ClassA()
var b: ClassB? = ClassB()
a?.b = b
b?.a = a
a = nil
b = nil

这里 ClassB 中的 a 被声明为 weak,打破了强引用循环,当 a 设为 nil 时,ClassA 实例的引用计数变为 0 被销毁,此时 ClassB 中的 a 自动设为 nilClassB 实例的引用计数也变为 0 被销毁。

  1. 使用无主引用(Unowned References):无主引用和弱引用类似,也不会增加对象的引用计数,但无主引用不会自动设为 nil,所以必须确保被引用的对象在无主引用存在期间不会被销毁。适用于两个对象之间存在一个对象的生命周期总是长于另一个对象的情况。例如:
class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

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

这里 CreditCard 中的 customerunowned,因为 Customer 的生命周期总是长于 CreditCard,避免了强引用循环。

在复杂场景下确保内存管理的正确性和高效性

  1. 使用闭包时:闭包捕获外部对象时可能会导致强引用循环。例如:
class SomeClass {
    var data: String = "Some data"
    var closure: (() -> Void)?
    deinit {
        print("SomeClass deinitialized")
    }
}

let obj = SomeClass()
obj.closure = { [weak self] in
    guard let self = self else { return }
    print(self.data)
}

在闭包中使用 [weak self] 捕获 self,避免了强引用循环。如果需要在闭包中多次使用 self,可以通过 guard letif let 解包 weak 引用,确保 self 存在。

  1. 类继承结构复杂时:在多层继承结构中,确保每个类的属性引用设置合理,避免间接的强引用循环。例如,在子类和父类之间,如果子类持有父类的强引用,而父类又持有子类的强引用,就可能形成循环。可以通过合理使用 weakunowned 来打破循环。同时,在重写父类的方法时,要注意内存管理的一致性,例如在 deinit 方法中,确保释放所有必要的资源,避免因继承结构导致资源未释放的问题。

总之,在复杂场景下,需要仔细分析对象之间的引用关系,合理使用 weakunowned 等修饰符,并且在代码逻辑中注意对象生命周期的管理,以确保内存管理的正确性和高效性。