CoreData导致应用程序崩溃:崩溃:NSOperationQueue 0x10530af90QOS:未定

问题描述

我正在使用URLRequest将视频文件上传到s3服务器。

1:文件值来自CoreData
2:文件成功上传后,将从CoreData删除

这是我的UploadManager类

struct APINotificationParameters: Codable {
    let status: String
}

final class UploadManager: NSObject {
    enum FileStatus {
        case uploading,uploaded,error
    }
    
    private var BASE_API = UserDefaults.standard.value(forKey: "CustomWebUrl") as? String ?? ""
    
    private var cancellableSet: Set<AnyCancellable>
    private var storage: NSPersistentContainer

    init(storage: NSPersistentContainer) {
        self.storage = storage
        self.cancellableSet = []
        super.init()
    }

    private func notifyApi(recordingId: String,fileId: String,fileStatus: FileStatus) {
        BASE_API = UserDefaults.standard.value(forKey: "CustomWebUrl") as? String ?? ""
        let url = URL(string: "\(self.BASE_API)/guest/recordings/\(recordingId)/files/\(fileId)")
        
        let status: String
        switch fileStatus {
        case .uploading:
            status = "uploading"
        case .uploaded:
            status = "uploaded"
        case .error:
            status = "error"
        }
        
        let jsonEncoder = JSONEncoder()
        
        var request = URLRequest(url: url!)
        request.httpMethod = "PATCH"
        request.addValue("application/json",forHTTPHeaderField: "Content-Type")
        request.httpBody = try! jsonEncoder.encode(APINotificationParameters(status: status))
        
        URLSession.shared.dataTaskPublisher(for: request)
            .tryMap { data,response in
                // Check for fundamental networking error.
                guard let response = response as? HTTPURLResponse else {
                    // print("UnkNown error while fetching session information.")
                    throw APIError.unkNown
                }

                // Check for http errors.
                guard (200 ... 299) ~= response.statusCode else {
                    // print("HTTP error with status code = \(response.statusCode).")
                    throw APIError.http(reason: "Failed with http status = \(response.statusCode)")
                }
            }
            .mapError { error -> APIError in
                if let error = error as? APIError {
                    return error
                } else {
                    return APIError.network(reason: error.localizedDescription)
                }
            }
            .erasetoAnyPublisher()
            .receive(on: RunLoop.main)
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    print("Sucessfully notified api")
                    break
                case .failure(let error):
                    print(error.localizedDescription)
                }
            },receiveValue: {})
            .store(in: &self.cancellableSet)
    }

    private func deleteFile(path: URL) {
        let fileManager = FileManager()
        do {
            try fileManager.removeItem(at: path)
            print("Deleted file: \(path.absoluteString)")
        } catch {
            print("Impossible to delete local file: \(error.localizedDescription)")
        }
    }
    
    private func uploadFiletoS3(remoteUrl: URL,localPath: URL,fileId: String) {
        var request = URLRequest(url: remoteUrl)
        request.httpMethod = "PUT"
        let config = URLSessionConfiguration.background(withIdentifier: fileId)
        let session = URLSession(configuration: config,delegate: self,delegateQueue: nil)
        let task = session.uploadTask(with: request,fromFile: localPath)
        task.resume()
    }
    
    func uploadFile(remoteUrl: URL,localName: String,recordingId: String,recordingName: String,sessionId: String) {
        let context = self.storage.viewContext
        
        let uploadRecord = UploadRecord(context: context)
        uploadRecord.fileId = fileId
        uploadRecord.localFileName = localName
        uploadRecord.recordingId = recordingId
        uploadRecord.recordingName = recordingName
        uploadRecord.sessionId = sessionId
        
        // store file id here
        UserDefaults.standard.setValue(fileId,forKey: "uploadingFileID")
        
        //try! context.save()
        do {
            try context.save()
        } catch {
            print("An error occurred while saving: \(error)")
        }
        
        self.notifyApi(recordingId: recordingId,fileId: fileId,fileStatus: .uploading)

        self.uploadFiletoS3(remoteUrl: remoteUrl,localPath: localPath,fileId: fileId)
    }
}

