Swift continiuations介绍与使用

所属分类:ios | 发布于 2024-12-06

在制作相机App时,发现官方例子AVCam中拍照和录像时使用了withCheckedThrowinigContinuation()函数,第一次见,看的迷迷糊糊,这里学习下

本文主要参考了下面两篇文章

这两篇文章中,分别在两个场景中使用了continuations

  • 使用continuation在async代码中使用带有completion handler异步回调函数的函数。
  • 使用continuation转换或使用现有的委托模式,并用Swift的结构化并发性包装它们,以便在您的应用程序中使用异步/等待机制。

这里我们发现它们有一个共性:就是在使用Concurrency并发编程的代码块中调用使用了原来的completion回调函数编程的函数。通俗的来讲,continuation就是创建一个桥梁,用来链接老的回调函数(completion handler)处理方式和新的异步(async)处理方式。

一些外部的Library或者API没有提供async/await方式调用,而我们又需要在async/await环境中使用这些类库或方法时,就可以使用continiuation来解决这个问题。

 

更新:后来实际使用过程中,发现还是对使用还是有些模糊,后来又看了一个教学视频,感觉算是入门了。

引入continuation

continuation是同步和异步代码之间的接口机制。他们包装我们现有的代码,等待我们通知他们更改。通知部分称为恢复操作,我们可以在继续时使用。

有两种类型的continuation:

Checked continuation-这种类型的continuation会检查是否多次恢复延续,或者是否在一段时间后恢复。从延续中恢复多次是未定义的行为。从不恢复会使任务无限期地处于挂起状态,并泄漏任何相关资源。如果违反了这些不变量中的任何一个,则选中的延续会记录一条消息。

Unsafe continuation-选中的继续执行运行时检查,以查找丢失或多个恢复操作。不安全延续避免了在运行时强制执行这些不变量,因为它旨在成为一种低开销的机制,用于将Swift任务与事件循环、委托方法、回调和其他非异步调度机制进行接口连接。

在开发过程中,验证不变量在测试中得到维护的能力非常重要。因为这两种类型具有相同的接口,所以在大多数情况下,您可以用另一种替换一种,而无需进行其他更改。

上面的类型解释看起来比较绕,不用具体了解。

Continuation可以抛出异常也可以不抛出异常,我们可以使用全局函数创建它们:

  • withCheckedContinuation()
  • withCheckedThrowingContinuation()
  • withUnsafeContinuation()
  • withUnsafeThrowingContinuation()

函数解释

那我们选一个函数,就选withCheckedThrowingContinuation()吧,先看看它的定义

withCheckedThrowingContinuation(_ body: (CheckedContinuation<T, any Error>) -> Void) async throws -> T

1、首先这是一个全局函数

2、这个函数是async的,所以调用的时候需要在前面加await

3、这个函数是throws的,所以调用的时候需要在前面加try

4、这个函数的参数body是一个闭包函数,函数的参数是CheckedContiniuation,类型是泛型,闭包函数的返回值是Void

基础调用

func capturePhoto() async throws -> Photo {
    try await withCheckedThrowingContinuation { continuation in
        // some completion handler codes
    }
}

正如你所看到的,开始一个continuation是使用withCheckedContinuation()函数完成的,该函数将我们需要的处理的延续传递给它自己。它被称为“检查”延续,因为Swift会检查我们是否正确使用延续。

这意味着要遵守一个非常简单、非常重要的规则:

 你的continuation必须恰好恢复一次。不是零次,也不是两次或两次以上同样,这种情况的发生没有时间限制,但必须在某个时候发生。

如果你调用延续两次或两次以上,这将引起Swift将你的程序停止-它只会崩溃。这听起来很糟糕,但是另外一种选择是做出一些奇怪、不可预测的行为时,崩溃听起来并不是那么糟糕。

另一方面,如果你根本没法恢复延续,Swift讲在你的调试日志中打印出一条类似这样的警告, “SWIFT TASK CONTINUATION MISUSE: fetchMessages() leaked its continuation!”,Swift任务延续错误:fetchMessage()泄露了它的延续!这是因为你让任务挂起,导致它正在使用的任何资源被无期限保留。

你可能认为这些错误很容易避免,但在实践中,如果不小心,它们可能会发生在各种地方。

彻底整明白的解释

如果还是感觉到有点绕,这里再通俗易懂的解释一番:

func capturePhoto() async throws -> Photo {
    try await withCheckedThrowingContinuation { continuation in
        // 进入函数内部后,这个capturePhoto()异步函数被suspended,也就是被挂起了
        // some completion handler codes
        // 所以一定要在某个时刻resume(),也就是恢复,而且只能resume()一次
        // 当使用withCheckedThrowingContinuation时,
        // 恢复可以使用continuation.resume(returning:)来返回一个值
        // 或者使用continuation.resume(throwing:)来抛出一个异常,注意如果后面还有resume(), 那这里要加上return,避免多次resume
    }
}

 

