iOS内存管理中的强引用问题

iOS内存管理

关于强引用循环

强引用循环是 ARC 无法自动处理的常见问题。如果两个对象互相强引用对方,就会造成引用计数不为零,导致对象无法释放。典型的情况是在闭包中引用 self 时,self 和闭包之间可能会互相持有,形成强引用循环。

解决方法: 使用 [weak self] 或 [unowned self] 来避免强引用循环,特别是在闭包内。

强引用(Strong Reference)是指一个对象在内存中持有对另一个对象的引用时,这个引用会增加被引用对象的引用计数。当一个对象的引用计数增加时,它的生命周期就被延长,直到引用计数变为零,表示没有任何对象引用它时,ARC 会自动释放这个对象的内存。

在 iOS 中,默认情况下,所有对象的引用都是强引用。强引用会导致引用的对象在内存中不会被释放,直到所有强引用都解除。

强引用的基本理解

  • 强引用 :指对象通过 strong 持有另一个对象。该对象的生命周期会被引用方延长,直到引用方解除对它的持有。
  • 当两个对象之间通过强引用相互持有时,就会形成 强引用循环(Retain Cycle),这种情况下,即使它们不再需要对方,它们的引用计数也不会降到零,从而导致内存泄漏。

强引用的例子

1. 基本的强引用示例

假设你有两个对象:Person 和 Car,Person 强引用 Car,意味着只要 Person 存在,Car 就不会被释放。

swift 复制代码
class Person {
    var car: Car?
}

class Car {
    var owner: Person?
}

var john: Person? = Person()
var toyota: Car? = Car()

john?.car = toyota
toyota?.owner = john

在这个例子中,john 对象强引用了 toyota 对象,同时 toyota 也强引用了 john 对象。由于两个对象互相持有对方,即使这两个对象的生命周期已经结束,它们的引用计数也不会变为零,因为它们彼此依赖,这就形成了 强引用循环。这会导致内存泄漏。

2. 解决强引用循环

可以通过使用 弱引用 (weak)来打破这种循环,确保当 Person 或 Car 不再需要时,它们能正确释放。

swift 复制代码
class Person {
    var car: Car?
}

class Car {
    weak var owner: Person?  // 使用 weak 避免强引用循环
}

var john: Person? = Person()
var toyota: Car? = Car()

john?.car = toyota
toyota?.owner = john

在这个例子中,owner 属性使用 weak 修饰符,这样当 john 被释放时,toyota 中的 owner 引用就会变为 nil,从而打破强引用循环。

3. 强引用与内存管理的关系

当我们使用强引用时,ARC 会增加被引用对象的引用计数。下面是一个具体例子,说明如何影响内存管理:

swift 复制代码
class Dog {
    var name: String
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

var dog1: Dog? = Dog(name: "Buddy")
var dog2 = dog1  // dog2 强引用 dog1

dog1 = nil  // dog1 被置为 nil,但是 dog2 仍然持有 Buddy 的引用

在上面的代码中:

  • 当 dog1 被赋值为 nil 时,它不再持有 Buddy 对象。
  • 但是由于 dog2 仍然强引用着 Buddy,Buddy 对象并不会立即被释放。
  • Buddy 只有当 dog2 也被置为 nil 后,才会释放内存。
4. 常见场景:闭包中的强引用

闭包是导致强引用循环的一个常见场景。例如,下面的代码就会导致强引用循环:

swift 复制代码
class ViewController: UIViewController {
    var someProperty: String = "Hello"
    
    func setupClosure() {
        let closure = {
            print(self.someProperty)  // 闭包捕获了 self
        }
        closure()
    }
}

let vc = ViewController()
vc.setupClosure()

在上面的代码中,closure 捕获了 self(即 ViewController 的实例),这意味着 ViewController 对象将始终被闭包引用,self 的引用计数不会变为零,导致 ViewController 永远不会被释放。为了避免这种情况,我们通常使用 弱引用 或 无主引用。

swift 复制代码
func setupClosure() {
    let closure: () -> Void = { [weak self] in
        print(self?.someProperty ?? "")
    }
    closure()
}

使用 [weak self] 来捕获 self,可以避免闭包对 ViewController 的强引用,从而避免强引用循环。

但需要注意的是,并不是所有的闭包都会产生强引用

不会产生强引用的情况

在闭包中使用 weak 或 unowned 主要是为了避免 强引用循环 (retain cycle),尤其是当闭包捕获 self 时。是否需要使用 weak 或 unowned 取决于闭包与被捕获对象之间的生命周期关系。以下是几种情况,你可以根据这些情况来判断何时不需要使用 weak 或 unowned。

1. 闭包的生命周期比捕获的对象短

如果闭包的生命周期比被捕获的对象短(即闭包不会持有对象,且对象的生命周期不会依赖于闭包),则无需使用 weak 或 unowned。

示例:

