【问题标题】:Waiting for completion handler to complete, before continuing等待完成处理程序完成,然后再继续
【发布时间】:2017-07-14 04:37:20
【问题描述】:

我有一组 9 张图片,我想将它们全部保存到用户的相机胶卷中。你可以通过UIImageWriteToSavedPhotosAlbum 做到这一点。我写了一个循环来保存每个图像。问题在于,由于某种原因,它将only save the first five。现在,顺序很重要,所以如果图像保存失败,我想重试并等待它成功,而不是进行一些不可预知的比赛。

所以,我实现了一个完成处理程序,并认为我可以像这样使用信号量:

func save(){
    for i in (0...(self.imagesArray.count-1)).reversed(){
        print("saving image at index ", i)
        semaphore.wait()

        let image = imagesArray[i]
        self.saveImage(image)

    }
}

func saveImage(_ image: UIImage){
    UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
}

func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) {
    //due to some write limit, only 5 images get written at once.
    if let error = error {
        print("trying again")
        self.saveImage(image)
    } else {
        print("successfully saved")
        semaphore.signal()
    }
}

我的代码的问题是它在第一次保存后被阻止并且 semaphore.signal 永远不会被调用。我在想我的完成处理程序应该在主线程上调用,但已经被 semaphore.wait() 阻塞了。任何帮助表示赞赏。谢谢

【问题讨论】:

  • 您应该尝试将您的代码放入 Dispatach.global 队列中,这肯定会有所帮助
  • @MikeAlter 它确实有帮助。现在可以了,但我不知道为什么,你能帮忙解释一下吗?谢谢

标签: ios multithreading grand-central-dispatch semaphore


【解决方案1】:

正如其他人所指出的,您希望避免在主线程上等待,从而冒死锁的风险。因此,虽然您可以将其推送到全局队列,但另一种方法是使用众多机制中的一种来执行一系列异步任务。选项包括异步Operation 子类或承诺(例如PromiseKit)。

例如,要将图像保存任务包装在异步Operation 中并将它们添加到OperationQueue,您可以像这样定义图像保存操作:

class ImageSaveOperation: AsynchronousOperation {

    let image: UIImage
    let imageCompletionBlock: ((NSError?) -> Void)?

    init(image: UIImage, imageCompletionBlock: ((NSError?) -> Void)? = nil) {
        self.image = image
        self.imageCompletionBlock = imageCompletionBlock

        super.init()
    }

    override func main() {
        UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
    }

    func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) {
        imageCompletionBlock?(error)
        complete()
    }

}

然后,假设您有一个数组images,即[UIImage],您可以这样做:

let queue = OperationQueue()
queue.name = Bundle.main.bundleIdentifier! + ".imagesave"
queue.maxConcurrentOperationCount = 1

let operations = images.map {
    return ImageSaveOperation(image: $0) { error in
        if let error = error {
            print(error.localizedDescription)
            queue.cancelAllOperations()
        }
    }
}

let completion = BlockOperation {
    print("all done")
}
operations.forEach { completion.addDependency($0) }

queue.addOperations(operations, waitUntilFinished: false)
OperationQueue.main.addOperation(completion)

显然,您可以自定义它以在出错时添加重试逻辑,但现在可能不需要这样做,因为“太忙”问题的根源是太多并发保存请求的结果,我们已经消除了这一点。这只留下不太可能通过重试解决的错误,所以我可能不会添加重试逻辑。 (错误更可能是权限失败、空间不足等)但如果您真的需要,您可以添加重试逻辑。更有可能的是,如果您有错误,您可能只想取消队列中所有剩余的操作,就像我上面所说的那样。

注意,上述子类AsynchronousOperation,只是Operation 的子类isAsynchronous 返回true。例如:

/// Asynchronous Operation base class
///
/// This class performs all of the necessary KVN of `isFinished` and
/// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer
/// a concurrent NSOperation subclass, you instead subclass this class which:
///
/// - must override `main()` with the tasks that initiate the asynchronous task;
///
/// - must call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
///   necessary and then ensuring that `completeOperation()` is called; or
///   override `cancel` method, calling `super.cancel()` and then cleaning-up
///   and ensuring `completeOperation()` is called.

public class AsynchronousOperation : Operation {

    private let syncQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".opsync")

    override public var isAsynchronous: Bool { return true }

    private var _executing: Bool = false
    override private(set) public var isExecuting: Bool {
        get {
            return syncQueue.sync { _executing }
        }
        set {
            willChangeValue(forKey: "isExecuting")
            syncQueue.sync { _executing = newValue }
            didChangeValue(forKey: "isExecuting")
        }
    }

    private var _finished: Bool = false
    override private(set) public var isFinished: Bool {
        get {
            return syncQueue.sync { _finished }
        }
        set {
            willChangeValue(forKey: "isFinished")
            syncQueue.sync { _finished = newValue }
            didChangeValue(forKey: "isFinished")
        }
    }

    /// Complete the operation
    ///
    /// This will result in the appropriate KVN of isFinished and isExecuting

    public func complete() {
        if isExecuting { isExecuting = false }

        if !isFinished { isFinished = true }
    }

    override public func start() {
        if isCancelled {
            isFinished = true
            return
        }

        isExecuting = true

        main()
    }
}

现在,我很欣赏操作队列(或承诺)对于您的情况来说似乎有点矫枉过正,但它是一种有用的模式,您可以在任何有一系列异步任务的地方使用它。有关操作队列的更多信息,请随时参考Concurrency Programming Guide: Operation Queues

【讨论】:

    【解决方案2】:

    一年前我遇到了同样的问题

    您应该尝试将您的代码放入Dispatach.global 队列中,这肯定会有所帮助

    原因:我也真的不知道原因,我认为它是什么信号量,可能需要在后台线程中执行以同步等待和信号

    【讨论】:

      【解决方案3】:

      正如 Mike alter 所说,使用Dispatch.global().async 有助于解决问题。现在的代码如下所示:

      func save(){
          for i in (0...(self.imagesArray.count-1)).reversed(){
              DispatchQueue.global().async { [unowned self] in
                  self.semaphore.wait()
      
                  let image = self.imagesArray[i]
                  self.saveImage(image)
              }
          }
      }
      

      我怀疑问题在于完成处理程序在主线程中执行,该线程已被最初调用的semaphore.wait() 锁定。所以当完成发生时,semaphore.signal() 永远不会被调用。

      这可以通过在异步队列中运行任务来解决。

      【讨论】:

        猜你喜欢
        • 2013-04-25
        • 2019-11-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-05-06
        • 2021-05-21
        • 1970-01-01
        相关资源
        最近更新 更多