示例一:

下面这些代码尝试从远程服务器获取JSON返回,并解码到一个叫Message的结构体的数组,使用的是回调函数的方式:

struct Message: Decodable, Identifiable {
    let id: Int
    let from: String
    let message: String
}

func fetchMessages(completion: @Sendable @escaping ([Message]) -> Void) {
    let url = URL(string: "https://hws.dev/user-messages.json")!

    URLSession.shared.dataTask(with: url) { data, response, error in
        if let data {
            if let messages = try? JSONDecoder().decode([Message].self, from: data) {
                completion(messages)
                return
            }
        }

        completion([])
    }.resume()
}

上面使用dataTask(with:)方法确实在自己的线程上运行我们的代码,但这不是Swift async/await的新特性意义上的异步函数,这意味着将它集成到一些使用了async新特性时会很麻烦。

为了解决这个问题,Swift提供了continuations,这是我们作为捕获值传递给完成处理程序的特殊对象。一旦完成处理程序触发,我们可以返回完成值,抛出错误,或者发回一个可以在其他地方处理的Result。对于完成延续所需的时间没有限制,但他们必须在某个时候完成。

下面的代码fetchMessage(),我们编写一个新的异步函数来调用原始函数,在它的完成处理程序中,我们将返回返回的任何值:

struct Message: Decodable, Identifiable {
    let id: Int
    let from: String
    let message: String
}

func fetchMessages(completion: @Sendable @escaping ([Message]) -> Void) {
    let url = URL(string: "https://hws.dev/user-messages.json")!

    URLSession.shared.dataTask(with: url) { data, response, error in
        if let data {
            if let messages = try? JSONDecoder().decode([Message].self, from: data) {
                completion(messages)
                return
            }
        }

        completion([])
    }.resume()
}

func fetchMessages() async -> [Message] {
    await withCheckedContinuation { continuation in
        fetchMessages { messages in
            continuation.resume(returning: messages)
        }
    }
}

let messages = await fetchMessages()
print("Downloaded \(messages.count) messages.")

正如你所看到的,开始一个延续是使用withCheckedContinuation()函数完成的,该函数将我们需要的处理的延续传递给它自己。它被称为“检查”延续,因为Swift会检查我们是否正确使用延续,这意味着要遵守一个非常简单、非常重要的规则:

 你的延续必须恰好恢复一次。不是零次,也不是两次或两次以上。同样,这种情况的发生没有时间限制,但必须在某个时候发生。

如果你调用延续两次或两次以上,这将引起Swift将你的程序停止-它只会崩溃。这听起来很糟糕,但是另外一种选择是做出一些奇怪、不可预测的行为时,崩溃听起来并不是那么糟糕。

另一方面,如果你根本没法恢复延续,Swift讲在你的调试日志中打印出一条类似这样的警告, “SWIFT TASK CONTINUATION MISUSE: fetchMessages() leaked its continuation!”,Swift任务延续错误:fetchMessage()泄露了它的延续!这是因为你让任务挂起,导致它正在使用的任何资源被无期限保留。

你可能认为这些错误很容易避免,但在实践中,如果不小心,它们可能会发生在各种地方。

举个例子,我们在原始的fetchMessages()方法中使用下面的代码:

if let data {
    if let messages = try? JSONDecoder().decode([Message].self, from: data) {
        completion(messages)
        return
    }
}

completion([])

这会检查返回的数据,并在完成和返回之前检查其是否可以正确解码,但如果这两项检查中的任何一项失败,则使用空数组调用完成处理程序——无论发生什么,完成处理程序都会被调用。

但是,如果我们写了一些不同的东西呢?看看你是否能发现这个替代方案的问题:

if let data {
    if let messages = try? JSONDecoder().decode([Message].self, from: data) {
        completion(messages)
    }
} else {
    completion([])
}

它试图将JSON解码为Message数组,并使用完成处理程序发回结果,或者如果服务器没有返回任何内容,则发回一个空数组。

然而,它有一个错误,会导致延续出现问题:如果一些有效数据返回,但无法解码为Message数组,则完成处理程序将永远不会被调用,我们的延续将被泄露。

这两个代码示例非常相似,这表明谨慎使用延续是多么重要。但是,如果你仔细检查了你的代码,并确定它是正确的,如果你想用调用withUnsafeContinuation()来替换withCheckedContinent()函数,它的工作方式完全相同,但不会增加检查你是否正确使用了延续的运行时成本。

我说如果你愿意,你可以这样做,但我对好处持怀疑态度。说“我知道我的代码是安全的,去做吧!”很容易,但除非你使用Instruments分析了你的代码,并且非常确定Swift的额外检查会导致性能问题,否则我会对转向不安全的代码持谨慎态度。

