iOS:为什么我的大文件没有用NSData(contentsOfFile:options :)转换?错误域= NSCocaErrorDomain代码= 256

时间:2018-12-13 16:48:10

标签: ios swift nsdata

我尝试使用NSData转换视频,它在处理小视频或100mb时效果很好,但是我的大文件(4.44Gb)无法发送...

   var video_data: NSData?
    do {
        video_data = try NSData(contentsOfFile: (videoPath), options: NSData.ReadingOptions.alwaysMapped)
    } catch let error as NSError {
        video_data = nil
        return
    }

如何将大文件放入NSData?

Error Domain=NSCocoaErrorDomain Code=256 "Impossible d’ouvrir le fichier « D9C7DABF-4BE3-4105-8D76-AA92B1D1502E_video.notsend »." UserInfo={NSFilePath=/var/mobile/Containers/Data/Application/EAE9B4C4-BE6B-490C-BEE7-381B2DF27CC9/Library/LEADS/D9C7DABF-4BE3-4105-8D76-AA92B1D1502E_video.notsend, NSUnderlyingError=0x283be1380 {Error Domain=NSPOSIXErrorDomain Code=12 "Cannot allocate memory"}}

有什么想法吗?

谢谢。

编辑1: 要发送的参数:
这是整个功能。我需要所有这些参数才能发送到我的服务器。我需要在数据值中发送eventId,contactId,类型和文件。问题是我有一个错误,我不知道如何使用InputStream将4.44Go文件放入数据中。

 func uploadVideo(_ videoPath: String, fileName: String, eventId: Int, contactId: Int, type: Int, callback: @escaping (_ data:Data?, _ resp:HTTPURLResponse?, _ error:NSError?) -> Void)
    {
        var video_data: Data
        video_data = self.getNextChunk(urlOfFile: NSURL(string: videoPath)! as URL)!

        let WSURL:String =  "https://" + "renauldsqffssfd3.sqdfs.fr/qsdf"

        let requestURLString = "\(WSURL)/qsdfqsf/qsdf/sdfqs/dqsfsdf/"
        let url = URL(string: requestURLString)
        let request = NSMutableURLRequest(url: url!)
        request.httpMethod = "POST"

        let boundary = generateBoundaryString()
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        request.setValue("Keep-Alive", forHTTPHeaderField: "Connection")

        let body = NSMutableData()
        let mimetype = "video/mp4"

        //define the data post parameter
        body.append("--\(boundary)\r\n".data(using: String.Encoding.utf8)!)
        body.append("Content-Disposition:form-data; name=\"eventId\"\r\n\r\n".data(using: String.Encoding.utf8)!)
        body.append("\(eventId)\r\n".data(using: String.Encoding.utf8)!)

        body.append("--\(boundary)\r\n".data(using: String.Encoding.utf8)!)
        body.append("Content-Disposition:form-data; name=\"contactId\"\r\n\r\n".data(using: String.Encoding.utf8)!)
        body.append("\(contactId)\r\n".data(using: String.Encoding.utf8)!)

        body.append("--\(boundary)\r\n".data(using: String.Encoding.utf8)!)
        body.append("Content-Disposition:form-data; name=\"type\"\r\n\r\n".data(using: String.Encoding.utf8)!)
        body.append("\(type)\r\n".data(using: String.Encoding.utf8)!)

        body.append("--\(boundary)\r\n".data(using: String.Encoding.utf8)!)
        body.append("Content-Disposition:form-data; name=\"file\"; filename=\"\(fileName)\"\r\n".data(using: String.Encoding.utf8)!)
        body.append("Content-Type: \(mimetype)\r\n\r\n".data(using: String.Encoding.utf8)!)
        body.append(video_data)
        body.append("\r\n".data(using: String.Encoding.utf8)!)

        body.append("--\(boundary)--\r\n".data(using: String.Encoding.utf8)!)

        request.httpBody = body as Data

        let configuration = URLSessionConfiguration.default
        let session = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main)

        let task = session.uploadTask(with: request as URLRequest, from: body as Data) { loc, resp, err in
            if (resp != nil)
            {
                let status = (resp as! HTTPURLResponse).statusCode
            }
            callback(loc, resp as? HTTPURLResponse, err as NSError?)
        }

        task.resume()
}

  public func getNextChunk(urlOfFile: URL) -> Data?{
        if inputStream == nil {
            inputStream = InputStream(url: urlOfFile)!
            inputStream!.open()
        }
        var buffer = [UInt8](repeating: 0, count: 1024*1024)
        let len = inputStream!.read(&buffer, maxLength: 1024*1024)
        if len == 0 {
            return nil
        }
        return Data(buffer)
    }

