iOS图库大视频上传

作者: 文兴 | 来源:发表于2016-05-31 18:06 被阅读4201次

最近工作中遇到一个需求,从系统相册中选择图片和视频,使用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<UInt8>.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呗 ~

相关文章

网友评论

  • 随便打几个字:ALAsset在ios10之后弃用了。使用Photos框架的童鞋,可以使用PHAssetResourceManager的方法-requestDataForAssetResource:options:dataReceivedHandler:completionHandler:来读取PHAsset的数据。
  • a2c8cfe90804:作者你好,我想请教一下,使用NSData(contentsOfFile: String>, options: NSDataReadingOptions.DataReadingMappedAlways)是可以不让内存暴增,但是这个貌似是有大小限制的,太大的文件比如2G的文件就不行了,使用NSInputSteam的话可以从特定位置截取一段数据上传吗?我这边要做断点上传,谢谢了。
    zhenwenl: _stream = [[NSInputStream alloc]initWithFileAtPath:self.srcFilePath];
    [_stream setProperty:[NSNumber numberWithUnsignedLongLong:self.srcFileFinishedSize] forKey:NSStreamFileCurrentOffsetKey];

    可以从某个offset开始读取流,实现断点上传。
    文兴:nsdata有一个getBytes方法,可以实现截取制定长度data
  • manajay:ALAssetRepresentationiOS9 已经过期了,iOS怎么办? 怎么在不写入沙盒的前提下,获取视频的 一部分二进制数据
    manajay:Phasset的并没有发现 这种映射内存的API? 你说的public static var alwaysMapped: NSData.ReadingOptions { get }这个是 沙盒的API ,相册的路径不适用
    manajay:@文兴 我找不到那种直接获取 内存映射的API啊, 我本身只想获取 视频的 部分nsdata,大约一兆
    文兴:phasset不提供这样的api喔,不过它可以获取到视频的内存映射形式的nsdata,不会出现占用内存过大的问题
  • 巴糖:mark
  • 54ca515c1a27:之前也有实现过,文章写的不错
    文兴:@挡不住de诸葛小亮 这方面资料比较少,有一些还是自己摸索的,分享给大家,共同进步:smile:

本文标题:iOS图库大视频上传

本文链接:https://www.haomeiwen.com/subject/evmfdttx.html