注意:如果你想要一个不返回任何内容的continuation,你可以直接使用continuation.resume(),不带任何参数。

 

下面的内容,您将学习如何转换或使用现有的委托模式,并用Swift的结构化并发性包装它们,以便在您的应用程序中使用异步/等待机制。

我将向您展示一个使用AVCapturePhotoCaptureDelegate的示例,开始捕获照片,并在捕获结束后返回生成的UIImage。

采用委托和使用完成处理程序

在下面的代码示例中,我实现了一个简单的类CameraPhotoProcessor,该类采用AVCapturePhotoCaptureDelegate协议。它的职责是开始捕获照片,并在捕获照片时将其转换为UIImage。它是异步完成的。我引入了一个完成处理程序,可以在实际捕获照片时通知调用startCapture()的代码。完成处理程序将使用Swift的Result类型调用,它将是两个支持的值之一,图像对象成功或错误对象失败。

class CameraPhotoProcessor: NSObject {
    
    private var completion: ((Result<UIImage, Error>) -> Void)?
    
    func startCapture(from photoOutput: AVCapturePhotoOutput, using settings: AVCapturePhotoSettings, completion: @escaping (Result<UIImage, Error>) -> Void) {
        self.completion = completion
        photoOutput.capturePhoto(with: settings, delegate: self)
    }
}

// MARK: - AVCapturePhotoCaptureDelegate

extension CameraPhotoProcessor: AVCapturePhotoCaptureDelegate {
    
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
         if let error = error {
             completion?(.failure(error))
             return
         }

         guard let data = photo.fileDataRepresentation() else {
             completion?(.failure(CustomError(msg: "Cannot get photo file data representation")))
             return
         }

        guard let image = UIImage(data: data) else {
            completion?(.failure(CustomError(msg: "Invalid photo data")))
            return
        }
        
        completion?(.success(image))
    }
}

我们可以看到上面的代码容易出错。将传递的完成处理程序需要使用[weak self]捕获来避免创建保留周期。假设我们有一个在按下捕获按钮时调用的函数。这就是我们如何使用上面的代码:

func captureButtonPressed() {
    photoProcessor.startCapture(from: photoOutput, using: settings) { [weak self] result in
        switch result {
            case .success(let image):
                // We have the image and now we can pass it or process it further
            case .failure(let error):
                print(error.localizedDescription)
        }
    }
}

当使用完成处理程序时,我们总是需要注意代码将在哪里继续执行,以及我们将在闭包下面或里面做什么。对于经验丰富的开发人员来说,这并不重要,但当使用async/await时,代码看起来更清晰,更不容易出错。

使用continuation

我们现在将使用上面的理论,并将其应用于我们的示例中,拍摄一张照片。我们现在将有一个imageContinuation属性,而不是创建completion属性。

在这个例子中,我将使用checked throwing continuation,因为即使我知道我对延续的恢复操作将被调用一次。需要抛出部分,因为我们的照片捕获操作可能会失败,我们需要一种方法在操作中止或失败时抛出错误。

class CameraPhotoProcessor: NSObject {
    
    private var imageContinuation: CheckedContinuation<UIImage, Error>?
    
    func startCapture(from photoOutput: AVCapturePhotoOutput, using settings: AVCapturePhotoSettings) async throws -> UIImage {
        return try await withCheckedThrowingContinuation { continuation in
            imageContinuation = continuation
            photoOutput.capturePhoto(with: settings, delegate: self)
        }
    }
}

// MARK: - AVCapturePhotoCaptureDelegate

extension CameraPhotoProcessor: AVCapturePhotoCaptureDelegate {
    
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
         if let error = error {
             imageContinuation?.resume(throwing: error)
             return
         }

         guard let data = photo.fileDataRepresentation() else {
             imageContinuation?.resume(throwing: CustomError(msg: "Cannot get photo file data representation"))
             return
         }

        guard let image = UIImage(data: data) else {
            imageContinuation?.resume(throwing: CustomError(msg: "Invalid photo data"))
            return
        }
        
        imageContinuation?.resume(returning: image)
    }
}

然后在Swift的Task()种调用

func captureButtonPressed() {
    Task {
        do {
            let image = try await startCapture(from: photoOutput, using: settings)
            // We have the image and now we can pass it or process it further
        } catch let error {
            print(error.localizedDescription)
        }
    }
}

正如我们所看到的,使用Swift的结构化并发更安全,更不容易出错。当使用多个异步操作时,它还可以提高可读性。想象一下,我们有三个使用完成处理程序的异步操作。在这种情况下,我们需要将代码缩进三个级别才能使用最终结果。当使用async/await时,我们没有这个功能。一切都是直线:)

 

 

文哥博客(https://wenge365.com)属于文野个人博客,欢迎浏览使用

联系方式:qq:52292959 邮箱:52292959@qq.com

备案号:粤ICP备18108585号 友情链接