You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift开发macOS RPA工具时DispatchQueue.asyncAfter重复执行挂起问题

问题产生原因
  • 核心问题是你的整个自动化流程执行逻辑全部运行在主队列上,且你在步骤切换时调用了usleep(400000)直接阻塞主队列:主队列是串行队列,一旦被阻塞,所有依赖主队列调度的异步任务(包括DispatchQueue.main.asyncAfter的回调、Timer的触发回调)都无法执行,就会出现流程挂起的现象。
  • 调试时挂起自动解除的原因是Xcode调试状态会激活App的runloop调度,同时App切换到前台会退出低功耗模式,原本被积压的异步任务会被一次性执行。
  • sleep能正常运行的原因是它是同步阻塞逻辑,不需要依赖队列的异步调度,等待时间到后会直接在当前线程执行回调,不会被队列的阻塞影响,但也因此直接卡住主队列导致UI无响应。
  • 修改Info.plist的Application does not run in background配置不生效是因为该配置是iOS平台专属,macOS平台的后台运行策略不识别该配置。
解决方案

核心思路是把自动化流程的执行逻辑完全迁移到独立的后台串行队列,避免阻塞主队列,同时所有延迟逻辑都使用异步实现:

  1. 为AutomationPlayer创建独立的执行队列,使用userInitiated优先级避免后台运行时被系统节流
  2. 移除主队列上的所有阻塞类方法(usleep/sleep),替换为对应队列的异步延迟实现
  3. Pause类的延迟逻辑使用独立执行队列的asyncAfter或者DispatchSourceTimer实现,不再依赖主队列

修改后的代码示例如下:

修改后的AutomationPlayer代码

class AutomationPlayer {
  
  static let shared = AutomationPlayer()
  
  var automation: Automation?
  var stepArray: [Step] = []
  var isCancelled = false // 新增:支持用户主动终止流程
  // 新增:独立的后台执行队列,QoS设置为userInitiated保证执行优先级
  private let executionQueue = DispatchQueue(label: "com.yourapp.rpa.execution", qos: .userInitiated)
  
  func play() {
    isCancelled = false
    // Create array of steps
    guard let steps = automation?.steps, let array = Array(steps) as? [Step] else { 
      return 
    }
    stepArray.removeAll()
    // If the automation repeats, add more steps to array.
    for _ in 0..<(automation?.numberOfRepeats ?? 1) {
      stepArray.append(contentsOf: array)
    }
    // 初始延迟切到执行队列运行
    executionQueue.asyncAfter(deadline: .now() + 0.4) { [weak self] in
      self?.execute(index: 0)
    }
  }
  
  // 新增:用户主动终止流程的方法
  func stop() {
    isCancelled = true
    stepArray.removeAll()
  }
  
  private func execute(index: Int) {
    // 每次执行前判断是否被终止
    guard !isCancelled, index < stepArray.count else {
      stepArray = []
      return
    }
    let step = stepArray[index]
    
    executeStep(step: step) { [weak self] success, error in
      guard let self = self, !self.isCancelled, error == nil else { return }
      let newIndex = index + 1
      if newIndex < self.stepArray.count {
        // 把原来的usleep改为异步延迟,不阻塞队列
        self.executionQueue.asyncAfter(deadline: .now() + 0.4) { [weak self] in
          self?.execute(index: newIndex)
        }
      } else {
        self.stepArray = []
      }
    }
  }
  
  private func executeStep(step: Step?, completionHandler: @escaping (Bool, Error?) -> Void) -> Void {
    step?.execute(completionHandler: { success, error in
      guard error == nil else {
        completionHandler(false, error)
        return
      }
      completionHandler(true, nil)
    })
  }
}

修改后的Pause类代码

@objc(Pause)
public class Pause: Step {
  
  override func execute(completionHandler: @escaping (Bool, Error?) -> Void)  {
    print("Pause for: \(self.time) seconds")
    // 直接使用AutomationPlayer的执行队列做异步延迟,不依赖主队列
    AutomationPlayer.shared.executionQueue.asyncAfter(deadline: .now() + Double(self.time)) {
      completionHandler(true, nil)
    }
  }
}

如果需要更稳定、不受App活跃状态影响的延迟,可以用DispatchSourceTimer实现Pause:

@objc(Pause)
public class Pause: Step {
  
  override func execute(completionHandler: @escaping (Bool, Error?) -> Void)  {
    print("Pause for: \(self.time) seconds")
    let timer = DispatchSource.makeTimerSource(queue: AutomationPlayer.shared.executionQueue)
    timer.schedule(deadline: .now() + Double(self.time))
    timer.setEventHandler {
      timer.cancel()
      completionHandler(true, nil)
    }
    timer.resume()
  }
}

内容的提问来源于stack exchange,提问作者Flightfire

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.01 16:18:03