Weak self,一个关于 Swift 内存管理和闭包的故事

原文:Weak self, a story about memory management and closure in Swift

内存管理是 Swift 和 iOS 开发中的一个大话题。如果有很多教程解释何时将 weak self 与闭包一起使用,这里有一个简短的故事,讲述它何时会发生内存泄漏。

出于本文章(演示)目的,假设我们有以下具有两个函数的类。每个函数都执行一些事务并通过执行闭包来完成执行。

💡 更新 - 2022 年 4 月 9 日:我重新访问了这些示例,以突出显示引用计数器何时增加以及何时可能导致内存泄漏。

swift 复制代码
class MyClass {

    func doSomething(_ completion: (() -> Void)?) {
        // do something
        completion?()
    }

    func doSomethingElse(_ completion: (() -> Void)?) {
        // do something else
        completion?()
    }
}

现在,出现了一个新需求,我们需要一个新函数 doEverything,它将按顺序同时执行 doSomethingdoSomethingElse。一路上,我们正在改变 class 的状态以跟随进度:

swift 复制代码
var didSomething: Bool = false
var didSomethingElse: Bool = false

func doEverything() {

    self.doSomething {
        self.didSomething = true // <- strong reference to self
        print("did something")

        self.doSomethingElse {
            self.didSomethingElse = true // <- strong reference to self
            print("did something else")
        }
    }
}

马上,我们可以看到 self 在第一个和第二个闭包中被强捕获:闭包保持对 self 的强引用,它在内部增加引用计数器,并可以防止在执行 doSomething 期间取消分配实例。

这意味着如果这些函数是异步的并且我们想在它完成执行之前释放实例,系统仍然必须等待它完成才能释放内存。

当然,我们知道得更多,并为闭包设置了 weak self

swift 复制代码
func doEverything() {

    self.doSomething { [weak self] in 
        self?.didSomething = true
        print("did something")

        self?.doSomethingElse { [weak self] in 
            self?.didSomethingElse = true
            print("did something else")
        }
    }
}

等等,我们真的需要为每个闭包都使用 [weak self] 吗?

实际上,我们没有(必要这么做)。

当我们有像这里这样的嵌套闭包时,我们应该始终将 weak self 设置为第一个,即外部闭包。嵌套在外部闭包中的内部闭包可以重用相同的 weak self

swift 复制代码
func doEverything() {

    self.doSomething { [weak self] in 
        self?.didSomething = true
        print("did something")

        self?.doSomethingElse { in
            self?.didSomethingElse = true
            print("did something else")
        }
    }
}

然而,如果我们反其道而行之,只在嵌套闭包中使用 weak self,外部闭包仍然会强捕获 self 并增加引用计数器。所以要小心你在哪里设置这个。

swift 复制代码
func doEverything() {

    self.doSomething { in 
        self.didSomething = true // <- strong reference to self**
        print("did something")

        self.doSomethingElse { [weak self] in 
            self?.didSomethingElse = true
            print("did something else")
        }
    }
}

到目前为止,一切都很好。

由于我们想在此过程中更改其他变量,因此让我们使用 guard let 清理代码以确保实例仍然可用。

swift 复制代码
func doEverything() {

    self.doSomething { [weak self] in 
        guard let self = self else { 
            return 
        }
        self.didSomething = true
        print("did something")

        self.doSomethingElse { in
            self.didSomethingElse = true // <-- strong reference?**
            print("did something else")
        }
    }
}

但是现在,问题来了:既然我们在外层闭包中有一个名为 self 的强引用,那么内层闭包是否强捕获了它?我们如何验证这一点?

这是值得深入研究的问题,Xcode Playground 非常适合这个问题。我将包含一些日志来跟踪步骤以及记录引用计数器。

对于第一个例子,让我们保持简单,这样我们就可以看到引用计数器是如何递增的。

swift 复制代码
class MyClass {

    func doSomething(_ completion: (() -> Void)?) {
        // do something
        completion?()
    }

    func doSomethingElse(_ completion: (() -> Void)?) {
        // do something else
        completion?()
    }

    var didSomething: Bool = false
    var didSomethingElse: Bool = false

    deinit {
        print("Deinit")
    }

    func printCounter() {
        print(CFGetRetainCount(self))
    }

    func doEverything() {
        print("start")
        printCounter() // 2
        self.doSomething {
            self.didSomething = true
            print("did something")
            self.printCounter() // 4

            self.doSomethingElse {
                self.didSomethingElse = true
                print("did something else")
                self.printCounter() // 6
            } // 6-2=4
        } // 4-2=2
        printCounter() // 2
    }
}

do {
    let model = MyClass()
    model.doEverything()
}

这是输出:

arduino 复制代码
# output
start
2
did something
4
did something else
6
2
Deinit

只有对 self 的强引用,我们可以看到计数器上升到 6。但是,正如预期的那样,一旦执行完两个函数,实例就会被释放。

现在让我们在外部闭包中引入 weak self

swift 复制代码
func doEverything() {
    print("start")
    printCounter() // 2
    self.doSomething { [weak self] in
        self?.didSomething = true
        print("did something")
        self?.printCounter() // 3

        self?.doSomethingElse {
            self?.didSomethingElse = true
            print("did something else")
            self?.printCounter() // 4
        } // 4-1=3
    } // 3-1=2
    printCounter() // 2
}

对于第一个 weak self,实例仍然被取消分配,计数器只增加到 4。

arduino 复制代码
# output
start
2
did something
3
did something else
4
2
Deinit

