我建议不要对视频进行 base64 编码。
资产已经如此之大,以至于:
您想防止 base64 使资产变得更大(因此,上传速度更慢);和
无论如何,您可能希望避免在任何给定时间将整个资产加载到内存中(即避免Data
在构建此上传请求的过程中使用 a )。标准的 base-64 编码Data
方法实际上要求您在内存中拥有整个资产以执行 base-64 编码,并且您还将同时在内存中拥有 base-64 字符串。
例如,对 50 mb 视频使用标准 base-64 编码Data
方法可能会使内存至少达到 116 mb。
multipart/form-data
请求是标准方法(允许嵌入二进制有效负载并发送附加字段)。但是要小心,因为您会发现大多数示例在线构建Data
然后发送,这可能是不谨慎的。将其写入文件,而无需在任何给定时间尝试将整个资产加载到 RAM 中。然后执行基于文件的上传任务,将其发送到您的服务器。
例如,如果您想自己创建这个多部分请求,您可以执行以下操作:
// MARK: - Public interface
extension URLSession {
/// Delegate-based upload task
@discardableResult
func uploadTask(
from url: URL,
headers: [String: String]? = nil,
parameters: [String: String]? = nil,
filePathKey: String,
fileURLs: [URL]
) throws -> URLSessionUploadTask {
let (request, fileURL) = try uploadRequestFile(from: url, headers: headers, parameters: parameters, filePathKey: filePathKey, fileURLs: fileURLs)
return uploadTask(with: request, fromFile: fileURL)
}
/// Completion-handler-based upload task
@discardableResult
func uploadTask(
from url: URL,
headers: [String: String]? = nil,
parameters: [String: String]? = nil,
filePathKey: String,
fileURLs: [URL],
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
) -> URLSessionUploadTask? {
do {
let (request, fileURL) = try uploadRequestFile(
from: url,
headers: headers,
parameters: parameters,
filePathKey: filePathKey,
fileURLs: fileURLs
)
return uploadTask(with: request, fromFile: fileURL, completionHandler: completionHandler)
} catch {
completionHandler(nil, nil, error)
return nil
}
}
/// Async-await-based upload task
@available(iOS 15.0, *)
func upload(
from url: URL,
headers: [String: String]? = nil,
parameters: [String: String]? = nil,
filePathKey: String,
fileURLs: [URL],
delegate: URLSessionTaskDelegate? = nil
) async throws -> (Data, URLResponse) {
let (request, fileURL) = try uploadRequestFile(
from: url,
headers: headers,
parameters: parameters,
filePathKey: filePathKey,
fileURLs: fileURLs
)
return try await upload(for: request, fromFile: fileURL, delegate: delegate)
}
}
// MARK: - Private implementation
private extension URLSession {
private func uploadRequestFile(
from url: URL,
headers: [String: String]? = nil,
parameters: [String: String]? = nil,
filePathKey: String,
fileURLs: [URL]
) throws -> (URLRequest, URL) {
let boundary = "Boundary-" + UUID().uuidString
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
headers?.forEach { (key, value) in
request.addValue(value, forHTTPHeaderField: key)
}
let fileURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
guard let stream = OutputStream(url: fileURL, append: false) else {
throw OutputStreamError.unableToCreateFile
}
stream.open()
try parameters?.forEach { (key, value) in
try stream.write("--\(boundary)\r\n")
try stream.write("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
try stream.write("\(value)\r\n")
}
for fileURL in fileURLs {
let filename = fileURL.lastPathComponent
try stream.write("--\(boundary)\r\n")
try stream.write("Content-Disposition: form-data; name=\"\(filePathKey)\"; filename=\"\(filename)\"\r\n")
try stream.write("Content-Type: \(fileURL.mimeType)\r\n\r\n")
try stream.write(from: fileURL)
try stream.write("\r\n")
}
try stream.write("--\(boundary)--\r\n")
stream.close()
return (request, fileURL)
}
}
和
extension URL {
/// Mime type for the URL
///
/// Requires `import UniformTypeIdentifiers` for iOS 14 solution.
/// Requires `import MobileCoreServices` for pre-iOS 14 solution
var mimeType: String {
if #available(iOS 14.0, *) {
return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? "application/octet-stream"
} else {
guard
let identifier = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(),
let mimeType = UTTypeCopyPreferredTagWithClass(identifier, kUTTagClassMIMEType)?.takeRetainedValue() as String?
else {
return "application/octet-stream"
}
return mimeType
}
}
}
和
enum OutputStreamError: Error {
case stringConversionFailure
case bufferFailure
case writeFailure
case unableToCreateFile
case unableToReadFile
}
extension OutputStream {
/// Write `String` to `OutputStream`
///
/// - parameter string: The `String` to write.
/// - parameter encoding: The `String.Encoding` to use when writing the string. This will default to `.utf8`.
/// - parameter allowLossyConversion: Whether to permit lossy conversion when writing the string. Defaults to `false`.
func write(_ string: String, encoding: String.Encoding = .utf8, allowLossyConversion: Bool = false) throws {
guard let data = string.data(using: encoding, allowLossyConversion: allowLossyConversion) else {
throw OutputStreamError.stringConversionFailure
}
try write(data)
}
/// Write `Data` to `OutputStream`
///
/// - parameter data: The `Data` to write.
func write(_ data: Data) throws {
try data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) throws in
guard var pointer = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
throw OutputStreamError.bufferFailure
}
var bytesRemaining = buffer.count
while bytesRemaining > 0 {
let bytesWritten = write(pointer, maxLength: bytesRemaining)
if bytesWritten < 0 {
throw OutputStreamError.writeFailure
}
bytesRemaining -= bytesWritten
pointer += bytesWritten
}
}
}
/// Write `Data` to `OutputStream`
///
/// - parameter data: The `Data` to write.
func write(from url: URL) throws {
guard let input = InputStream(url: url) else {
throw OutputStreamError.unableToReadFile
}
input.open()
defer { input.close() }
let bufferSize = 65_536
var data = Data(repeating: 0, count: bufferSize)
try data.withUnsafeMutableBytes { (buffer: UnsafeMutableRawBufferPointer) throws in
guard let buffer = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
throw OutputStreamError.bufferFailure
}
while input.hasBytesAvailable {
var remainingCount = input.read(buffer, maxLength: bufferSize)
if remainingCount < 0 { throw OutputStreamError.unableToReadFile }
var pointer = buffer
while remainingCount > 0 {
let countWritten = write(pointer, maxLength: remainingCount)
if countWritten < 0 { throw OutputStreamError.writeFailure }
remainingCount -= countWritten
pointer += countWritten
}
}
}
}
}
然后您可以执行以下操作(在 iOS 15 中):
extension ViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let fileURL = info[.mediaURL] as? URL else {
print("no media URL")
return
}
Task {
do {
let (data, response) = try await URLSession.shared.upload(from: url, filePathKey: "file", fileURLs: [fileURL])
try? FileManager.default.removeItem(at: fileURL)
// check `data` and `response` here
} catch {
print(error)
}
}
dismiss(animated: true)
}
}
或者,在早期的 Swift 版本中:
extension ViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let fileURL = info[.mediaURL] as? URL else {
print("no media URL")
return
}
URLSession.shared.uploadTask(from: url, filePathKey: "file", fileURLs: [fileURL]) { data, _, error in
try? FileManager.default.removeItem(at: fileURL)
guard let data = data, error == nil else {
print(error!)
return
}
// check `data` and `response` here
}?.resume()
dismiss(animated: true)
}
}
在这里,虽然我上传了两个 55mb 的视频,但总分配量从未超过 8mb(其中一些似乎是图像选择器本身缓存的内存)。我重复了两次,以说明每次后续上传的内存都不会继续增长。
(绿色间隔是在图像/视频选择器中花费的时间以及相关的视频压缩时间。红色间隔是实际上传的时间。这样您可以将过程与内存使用情况相关联。)