iOS圖庫大視頻上傳

最近工作中遇到一個需求,從系統(tǒng)相冊中選擇圖片和視頻,使用HTTP上傳到服務器端。在這個過程中也踩了一些坑,在這里和大家分享一下,共同進步。

選擇圖片和視頻

首先是同系統(tǒng)相冊選擇圖片和視頻。iOS系統(tǒng)自帶有UIImagePickerController,可以選擇或拍攝圖片視頻,但是最大的問題是只支持單選,由于項目要求需要支持多選,只能自己自定義。獲取系統(tǒng)圖庫的框架有兩個,一個是ALAssetsLibrary,兼容iOS低版本,但是在iOS9中是不建議使用的;另一個是PHAsset,但最低要求iOS8以上。我們的項目需要兼容到iOS7,所以選擇了ALAssetsLibrary。具體的實現(xiàn)可以參考我之前寫的仿微信iOS相冊選擇 MTImagePicker,github地址https://github.com/luowenxing/MTImagePicker ,這里就不再贅述啦。

HTTP上傳

接下來就是使用HTTP上傳到服務器端。通常來說,文件服務器一般會有兩種實現(xiàn)的方式。

  • 一種是純的二進制文件上傳,對應的HTTP Content-Type可以是application/octet-streamapplication開頭的MIME-Type,即HTTP報文的Body的內(nèi)容就是文件的二進制內(nèi)容,其他的文件名、鑒權等附加信息則放在cookieHTTP Header里。
  • 另一種就是HTML表單傳輸,對應的HTTP Content-Typemultipart/form-data,HTTP報文的 Body內(nèi)容除了文件的二進制內(nèi)容,還多了附加的表單字段信息和分割符等。表單上傳文件瀏覽器有原生的支持,如果iOS端需要使用這種方式就需要按照報文格式去拼裝你的HTTP Body,具體的報文格式可以參考iOS里實現(xiàn)multipart/form-data格式上傳文件。主流的網(wǎng)絡庫比如AFNetworking就已經(jīng)有了這類功能的封裝,比較方便。

我們的服務器端這兩種方式都支持,所以這里就直接使用二進制上傳的方式。在沒有第三方的網(wǎng)絡庫的情況下,使用NSURLConnectionNSURLSession發(fā)起網(wǎng)絡請求前,我們都需要一個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的內(nèi)容有兩種方式

  • request.HTTPBodyStream = NSInputStream()
  • request.HTTPBody = NSData()
    這兩者設置其中任何一個都會使得另一個失效。

大文件處理

通常,對于小文件,我們可以任意選擇其中任何一種方式進行設置。對于比較大的文件,處理的原則是,不能把文件直接裝入內(nèi)存中,否則會造成內(nèi)存不足而使得App崩潰。具體的做法是:

  • 對于沙箱內(nèi)的文件,推薦使用NSInputStream(fileAtPath: fileUrl)初始化為文件流,不占內(nèi)存。也可以使用NSData(contentsOfFile: String>, options: NSDataReadingOptions.DataReadingMappedAlways),使用內(nèi)存映射的方式獲取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.
類似虛擬內(nèi)存的技術,簡單來說就是一次拷貝一頁的內(nèi)存大小(頁是內(nèi)存映射的最小單位),而不是整個拷貝到內(nèi)存中。

  • 對于系統(tǒng)相冊的文件,在此處具體來說就是一個ALAsset對象,我們能夠通過ALAssetRepresentationgetBytes方法獲取到文件的內(nèi)容到一段緩沖區(qū),繼而生成NSData,但是這個NSData并不是內(nèi)存映射的,所以文件多大,就會占用多少內(nèi)存。
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

根據(jù)官方文檔,以及我收集的資料,具體的做法是使用CFStreamCreateBoundPair創(chuàng)建一對readStream/writeStreamreadStream就作為HTTPBodyStream,設置NSStream的代理,writeStream加入Runloop,監(jiān)測其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暫時還沒有,有時間會補上??垂賯冸S手給個Star唄 ~

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,628評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,160評論 25 708
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,628評論 30 472
  • 平平出生在一個小鎮(zhèn)里。 或許是因為爸爸媽媽不在身邊的緣故,經(jīng)常被別人看不起…… (1) 平平和爺爺奶奶一起...
    赫赫h閱讀 217評論 0 0
  • 最近一直在練習演講,看完這本書,我覺得這是練習演講必須要看的一本書。先和大家分享看到的這幾個方法,剩下的可以自己去...
    王子木閱讀 1,129評論 0 0

友情鏈接更多精彩內(nèi)容