那么 guard let self 会发生什么呢?

swift 复制代码
func doEverything() {
    print("start")
    printCounter() // 2
    self.doSomething { [weak self] in
        guard let self = self else { return } // +1
        self.didSomething = true
        print("did something")
        self.printCounter() // 3

        self.doSomethingElse { // + 1
            self.didSomethingElse = true // +1
            print("did something else")
            self.printCounter() // 5
        }
    }
    printCounter() // 2
}

这是输出:

arduino 复制代码
# output
start
2
did something
3
did something else
5
2
Deinit

如果实例成功取消初始化,我们可以看到当我们执行 doSomethingElse 时计数器实际上从 4 增加到 5,这意味着嵌套闭包强烈捕获了我们的临时的 self

看起来已经很可疑了,但让我们尝试一个不同的例子。如果 doSomethingdoSomethingElse 不是函数,而是类的闭包属性,会怎样?让我们调整代码以进行类似的执行:

swift 复制代码
class MyClass {

    var doSomething: (() -> Void)?
    var doSomethingElse: (() -> Void)?

    var didSomething: Bool = false
    var didSomethingElse: Bool = false

    deinit {
        print("Deinit")
    }

    func printCounter() {
        print(CFGetRetainCount(self))
    }

    func doEverything() {

        print("start")
        printCounter() // 2
        doSomething = { [weak self] in
            guard let self = self else { return } // +1
            self.didSomething = true
            print("did something")
            self.printCounter() // 3

            self.doSomethingElse = { // +1
                self.didSomethingElse = true // +1
                print("did something else")
                self.printCounter() // 5
            }

            self.doSomethingElse?()
        }
        doSomething?()
        printCounter() // 3
    }
}

do {
    let model = MyClass()
    model.doEverything()
}

这是输出:

arduino 复制代码
# output
start
2
did something
3
did something else
5
3

这一次,类甚至没有取消分配🤯。要修复它,我们必须保留一个 weak 实例:

swift 复制代码
func doEverything() {

    print("start")
    printCounter() // 2
    doSomething = { [weak self] in
        self?.didSomething = true
        print("did something")
        self?.printCounter() // 3

        self?.doSomethingElse = {
            self?.didSomethingElse = true
            print("did something else")
            self?.printCounter() // 3
        }

        self?.doSomethingElse?()
    }
    doSomething?() // 3-1=2
    printCounter() // 2
}
arduino 复制代码
# output
start
2
did something
3
did something else
3
2
Deinit

是的,这次实例已正确解除分配。可以确认,内部闭包是对 guard let self 的强引用

那么,这对我的代码意味着什么?

当我们面对闭包时,我们倾向于编写 weak self 后跟一个 guard let 快速绕过而不会过多考虑进一步执行。这是我们仍然需要小心的地方。很容易错过这种内存泄漏,所以这里有一些要点:

首先,关于格式,我个人在闭包中使用 guard let strongSelf 而不是 guard let self。原因是在代码审查期间,很难知道我们指的是代码中的哪个 "self"。

其次,如果有嵌套闭包,我宁愿保留对 weak(和 optionalself? 的引用?并且永远不会指向 strongSelf,因此我有把握避免对它的任何强引用。

swift 复制代码
func doEverything() {
    print("start")
    self.printCount() // 2
    doSomething = { [weak self] in
        // 在闭包中声明 guard let strongSelf 以明确指向的 "self"
        guard let strongSelf = self else { return } // +1
        
        strongSelf.didSomething = true
        print("did something")
        strongSelf.printCount() // 3

        strongSelf.doSomethingElse = { // + 1
            // 在嵌套闭包中保持对 weak self? 的引用
            self?.didSomethingElse = true
            
            print("did something else")
            self?.printCount() // 4
        }
        strongSelf.doSomethingElse?()
    }
    doSomething?()
    self.printCount() // 2
}

最后但同样重要的是,如果我们有太多的闭包需要处理,最好的办法仍然是将其重构为一个单独的函数,或者使用更新的 API 来避免这些错误。我正在考虑像 RxSwiftCombine 这样的函数式响应式编程,但也考虑使用 Swift Async

当然,今天分享的代码可能有点牵强,可能无法反映您对闭包的日常使用,但在我看来,记住我们对实例的内存管理和引用仍然很重要。此外,这个问题正好进入同行评审的中间,所以我们永远不会太小心;)

希望你喜欢这篇文章,编码愉快!

问题?反馈?请随时在 Twitter 上给我发消息。

相关推荐
天下无贼!1 小时前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue
Jiaberrr1 小时前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选
赵啸林1 小时前
npm发布插件超级简单版
前端·npm·node.js
罔闻_spider2 小时前
爬虫----webpack
前端·爬虫·webpack
吱吱鼠叔2 小时前
MATLAB数据文件读写:1.格式化读写文件
前端·数据库·matlab
爱喝水的小鼠2 小时前
Vue3(一) Vite创建Vue3工程,选项式API与组合式API;setup的使用;Vue中的响应式ref,reactive
前端·javascript·vue.js
盏灯2 小时前
前端开发,场景题:讲一下如何实现 ✍电子签名、🎨你画我猜?
前端
WeiShuai2 小时前
vue-cli3使用DllPlugin优化webpack打包性能
前端·javascript
Wandra2 小时前
很全但是超级易懂的border-radius讲解,让你快速回忆和上手
前端
ice___Cpu2 小时前
Linux 基本使用和 web 程序部署 ( 8000 字 Linux 入门 )
linux·运维·前端