extension UploadManager: URLSessionTaskDelegate {
    private func findUploadRecord(fileId: String) -> UploadRecord? {
        let context = self.storage.viewContext
        let uploadFetch = NSFetchRequest<UploadRecord>(entityName: "Upload")
        uploadFetch.predicate = nspredicate(format: "fileId == %@",fileId)
        var uploadRecords = [UploadRecord]()
        do {
            uploadRecords = try context.fetch(uploadFetch)
            return uploadRecords[0]
        } catch let error {
            print(error.localizedDescription)
            return nil
        }
    }
    
    func urlSession(_ session: URLSession,task: URLSessionTask,didCompleteWithError error: Error?) {
        guard let fileId = session.configuration.identifier,error == nil else {
            // Todo(jdaeli): Retry upload
            print("Upload Failed: \(String(describing: error))")
            return;
        }
        
        let context = self.storage.viewContext
        let uploadRecord = self.findUploadRecord(fileId: fileId)
        let localName = uploadRecord?.localFileName ?? ""
        
        print("Uploaded: \(localName)")
        
        let documentsDirectory = FileManager.default.urls(for: .documentDirectory,in: .userDomainMask).last
        let localPath = documentsDirectory?.appendingPathComponent(localName)
        self.deleteFile(path: localPath!)
        
        // self.notifyApi(recordingId: recordingId,fileId:fileId,fileStatus: .uploaded)
        
        dispatchQueue.main.async {
            context.delete(uploadRecord ?? UploadRecord())
            //try? context.save()
            do {
                try context.save()
            } catch {
                print("Failed saving")
            }
        }
    }
    
    func urlSession(_ session: URLSession,didSendBodyData bytesSent: Int64,totalBytesSent: Int64,totalBytesExpectedToSend: Int64) {
        let fileId = session.configuration.identifier!
        let context = self.storage.viewContext
        let recordUpload = self.findUploadRecord(fileId: fileId)
        dispatchQueue.main.async {
            recordUpload?.totalBytesSent = totalBytesSent
            recordUpload?.totalBytes = totalBytesExpectedToSend
            
            let fileID = UserDefaults.standard.value(forKey: "uploadingFileID") as? String ?? ""
            do {
                try context.save()
            } catch {
                print("Failed saving")
            }
            //print("Sent \(totalBytesSent) of \(totalBytesExpectedToSend)")            
            // only show on going uplaod progress
            if recordUpload?.fileId == fileID {
                //print("same file,need to show")
                let uploadProgress = (String(format: "%.2f",Double(recordUpload?.totalBytesSent ?? 0)/Double(recordUpload?.totalBytes ?? 0)*100))
                // store value here,so we can use in uploading progress
                UserDefaults.standard.setValue(uploadProgress,forKey: "uploadingProgress")
            }
            
        }
    }
}

它工作正常,但是有时在视频上传过程中应用程序崩溃了,当我登录Firebase Crashlytics时,发现此图片下方检查

enter image description here

应用程序在这里崩溃

       do {
            uploadRecords = try context.fetch(uploadFetch)
                return uploadRecords[0]
            } catch let error {
                print(error.localizedDescription)
                return nil
            }

此行 uploadRecords =试试context.fetch(uploadFetch)

即使我正在使用“执行捕获”功能,应用仍然崩溃。 问题未在我的手机中重现,但客户端面临此类崩溃。

代码有问题,或者CoreData没有同时管理多个视频上传

修复此崩溃所需的帮助很大。

解决方法

崩溃可能是由于无条件访问空数组的索引所致,所以我建议改为类似

if let uploadRecords = try? context.fetch(uploadFetch),!uploadRecords.isEmpty {
    return uploadRecords[0]
} else {
    return nil
}