Swift 异步序列 AsyncStream 新“玩法”以及内存泄漏、死循环那些事儿(下)

概览

在上一篇博文

我们讨论了 Swift 语言利用 AsyncStream 创建异步序列并获取其中连续体(Continuation)对象的新方法。而在这里我们将承接之前的话题继续来聊聊异步序列实际使用中可能遇到的各种陷阱,并介绍如何彻底摆脱它们。

在本篇博文中,您将学到如下内容:

  1. 一个潜在的内存泄漏点
  2. 驯服"桀骜不驯"的死循环

相信本篇会为 Swift 5.5 新并发模型中异步序列(Async Sequence)的使用实战让大家如虎添翼。

废话不再,让我们马上开始吧!Let's go!!!;)


3. 一个潜在的内存泄漏点

我们继续上篇博文的未完成之旅。

在这之前,我们已经可以熟练运用 AsyncStream 来创建各色异步序列了,不过这并不代表我们已然"滴水不漏"。在真实的使用场景中,稍不留神可能就会千疮百孔、遍体鳞伤。

不信?"栗子"来了!

首先,我们利用 AsyncStream 新建一个 NumberProvider 数字发生器:

swift 复制代码
class NumberProvider {
    let numbers: AsyncStream<Int>
    private let continuation: AsyncStream<Int>.Continuation
    private var cancellable: AnyCancellable?
    
    init() {
        let result = AsyncStream.makeStream(of: Int.self)
        numbers = result.stream
        continuation = result.continuation
    }
    
    deinit {
        print("NumberProvider:我 GG 了!")
    }
    
    func start() {
        cancellable = Timer.publish(every: 1.0, on: .main, in: .common)
            .autoconnect()
            .sink {[unowned self] _ in
                self.continuation.yield(Int.random(in: 0...10000))
            }
    }
}

为了模拟真实的使用场景,我们创建一个模型对象用于嵌入上面的 NumberProvider 对象:

swift 复制代码
class Model {
    let provider = NumberProvider()
    
    var numbers: AsyncStream<Int> {
        provider.numbers
    }
    
    deinit {
        print("Model:我 GG 了!")
    }
    
    init() {
        provider.start()
    }
}

现在遥想一下:当视图退出时我们需要"清理门户"将 Model 对象从内存中删除掉,于是乎有了如下 Clean 代码:

swift 复制代码
var model: Model? = Model()

Task {
    guard let numbers = model?.numbers else { return }

    print("#1: before for loop")
    for await i in numbers {
        print(i)
    }
    print("#2: after for loop")
}

Task {
	// 模拟 2.2 秒后用户退出视图,随即我们需要清除 model 对象
    try await Task.sleep(for: .seconds(2.2))
    model = nil
}

编译运行代码,结果如下所示:

小伙伴们看到了吗?虽然 Model 和 NumberProvider 对象被如愿删除,但循环并没有正常结束,它会永远处在等待状态中不能自拔!(因为 "#2: after for loop" 一句没有被打印出来)

这意味着,NumberProvider 内部创建的异步序列没有被释放,所以在这种情况下铁定会造成内存泄漏(Memory Leaks)。

那么如何解决呢?简单的出乎意料!

我们只需在 NumberProvider 对象被删除时调用连续体(Continuation)的 finish() 方法即可:

swift 复制代码
class NumberProvider {
    private let continuation: AsyncStream<Int>.Continuation
    
    deinit {
        print("NumberProvider:我 GG 了!")
        continuation.finish()
    }
}

现在,我们在 NumberProvider 自己 GG 时,告知 Continuation 背后的异步序列也必须同时"舍生取义",从而完美地得偿所愿:

4. 驯服"桀骜不驯"的死循环

现在,我们已经封堵了异步序列的内存泄漏。不过,别高兴的太早!狡诈的 Async Sequence 还是不能让我们完全放松警惕。

仍然倔强的不信?再举个"栗子"。

我们对上面的代码略加修改,首先在 NumberProvider 的 start() 方法中增加一行打印语句以便让问题"原形毕露":

swift 复制代码
class NumberProvider {
    let numbers: AsyncStream<Int>
    private let continuation: AsyncStream<Int>.Continuation
    private var cancellable: AnyCancellable?
    
    func start() {
        cancellable = Timer.publish(every: 1.0, on: .main, in: .common)
            .autoconnect()
            .sink {[unowned self] _ in
                // 增加下面一句输出命令
                print("准备 Send")
                
                self.continuation.yield(Int.random(in: 0...10000))
            }
    }
}

接着,我们用结束任务(Task)的方式来试图结束异步序列的遍历:

swift 复制代码
var model: Model? = Model()

let job = Task {
    guard let numbers = model?.numbers else { return }

    print("#1: before for loop")
    for await i in numbers {
        print(i)
    }
    print("#2: after for loop")
}

Task {
	// 在 2.2 秒后结束任务
    try await Task.sleep(for: .seconds(2.2))
    job.cancel()
}

运行效果如下所示:

可以看到,我们自以为通过结束 Task 可以干脆利落地终结一切,但结果却啪啪打脸。看来 NumberProvider 中都计时器仍然"执迷不悟"似的一路走到黑,这该如何是好呢?

其实我们只需在异步序列结束时趁机停止计时器循环即可,这可以通过设置 Continuation 的结束回调闭包来实现:

swift 复制代码
class NumberProvider {
    let numbers: AsyncStream<Int>
    private let continuation: AsyncStream<Int>.Continuation
    private var cancellable: AnyCancellable?
    
    func start() {
        cancellable = Timer.publish(every: 1.0, on: .main, in: .common)
            .autoconnect()
            .sink {[unowned self] _ in
                print("准备 Send")
                
                self.continuation.yield(Int.random(in: 0...10000))
            }
        
        continuation.onTermination = { [unowned self] _ in
            // 或者 self.cancellable = nil 也可以
            self.cancellable?.cancel()
        }
    }
}

如上代码所示:我们在连续体背后的异步序列 GG 时,识趣的取消了定时器"死不悔改"的循环,从而一发入魂,使整个异步世界变的清净了!棒棒哒!💯

总结

在本篇博文中,我们通过实际代码中出现的例子介绍了 Swift 并发模型里使用异步序列可能出现的陷阱,并成功的让它们"全面瓦解"。

感谢观赏,再会!8-)

相关推荐
杂雾无尘3 小时前
iOS 分享扩展(四):让分享扩展与主应用无缝衔接
ios·swift·apple
Larva5 小时前
记录使用 SwiftLint检测代码内的硬编码字符串
ios·swift·代码规范
iOS阿玮7 小时前
鬼才网友给苹果CEO写邮件,申诉找回账号的奇幻之旅。
uni-app·app·apple
大熊猫侯佩7 小时前
SwiftUI 中无法对添加模糊(blur)效果视图截图的初步解决
swiftui·swift·apple
大熊猫侯佩7 小时前
Swift 异步序列 AsyncStream 新“玩法”以及内存泄漏、死循环那些事儿(上)
swift·编程语言·apple
Lei活在当下8 小时前
Java 8 效率精进指南(1)前言
java·后端·编程语言
杂雾无尘1 天前
iOS 分享扩展(三):轻松定制 iOS 分享界面,提升用户体验
ios·swift·apple
Moonbit1 天前
IDEA 编程语言 MoonBit 进入Beta版本,构建下一代基础软件系统入口
编程语言
iOS阿玮1 天前
Pingpong和连连的平替,让AppStore收款无需新增持有人。
uni-app·app·apple