编辑2: 对解决方案的补充:

上面的Rob解决方案是完美的。我只是添加了一个磁盘空间控制功能,以警告临时文件是否无法复制,如果临时文件不完整,则将其删除,最后将问题告知用户。
实际上,如果没有该控件,应用程序将尝试将文件发送到服务器,即使文件不完整...

  func sizeOfFileAtPath(path: String) -> UInt64
        {
            var fileSize : UInt64

            do {
                //return [FileAttributeKey : Any]
                let attr = try FileManager.default.attributesOfItem(atPath: path)
                fileSize = attr[FileAttributeKey.size] as! UInt64

                //if you convert to NSDictionary, you can get file size old way as well.
                let dict = attr as NSDictionary
                fileSize = dict.fileSize()
                            return fileSize

            } catch {
                print("Error: \(error)")
            }

            return 0
        }

    private func buildPayloadFile(videoFileURL: URL, boundary: String, fileName: String, eventId: Int, contactId: Int, type: Int) throws -> URL {
        let mimetype = "video/mp4"

        let payloadFileURL = URL(fileURLWithPath: NSTemporaryDirectory())
            .appendingPathComponent(UUID().uuidString)

        guard let stream = OutputStream(url: payloadFileURL, append: false) else {
            throw UploadError.unableToOpenPayload(payloadFileURL)
        }

        stream.open()

        //define the data post parameter
        stream.write("--\(boundary)\r\n")
        stream.write("Content-Disposition:form-data; name=\"eventId\"\r\n\r\n")
        stream.write("\(eventId)\r\n")

        stream.write("--\(boundary)\r\n")
        stream.write("Content-Disposition:form-data; name=\"contactId\"\r\n\r\n")
        stream.write("\(contactId)\r\n")

        stream.write("--\(boundary)\r\n")
        stream.write("Content-Disposition:form-data; name=\"type\"\r\n\r\n")
        stream.write("\(type)\r\n")

        stream.write("--\(boundary)\r\n")
        stream.write("Content-Disposition:form-data; name=\"file\"; filename=\"\(fileName)\"\r\n")
        stream.write("Content-Type: \(mimetype)\r\n\r\n")
        if stream.append(contentsOf: videoFileURL) < 0 {
            throw UploadError.unableToOpenVideo(videoFileURL)
        }
        stream.write("\r\n")

        stream.write("--\(boundary)--\r\n")
        stream.close()

/*-------BEGIN ADDITION TO THE CODE---------*/
        //check the size
        let temporaryFileSize = self.sizeOfFileAtPath(path: payloadFileURL.relativePath)
        let originalFileSize = self.sizeOfFileAtPath(path: videoFileURL.relativePath)

        if (temporaryFileSize < originalFileSize || temporaryFileSize == 0)
        {
            let alert = UIAlertView()
            alert.title = "Alert"
            alert.message = "There is not enough space on the disk."
            alert.addButton(withTitle: "Ok")
            alert.show()

            do {
                try FileManager.default.removeItem(at: payloadFileURL)
            } catch let error as NSError {
                print("Error: \(error.domain)")
            }
        }  
/*-------END ADDITION TO THE CODE---------*/

        return payloadFileURL
    }

3 个答案:

答案 0 :(得分:4)

处理如此大的资产时,您要避免完全使用Data(和NSData)。所以:

  • 使用InputStream阅读视频;
  • 使用OutputStream将请求的正文写入另一个文件;和
  • 将该有效负载上传为文件,而不是设置请求的httpBody;和
  • 确保随后进行清理,删除该临时有效负载文件。

所有这些都避免了一次将整个资产加载到内存中,并且您的峰值内存使用率将大大低于使用Data时的峰值。这也确保了由于内存不足而导致此操作永远不会失败。

func uploadVideo(_ videoPath: String, fileName: String, eventId: Int, contactId: Int, type: Int, callback: @escaping (_ data: Data?, _ resp: HTTPURLResponse?, _ error: Error?) -> Void) {
    let videoFileURL = URL(fileURLWithPath: videoPath)
    let boundary = generateBoundaryString()

    // build the request

    let request = buildRequest(boundary: boundary)

    // build the payload

    let payloadFileURL: URL

    do {
        payloadFileURL = try buildPayloadFile(videoFileURL: videoFileURL, boundary: boundary, fileName: fileName, eventId: eventId, contactId: contactId, type: type)
    } catch {
        callback(nil, nil, error)
        return
    }

    // perform the upload

    performUpload(request, payload: payloadFileURL, callback: callback)
}

enum UploadError: Error {
    case unableToOpenPayload(URL)
    case unableToOpenVideo(URL)
}

