iOS图库大视频上传

最近工作中遇到一个需求,从系统相册中选择图片和视频,使用HTTP上传到服务器端。在这个过程中也踩了一些坑,在这里和大家分享一下,共同进步。

选择图片和视频

首先是同系统相册选择图片和视频。iOS系统自带有UIImagePickerController,可以选择或拍摄图片视频,但是最大的问题是只支持单选,由于项目要求需要支持多选,只能自己自定义。获取系统图库的框架有两个,一个是ALAssetsLibrary,兼容iOS低版本,但是在iOS9中是不建议使用的;另一个是PHAsset,但最低要求iOS8以上。我们的项目需要兼容到iOS7,所以选择了ALAssetsLibrary。具体的实现可以参考我之前写的仿微信iOS相册选择 MTImagePicker,github地址https://github.com/luowenxing/MTImagePicker ,这里就不再赘述啦。

HTTP上传

接下来就是使用HTTP上传到服务器端。通常来说,文件服务器一般会有两种实现的方式。

  • 一种是纯的二进制文件上传,对应的HTTP Content-Type可以是application/octet-streamapplication开头的MIME-Type,即HTTP报文的Body的内容就是文件的二进制内容,其他的文件名、鉴权等附加信息则放在cookieHTTP Header里。
  • 另一种就是HTML表单传输,对应的HTTP Content-Typemultipart/form-data,HTTP报文的 Body内容除了文件的二进制内容,还多了附加的表单字段信息和分割符等。表单上传文件浏览器有原生的支持,如果iOS端需要使用这种方式就需要按照报文格式去拼装你的HTTP Body,具体的报文格式可以参考iOS里实现multipart/form-data格式上传文件。主流的网络库比如AFNetworking就已经有了这类功能的封装,比较方便。

我们的服务器端这两种方式都支持,所以这里就直接使用二进制上传的方式。在没有第三方的网络库的情况下,使用NSURLConnectionNSURLSession发起网络请求前,我们都需要一个NSURLRequest对象,在这个对象上完成请求初始化。

let request = NSMutableURLRequest(URL: url, cachePolicy: .UseProtocolCachePolicy, timeoutInterval: 10)
request.HTTPMethod = "POST"
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")

设置好相关的HTTP Header之后,设置HTTP Body的内容有两种方式

  • request.HTTPBodyStream = NSInputStream()
  • request.HTTPBody = NSData()
    这两者设置其中任何一个都会使得另一个失效。

大文件处理

通常,对于小文件,我们可以任意选择其中任何一种方式进行设置。对于比较大的文件,处理的原则是,不能把文件直接装入内存中,否则会造成内存不足而使得App崩溃。具体的做法是:

  • 对于沙箱内的文件,推荐使用NSInputStream(fileAtPath: fileUrl)初始化为文件流,不占内存。也可以使用NSData(contentsOfFile: String>, options: NSDataReadingOptions.DataReadingMappedAlways),使用内存映射的方式获取NSData,在StackOverflow上有对这个问题的解释。

Memory-mapped files copy data from disk into memory a page at a time. Unused pages are free to be swapped out, the same as any other virtual memory, unless they have been wired into physical memory using mlock(2). Memory mapping leaves the determination of what to copy from disk to memory and when to the OS.
类似虚拟内存的技术,简单来说就是一次拷贝一页的内存大小(页是内存映射的最小单位),而不是整个拷贝到内存中。

  • 对于系统相册的文件,在此处具体来说就是一个ALAsset对象,我们能够通过ALAssetRepresentationgetBytes方法获取到文件的内容到一段缓冲区,继而生成NSData,但是这个NSData并不是内存映射的,所以文件多大,就会占用多少内存。
let rept =  asset.defaultRepresentation()
let imageBuffer = UnsafeMutablePointer.alloc(Int(rept.size()))
let bufferSize = rept.getBytes(imageBuffer, fromOffset: Int64(0),length: Int(rept.size()), error: nil)
let data =  NSData(bytesNoCopy:imageBuffer ,length:bufferSize, freeWhenDone:true)

此时我们需要把ALAsset转化为NSInputStream,通过CFStreamCreateBoundPair这个类。在苹果的官方文档上有对这个类的使用场景介绍,但是没有官方例子。

For large blocks of constructed data, call CFStreamCreateBoundPair to create a pair of streams, then call the setHTTPBodyStream: method to tell NSMutableURLRequest to use one of those streams as the source for its body content. By writing into the other stream, you can send the data a piece at a time.

其他的参考资料也很少,我找到的对我有帮助的资料之一就是StackOverflow上的这个问题:ios-how-to-upload-a-large-asset-file-into-sever-by-streaming

根据官方文档,以及我收集的资料,具体的做法是使用CFStreamCreateBoundPair创建一对readStream/writeStreamreadStream就作为HTTPBodyStream,设置NSStream的代理,writeStream加入Runloop,监测其NSStreamEventHasSpaceAvailable时,调用getBytes方法获取一段NSData,写入到writeStream中。主要的代码如下。

    func stream(aStream: NSStream, handleEvent eventCode: NSStreamEvent) {
        switch (eventCode) {
        case NSStreamEvent.None:
            break
            
        case NSStreamEvent.OpenCompleted:
            break
            
        case NSStreamEvent.HasBytesAvailable:
            break
            
        case NSStreamEvent.HasSpaceAvailable:
            self.write()
            break
            
        case NSStreamEvent.ErrorOccurred :
            self.finish()
            break
        case NSStreamEvent.EndEncountered:
            // weird error: the output stream is full or closed prematurely, or canceled.
            self.finish()
            break
        default:
            break
        }
    }
    
    func write() {
        let rept =  asset.defaultRepresentation()
        let length = self.assetSize - self.offset > self.bufferSize ? self.bufferSize :  self.assetSize - self.offset
        if length > 0 {
            let writeSize = rept.getBytes(assetBuffer, fromOffset: self.offset ,length: length, error:nil)
            let written = self.writeStream.write(assetBuffer, maxLength: writeSize)
            self.offset += written
        } else {
            self.finish()
        }
    }
    
    func finish() {
        self.writeStream.close()
        self.writeStream.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
        self.strongSelf = nil
    }

完整的代码我上传在了github,ALAssetToNSInputStream,把ALAssetToNSInputStream.swift加入工程即可使用,Demo暂时还没有,有时间会补上。看官们随手给个Star呗 ~

你可能感兴趣的:(iOS图库大视频上传)