Swift闭包在类中的引用问题
Swift闭包在类中的引用问题

Swift闭包在类中的引用问题

关于闭包的前提知识《Swift闭包的三种形式以及理解闭包的嵌套函数》,如果不了解闭包,可以先阅读链接中的知识文档。

闭包访问类的属性

在 Swift 中,当你在闭包内访问类的属性(比如 products),需要明确地引用 self。这样做是为了确保开发者清楚闭包会捕获 self,避免意外的循环引用,如果不使用self会引起相关的报错

class IAPManager:ObservableObject {
    @Published var products: [Product] = [] 
    func loadProduct() async {
        do {
            print("加载产品中...")
            let fetchedProducts = try await fetchProduct()
            DispatchQueue.main.async {
                self.products = fetchedProducts // 在闭包中使用self关键字
            }
            print("成功加载产品: \(products)")
        } catch {
            print("加载产品失败:\(error)")
        }
    }
}

self.products:添加 self 可以确保 Swift 明确知道你是在访问类实例的属性,而不是闭包内的局部变量。

这样做也是一种常见的做法,以提醒开发者闭包内的代码会捕获 self,尤其是在异步或长时间运行的任务中,以避免潜在的内存泄漏。

闭包为什么会保存它引用的外部变量的值?

闭包会捕获并保存它在定义时引用的外部变量,是因为闭包是一种独立的代码块,它可能会在它的作用域外执行。为了确保闭包在执行时可以访问它需要的数据(包括变量、常量、属性等),闭包会捕获这些外部变量的值,并将它们保留在闭包的上下文中。

func makeIncrementer() -> () -> Int {
    var total = 0
    let incrementer: () -> Int = {
        total += 1
        return total
    }
    return incrementer
}

let increment = makeIncrementer()
print(increment()) // 输出: 1
print(increment()) // 输出: 2

在上面的代码中,total 是在函数 makeIncrementer 内部声明的局部变量,但闭包 incrementer 引用了 total。当 incrementer 被返回并在 makeIncrementer 的作用域外执行时,它仍然能够访问并修改 total 的值。这是因为闭包在创建时捕获并保存了 total。

闭包为什么会引起循环引用(retain cycle)?

闭包捕获变量

闭包会捕获并“持有”它上下文中的变量。如果这些变量是引用类型(例如类实例),闭包会对它们建立强引用(strong reference)。

这意味着,只要闭包存在,它就会保持对这些变量的引用,导致这些变量无法被释放。

对象持有闭包

当一个对象(类实例)持有一个闭包(比如作为属性存储),这个对象就对该闭包有强引用。

如果这个闭包在其代码中又捕获了对象自身(即 self),那么闭包就会对 self 也有一个强引用。

循环引用的形成

当闭包和对象互相持有强引用时,就会形成一个循环引用(retain cycle),导致两者都无法释放。比如:

class SomeClass {
    var closure: (() -> Void)?

    func startTask() {
        closure = {
            print("Task started!")
            // 捕获 self
            self.doSomething()
        }
    }
    
    func doSomething() {
        print("Doing something...")
    }
}

在上面的代码中:

1、SomeClass 的实例 self 持有 closure 作为属性。

2、closure 闭包捕获了 self,因为在闭包的代码中使用了 self.doSomething()。

3、这样,self 和 closure 互相持有,形成了一个循环引用:

self → 强引用 → closure

closure → 强引用 → self

由于这种互相持有的强引用,self 和 closure 都无法被释放,即使它们超出了作用域。这会导致内存泄漏。

循环引用

下面将通过实际的代码展示循环引用

class SomeClass {
    var closure: (() -> Void)?

    func startTask() {
        closure = {
            print("Task started!")
            // 捕获 self
            self.doSomething()
        }
    }
    
    func doSomething() {
        print("Doing something...")
    }
    deinit {
        print("SomeClass instance is being deinitialized") // 释放时输出
    }
}
var some: SomeClass? = SomeClass()

上面的代码中,首先创建一个名为some的SomeClass实例并设置为可选类型,以便后面赋值nil。