  • 闭包作为局部变量在方法内使用,并且被调用后立即销毁。
  • 闭包没有持有对对象的引用,且被调用时对象已被销毁。
swift 复制代码
class MyClass {
    var value = 10
    
    func performAction() {
        // 闭包在执行后立即销毁,不会产生强引用循环
        let closure = {
            print(self.value)
        }
        closure()
    }
}

let obj = MyClass()
obj.performAction()

在这个例子中,闭包在方法内部执行时,仅仅是一个局部变量。它不会在方法外部持有 self,且方法执行完毕后,闭包就会被销毁,所以不需要使用 weak 或 unowned 来捕获 self。

2. 闭包被存储在对象的属性中,并且对象的生命周期与闭包相同

如果闭包是对象的一个属性,并且该对象的生命周期与闭包相同(即闭包和对象一起销毁),则也不需要使用 weak 或 unowned。

例如,闭包可能会作为一个委托回调、通知、或者是一些任务的完成闭包。如果该对象本身不会被提前销毁,闭包引用 self 是安全的,因为对象的生命周期会保持直到闭包不再需要。

示例:

swift 复制代码
class Task {
    var completion: (() -> Void)?
    
    func start() {
        completion = {
            print("Task is done")
        }
    }
}

let task = Task()
task.start()

在这个例子中,Task 对象持有 completion 闭包,闭包本身不会引发强引用循环,因为 Task 对象会一直存在直到 completion 被销毁。这里没有强引用循环问题。

3. 捕获的对象不会导致内存泄漏

如果闭包捕获的对象是一个"非持有"对象(例如全局对象、常量、静态对象),或者闭包执行时被捕获的对象本身在执行完成后就会被销毁(比如临时创建的对象),则不需要使用 weak 或 unowned。

示例:

swift 复制代码
class MyClass {
    func fetchData() {
        let completion: () -> Void = {
            print("Data fetched!")
        }
        completion()
    }
}

let instance = MyClass()
instance.fetchData()

在这个例子中,completion 闭包没有捕获 self,因此不会形成强引用循环。并且 completion 的生命周期非常短,执行完成后就会被销毁。

4. 闭包没有捕获 self

如果闭包本身不捕获 self(即没有使用 [weak self] 或 [unowned self]),且闭包本身不会导致引用循环,也可以不使用 weak 或 unowned。这种情况下,闭包本身不会持有对象。

示例:

swift 复制代码
class MyClass {
    var name = "Hello"
    
    func fetchData() {
        let completion: () -> Void = {
            print("Data fetched!")
        }
        completion()
    }
}

let instance = MyClass()
instance.fetchData()

这里 completion 不捕获 self,所以 MyClass 对象不会因为闭包而延长生命周期。

总结:

我们需要注意闭包和实例是否会相互引用,还需要注意闭包和实例的生命周期

你不需要使用 weak 或 unowned 来捕获 self 的情况包括:

  1. 闭包的生命周期比捕获的对象短,例如临时的局部闭包。
  2. 闭包被存储在对象属性中,并且对象的生命周期与闭包相同。
  3. 闭包没有捕获 self,且不涉及持有对象的引用。
  4. 捕获的对象不会导致内存泄漏,例如全局变量、常量等对象。
相关推荐
传奇开心果编程4 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
xUxIAOrUIII6 小时前
日常笔记-1005-1
linux·笔记·python·macos
tink6 小时前
告别 Xcode IDE:用 VS Code + SweetPad + XcodeGen 开发 iOS 应用的完整指南
ios·swiftui·xcode
传奇开心果编程7 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程9 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程9 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
黑科技iOS上架9 小时前
iOS隐私合规扫描工具
ios·审核
传奇开心果编程9 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
传奇开心果编程9 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
可乐鸡翅yeah_10 小时前
hls.js 缓冲区参数 maxBufferLength、maxBufferSize 通俗讲解与业务调参
开发语言·javascript·ios·音视频·safari·m3u8