Сбой EXC_BAD_ACCESS KERN_INVALID_ADDRESS в addOperation of OperationQueue

У меня есть реализация асинхронной операции следующим образом:

      class AsyncOperation: Operation {
  private enum State: String {
    case ready, executing, finished

    fileprivate var keyPath: String {
      return "is\(rawValue.capitalized)"
    }
  }
  
  private let stateAccessQueue = DispatchQueue(label: "com.beamo.asyncoperation.state")

  private var unsafeState = State.ready
  
  private var state: State {
    get {
      return stateAccessQueue.sync {
        return unsafeState
      }
    }
    set {
      willChangeValue(forKey: newValue.keyPath)
      stateAccessQueue.sync(flags: [.barrier]) {
        self.unsafeState = newValue
      }
      didChangeValue(forKey: newValue.keyPath)
    }
  }

  override var isReady: Bool {
    return super.isReady && state == .ready
  }

  override var isExecuting: Bool {
    return state == .executing
  }

  override var isFinished: Bool {
    return state == .finished
  }

  override var isAsynchronous: Bool {
    return true
  }
 
  override func start() {
    if isCancelled {
      if !isFinished {
        finish()
      }
      return
    }

    state = .executing
    main()
  }
  
  public final func finish() {
    state = .finished
  }
}

Приложение будет загружать несколько ресурсов с помощью API. Реализация загрузчика управляется очередью операций и пользовательской асинхронной операцией. Вот реализация работы асинхронного загрузчика:

      final class DownloaderOperation: AsyncOperation {
  private let downloadTaskAccessQueue = DispatchQueue(label: "com.download.task.access.queue")
  
  private var downloadTask: URLSessionDownloadTask?
  private var threadSafeDownloadTask: URLSessionDownloadTask? {
    get {
      return downloadTaskAccessQueue.sync {
        return downloadTask
      }
    }
    set {
      downloadTaskAccessQueue.sync(flags: [.barrier]) {
        self.downloadTask = newValue
      }
    }
  }
  
  private let session: URLSession
  let download: Download
  let progressHandler: DownloadProgressHandler
  init(session: URLSession,
       download: Download,
       progressHandler: @escaping DownloadProgressHandler) {
    self.session = session
    self.download = download
    self.progressHandler = progressHandler
  }
  
  override func main() {
    let bodyModel = DownloaderRequestBody(fileUrl: download.fileUrl.absoluteString)
    let bodyData = (try? JSONEncoder().encode(bodyModel)) ?? Data()
    
 
    var request = URLRequest(url: URL(string: "api url here")!)
    request.httpMethod = "POST"
    request.httpBody = bodyData
    let task = session.downloadTask(with: request)
    task.countOfBytesClientExpectsToSend = 512
    task.countOfBytesClientExpectsToSend = 1 * 1024 * 1024 * 1024 // 1GB
    
    task.resume()
    threadSafeDownloadTask = task
    DispatchQueue.main.async {
      self.download.progress = 0
      self.download.status = .inprogress
      self.progressHandler(self.model, 0)
    }
  }
  
  override func cancel() {
    super.cancel()
    
    threadSafeDownloadTask?.cancel()
  }
}

И реализация загрузчика здесь:

      final class MultipleURLSessionDownloader: NSObject {
  private(set) var unsafeOperations: [Download: DownloaderOperation] = [:]
  private(set) var progressHandlers: [Download: DownloadProgressHandler] = [:]
  private(set) var completionHandlers: [Download: DownloadCompletionHandler] = [:]
  
  private let operationsAccessQueue = DispatchQueue(label: "com.multiple.downloader.operations")
  
  private(set) var operations: [Download: DownloaderOperation] {
    get {
      return operationsAccessQueue.sync {
        return unsafeOperations
      }
    }
    set {
      operationsAccessQueue.sync(flags: [.barrier]) {
        self.unsafeOperations = newValue
      }
    }
  }
  
  private lazy var downloadOperationQueue: OperationQueue = {
    var queue = OperationQueue()
    queue.name = "com.multiple.downloader.queue"
    queue.maxConcurrentOperationCount = 2
    return queue
  }()
  
  private(set) var urlSession = URLSession.shared
  init(configuration: URLSessionConfiguration = URLSessionConfiguration.background(withIdentifier: "com.multiple.downloader.session")) {
    super.init()
    configuration.isDiscretionary = false
    configuration.sessionSendsLaunchEvents = true
    configuration.timeoutIntervalForResource = 120
    configuration.timeoutIntervalForRequest  = 10
    self.urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
  }
  
  deinit {
    debugPrint("[MultipleURLSessionDownloader] deinit")
  }
  
  func startDownload(downloads: [Download],
                     progressHandler: @escaping DownloadProgressHandler,
                     completionHandler: @escaping DownloadCompletionHandler) {
    for download in downloads {
      startDownload(
        download: download,
        progressHandler: progressHandler,
        completionHandler: completionHandler
      )
    }
  }
  
  func cancelDownload(download: Download) {
    cancelOperation(download: download)
  }
  
  func cancelAllDownloads() {
    for download in operations.keys {
      cancelOperation(download: download)
    }
    urlSession.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks  in
      dataTasks.forEach {
        $0.cancel()
      }
      uploadTasks.forEach {
        $0.cancel()
      }
      downloadTasks.forEach {
        $0.cancel()
      }
    }
  }
  
  private func startDownload(download: Download,
                             progressHandler: @escaping DownloadProgressHandler,
                             completionHandler: @escaping DownloadCompletionHandler) {
    download.status = .default
    
    progressHandlers[download] = progressHandler
    completionHandlers[download] = completionHandler
    
    if let operation = operations[download] {
      if operation.isExecuting,
         let inProgressDownload = operations.keys.first(where: { $0.id == download.id }) {
        progressHandlers[download]?(inProgressDownload, inProgressDownload.progress ?? 0)
      }
    } else {
      let operation = DownloaderOperation(
        session: urlSession,
        download: download) {[weak self] (progressDownload, progress) in
          self?.progressHandlers[progressDownload]?(progressDownload, progress)
      }
      operations[download] = operation
      
      downloadOperationQueue.addOperation(operation)
    }
  }
  
  private func cancelOperation(download: Download) {
    download.status = .cancelled
    operations[download]?.cancel()
    callCompletion(download: download, error: nil)
  }
  
  private func callCompletion(download: Download,
                              error: DownloadError?) {
    operations[download]?.finish()
    operations[download] = nil
    progressHandlers[download] = nil
    download.progress = nil
    let handler = completionHandlers[download]
    completionHandlers[download] = nil
    handler?(download, error)
  }

}

Существует 2 метода отмены:

  1. cancelDownload(download: Download)
  2. cancelAllDownloads()

Если загрузка отменена и несколько раз пытались загрузить приложение, происходит сбой, и журнал сбоев находится здесь:

Если загрузка отменена все и несколько раз пытались загрузить приложение, происходит сбой, и журнал сбоев находится здесь:

И вот что странно: если я открою приложение, запустив его из Xcode, сбоя не произойдет. Это происходит только тогда, когда я открываю приложение с устройства без запуска Xcode.

На данный момент я исправил повторную очередь операций с очередью отправки следующим образом: downloadOperationQueue.addOperation(operation)

      downloadDispatchQueue.async {
    operation.start()
  }

И это работает нормально, без каких-либо сбоев.

Я думаю, что сбой происходит наaddOperationметодOperationQueue, но я не знаю причину.

0 ответов

Другие вопросы по тегам