some?.doSomething()
some = nil

通过调用doSomething()方法,查看对应的输出,然后将some赋值为nil。

我们可以看到Xcode的Playground环境中,doSomething和deinit都有输出。

当实例被释放时,deinit代码就会被执行。

一切都没有问题。

在上面的代码中,添加实例对于startTask()方法的调用:

some?.startTask()
some?.doSomething()
some = nil

这时,我们会看到deinit没有再输出,这说明我在们给实例赋值nil后,实例并没有被释放。相反,因为startTask方法中closure有对self的捕获,导致了循环引用的出现。

Swift不会检测和提示循环引用,因为内存泄漏是一种在程序运行时资源未被释放的问题,而不是编译时可以发现的语法错误。因此,即使出现循环引用,程序可以正常运行,只是未被释放的内存会累积。

闭包捕获self的注意事项

1、异步任务和长时间运行的任务:

如果在异步任务中直接捕获 self,这段异步代码执行完毕前,self 会一直被保持在内存中,无法被释放。

如果 self 是一个视图控制器,而用户已经导航离开了该视图控制器,那么这个控制器本应该被释放,但由于异步任务的闭包还持有 self,它会继续占用内存。

2、如何避免循环引用?

你可以在闭包中显式使用 [weak self] 或 [unowned self] 来避免强引用 self,从而打破循环引用。

例如:

closure = { [weak self] in
    self?.doSomething()
}

使用 [weak self] 告诉闭包对 self 进行弱引用捕获。

如果闭包使用 self 时,self 已经释放了,那么 self 会是 nil,这样就不会再持有强引用。

这样一来,self 持有 closure,但 closure 只是弱引用 self,从而避免了循环引用。

再返回到前面的循环引用部分,尝试给循环引用的代码添加弱引用:

closure = { [weak self] in
    print("Task started!")
    // 捕获 self
    self?.doSomething()
}

再次执行代码,会发现deinit正常输出,说明弱引用成功的避免了循环引用的问题,并且释放了SomeClass实例。

3、为何明确捕获 self?

Swift 强制你在闭包中显式使用 self,这是为了提醒开发者注意捕获 self 可能带来的问题。特别是在使用异步任务和长时间运行的任务时,这种捕获会导致意想不到的内存保留行为,因此需要谨慎

总结

闭包捕获外部变量的原因:为了在闭包执行时确保它所需的外部变量仍然可用,闭包会保存这些变量。

循环引用的原因:self 持有闭包,闭包又持有 self,这导致它们相互引用,无法释放。

如何避免:使用 [weak self] 或 [unowned self] 来打破强引用链条,从而防止循环引用和内存泄漏。

SomeClass代码

class SomeClass {
    var closure: (() -> Void)?

    func startTask() {
        closure = { [weak self] in
            print("Task started!")
            // 捕获 self
            self?.doSomething()
        }
    }
    
    func doSomething() {
        print("Doing something...")
    }
    deinit {
        print("SomeClass instance is being deinitialized") // 释放时输出
    }
}
var some: SomeClass? = SomeClass()
some?.startTask()
some?.doSomething()
some = nil

相关文章

1、Xcode报错:Reference to property ‘products’ in closure requires explicit use of ‘self’ to make capture semantics explicit:https://fangjunyu.com/2024/10/22/xcode%e6%8a%a5%e9%94%99%ef%bc%9areference-to-property-products-in-closure-requires-explicit-use-of-self-to-make-capture-semantics-explicit/

2、Swift闭包的三种形式以及理解闭包的嵌套函数:https://fangjunyu.com/2024/10/23/swift%e9%97%ad%e5%8c%85%e7%9a%84%e4%b8%89%e7%a7%8d%e5%bd%a2%e5%bc%8f%e4%bb%a5%e5%8f%8a%e7%90%86%e8%a7%a3%e9%97%ad%e5%8c%85%e7%9a%84%e5%b5%8c%e5%a5%97%e5%87%bd%e6%95%b0/

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注