private func buildPayloadFile(videoFileURL: URL, boundary: String, fileName: String, eventId: Int, contactId: Int, type: Int) throws -> URL {
    let mimetype = "video/mp4"

    let payloadFileURL = URL(fileURLWithPath: NSTemporaryDirectory())
        .appendingPathComponent(UUID().uuidString)

    guard let stream = OutputStream(url: payloadFileURL, append: false) else {
        throw UploadError.unableToOpenPayload(payloadFileURL)
    }

    stream.open()

    //define the data post parameter
    stream.write("--\(boundary)\r\n")
    stream.write("Content-Disposition:form-data; name=\"eventId\"\r\n\r\n")
    stream.write("\(eventId)\r\n")

    stream.write("--\(boundary)\r\n")
    stream.write("Content-Disposition:form-data; name=\"contactId\"\r\n\r\n")
    stream.write("\(contactId)\r\n")

    stream.write("--\(boundary)\r\n")
    stream.write("Content-Disposition:form-data; name=\"type\"\r\n\r\n")
    stream.write("\(type)\r\n")

    stream.write("--\(boundary)\r\n")
    stream.write("Content-Disposition:form-data; name=\"file\"; filename=\"\(fileName)\"\r\n")
    stream.write("Content-Type: \(mimetype)\r\n\r\n")
    if stream.append(contentsOf: videoFileURL) < 0 {
        throw UploadError.unableToOpenVideo(videoFileURL)
    }
    stream.write("\r\n")

    stream.write("--\(boundary)--\r\n")
    stream.close()

    return payloadFileURL
}

private func buildRequest(boundary: String) -> URLRequest {
    let WSURL = "https://" + "renauldsqffssfd3.sqdfs.fr/qsdf"

    let requestURLString = "\(WSURL)/qsdfqsf/qsdf/sdfqs/dqsfsdf/"
    let url = URL(string: requestURLString)!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"

    request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
    request.setValue("Keep-Alive", forHTTPHeaderField: "Connection")

    return request
}

private func performUpload(_ request: URLRequest, payload: URL, callback: @escaping (_ data: Data?, _ resp: HTTPURLResponse?, _ error: Error?) -> Void) {
    let session = URLSession(configuration: .default, delegate: self, delegateQueue: .main)

    let task = session.uploadTask(with: request, fromFile: payload) { data, response, error in
        try? FileManager.default.removeItem(at: payload) // clean up after yourself

        if let response = response as? HTTPURLResponse {
            let status = response.statusCode
        }

        callback(data, response as? HTTPURLResponse, error)
    }

    task.resume()
}

顺便说一句,将其上传为文件还具有一个优点,即您可以考虑在将来的某个日期使用背景URLSessionConfiguration(即,上传4 GB视频的时间可能很长,以至于用户可能不倾向于让应用程序运行并让上传完成;即使您的应用程序不再运行,后台会话也让上传完成;但是后台上传需要基于文件的任务,而不依赖于文件的httpBody请求)。

这是一个完全不同的问题,超出了此处的范围,但是希望以上内容可以说明这里的关键问题,即在处理如此大的资产时不要使用NSData / Data。 / p>


请注意,以上代码对OutputStream使用了以下扩展名,包括将字符串写入输出流并将其他文件的内容附加到该流的方法:

extension OutputStream {
    @discardableResult
    func write(_ string: String) -> Int {
        guard let data = string.data(using: .utf8) else { return -1 }
        return data.withUnsafeBytes { (buffer: UnsafePointer<UInt8>) -> Int in
            write(buffer, maxLength: data.count)
        }
    }

    @discardableResult
    func append(contentsOf url: URL) -> Int {
        guard let inputStream = InputStream(url: url) else { return -1 }
        inputStream.open()
        let bufferSize = 1_024 * 1_024
        var buffer = [UInt8](repeating: 0, count: bufferSize)
        var bytes = 0
        var totalBytes = 0
        repeat {
            bytes = inputStream.read(&buffer, maxLength: bufferSize)
            if bytes > 0 {
                write(buffer, maxLength: bytes)
                totalBytes += bytes
            }
        } while bytes > 0

        inputStream.close()

        return bytes < 0 ? bytes : totalBytes
    }
}

答案 1 :(得分:3)

根据Apple文档,您可以使用NSData(contentsOf:options:)“同步读取短文件”,因此它不应该能够处理4 GB的文件。相反,您可以使用InputStream并使用带有文件路径的URL对其进行初始化。

答案 2 :(得分:1)

在捕获区域中有一个错误对象,这就是您的答案。

UPD:我猜是这个错误,正确的原因是Code=12 "Cannot allocate memory"

您可以尝试像-Is calling read:maxLength: once for every NSStreamEventHasBytesAvailable correct?