如何在后台线程(Swift)上有效地将大文件写入磁盘

Tom*_* C. 34 multithreading large-files large-data ios swift

更新

我已经解决并删除了令人分心的错误.请阅读整篇文章,如果仍有问题,请随时留言.

背景

我试图使用Swift 2.0,GCD和完成处理程序在iOS上将相对较大的文件(视频)写入磁盘.我想知道是否有更有效的方法来执行此任务.在使用完成逻辑的同时,需要在不阻塞主UI的情况下完成任务,并确保尽可能快地执行操作.我有自定义对象与NSData属性,所以我目前正在尝试使用NSData上的扩展.作为示例,替代解决方案可能包括使用NSFilehandle或NSStreams以及某种形式的线程安全行为,这导致比基于当前解决方案的NSData writeToURL函数快得多的吞吐量.

NSData有什么问题吗?

请注意以下从NSData类参考(保存数据)中进行的讨论.我确实对我的临时目录执行写操作,但是我遇到问题的主要原因是我在处理大文件时可以看到UI明显滞后.这种滞后恰恰是因为NSData不是异步的(Apple Docs注意到原子写入会导致"大"文件的性能问题〜> 1mb).因此,在处理大型文件时,无论NSData方法中的内部机制是什么,都可以使用.

我做了一些挖掘,并从Apple发现了这个信息..."这个方法非常适合将数据:// URL转换为NSData对象,也可以用于同步读取短文件.如果你需要读取可能很大的文件,使用inputStreamWithURL:打开一个流,然后一次读取一个文件." (NSData类参考,Objective-C,+ dataWithContentsOfURL).这个信息似乎暗示我可以尝试使用流将文件写在后台线程上,如果将writeToURL移动到后台线程(由@jtbandes建议)是不够的.

NSData类及其子类提供了快速轻松地将其内容保存到磁盘的方法.为了最大限度地降低数据丢失的风险,这些方法提供了以原子方式保存数据的选项.原子写保证数据可以完整保存,也可以完全失败.原子写入从将数据写入临时文件开始.如果此写入成功,则该方法将临时文件移动到其最终位置.

虽然原子写操作可以最大限度地降低因文件损坏或部分写入而导致数据丢失的风险,但在写入临时目录,用户主目录或其他可公开访问的目录时,它们可能不合适.每次使用可公开访问的文件时,都应将该文件视为不受信任且可能存在危险的资源.攻击者可能会破坏或破坏这些文件.攻击者还可以使用硬链接或符号链接替换文件,从而导致写入操作覆盖或损坏其他系统资源.

在可公开访问的目录中工作时,避免使用writeToURL:atomically:方法(以及相关方法).而是使用现有文件描述符初始化NSFileHandle对象,并使用NSFileHandle方法安全地写入文件.

其他替代品

一个物品在objc.io上并发编程提供了有趣的选择"高级:文件I/O在后台".一些选项也涉及使用InputStream.Apple还有一些旧的参考文件,用于异步读取和写入文件.我发布这个问题是为了期待Swift的替代方案.

适当答案的示例

以下是可能满足此类问题的适当答案的示例.(用于流编程指南,写入输出流)

使用NSOutputStream实例写入输出流需要几个步骤:

  1. 使用写入数据的存储库创建并初始化NSOutputStream的实例.还设置了一个代表.
  2. 在运行循环上计划流对象并打开流.
  3. 处理流对象报告给其委托的事件.
  4. 如果流对象已将数据写入内存,请通过请求NSStreamDataWrittenToMemoryStreamKey属性来获取数据.
  5. 当没有更多数据要写入时,丢弃流对象.

我正在寻找最熟练的算法,适用于使用Swift,API或甚至C/ObjC将极大文件写入iOS就足够了.我可以将算法转换为适当的Swift兼容结构.

Nota Bene

我理解下面的信息错误.它包含完整性.这个问题是询问是否有更好的算法用于将大文件写入具有保证依赖序列的磁盘(例如NSOperation依赖).如果有请提供足够的信息(描述/样本为我重建相关的Swift 2.0兼容代码).如果我遗漏任何有助于回答问题的信息,请告知我们.

关于扩展名的说明

我已经在基本writeToURL中添加了一个完成处理程序,以确保不会发生意外的资源共享.我使用该文件的依赖任务永远不会面临竞争条件.

extension NSData {

    func writeToURL(named:String, completion: (result: Bool, url:NSURL?) -> Void)  {

       let filePath = NSTemporaryDirectory() + named
       //var success:Bool = false
       let tmpURL = NSURL( fileURLWithPath:  filePath )
       weak var weakSelf = self


      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
                //write to URL atomically
                if weakSelf!.writeToURL(tmpURL, atomically: true) {

                        if NSFileManager.defaultManager().fileExistsAtPath( filePath ) {
                            completion(result: true, url:tmpURL)                        
                        } else {
                            completion (result: false, url:tmpURL)
                        }
                    }
            })

        }
    }
Run Code Online (Sandbox Code Playgroud)

此方法用于使用以下方法处理来自控制器的自定义对象数据:

var items = [AnyObject]()
if let video = myCustomClass.data {

    //video is of type NSData        
    video.writeToURL("shared.mp4", completion: { (result, url) -> Void in
        if result {
            items.append(url!)
            if items.count > 0 {

                let sharedActivityView = UIActivityViewController(activityItems: items, applicationActivities: nil)

                self.presentViewController(sharedActivityView, animated: true) { () -> Void in
                //finished
    }
}
        }
     })
}
Run Code Online (Sandbox Code Playgroud)

结论

Apple Docs on Core Data Performance为处理内存压力和管理BLOB提供了一些很好的建议.这真是一篇文章,其中包含很多行为线索以及如何缓解应用程序中大文件的问题.现在虽然它特定于Core Data而不是文件,但是对原子写入的警告确实告诉我,我应该实现非常谨慎地原子写入的方法.

对于大型文件,管理写入的唯一安全方法似乎是在完成处理程序(写入方法)中添加并在主线程上显示活动视图.无论是通过流还是通过修改现有API来添加完成逻辑,都取决于读者.我在过去做过这两件事,并且正在测试最佳性能.

在此之前,我正在更改解决方案以从Core Data中删除所有二进制数据属性,并用字符串替换它们以在磁盘上保存资产URL.我还利用Assets Library和PHAsset的内置功能来获取和存储所有相关的资产URL.当我需要复制任何资产时,我将使用标准API方法(PHAsset /资源库上的导出方法)和完成处理程序来通知用户主线程上的已完成状态.

(Core Data Performance文章中真正有用的片段)

减少内存开销

有时您希望临时使用托管对象,例如计算特定属性的平均值.这会导致对象图和内存消耗增长.您可以通过重新断开不再需要的单个托管对象来减少内存开销,也可以重置托管对象上下文以清除整个对象图.您也可以使用适用于Cocoa编程的模式.

您可以使用NSManagedObjectContext的refreshObject:mergeChanges:方法重新验证单个托管对象.这具有清除其内存中属性值从而减少其内存开销的效果.(请注意,这与将属性值设置为nil不同 - 如果故障被触发,将按需检索值 - 请参阅Faulting和Uniquing.)

创建提取请求时,可以将includesPropertyValues设置为NO>,以避免创建表示属性值的对象,从而减少内存开销.但是,如果您确定要么不需要实际的属性数据,要么已经在行缓存中已有信息,则通常应该这样做,否则将导致多次访问持久性存储.

您可以使用NSManagedObjectContext的reset方法删除与上下文关联的所有托管对象,并"重新开始",就像您刚刚创建它一样.请注意,与该上下文关联的任何托管对象都将失效,因此您需要放弃对您仍然感兴趣的上下文关联的任何引用并重新获取.如果迭代很多对象,则可能需要使用本地自动释放池块来确保尽快释放临时对象.

如果您不打算使用Core Data的撤消功能,则可以通过将上下文的撤消管理器设置为nil来减少应用程序的资源需求.这对于后台工作线程以及大型导入或批处理操作尤其有用.

最后,Core Data默认情况下不会保留对托管对象的强引用(除非它们有未保存的更改).如果内存中有很多对象,则应确定拥有引用.托管对象通过关系保持对彼此的强引用,这可以轻松地创建强大的引用周期.您可以通过重新断层对象来中断循环(再次使用modMan:mergeChanges:NSManagedObjectContext方法).

大数据对象(BLOB)

如果您的应用程序使用大型BLOB("二进制大对象",如图像和声音数据),则需要注意尽量减少开销."小","适度"和"大"的确切定义是流动的,取决于应用程序的用法.一个宽松的经验法则是,大小为千字节的对象是"适度"大小的,大小为兆字节的对象是"大"大小的.一些开发人员在数据库中使用10MB BLOB取得了良好的性能.另一方面,如果一个应用程序在一个表中有数百万行,那么即使128个字节也可能是一个"适度"大小的CLOB(字符大对象),需要将其标准化为一个单独的表.

通常,如果需要将BLOB存储在持久性存储中,则应使用SQLite存储.XML和二进制存储要求整个对象图驻留在内存中,并且存储写入是原子的(请参阅持久存储功能),这意味着它们不能有效地处理大型数据对象.SQLite可以扩展以处理极大的数据库.正确使用,SQLite为高达100GB的数据库提供了良好的性能,单行可以容纳1GB(尽管当然,无论存储库的效率如何,将1GB的数据读入内存都是一项昂贵的操作).

BLOB通常表示实体的属性 - 例如,照片可能是Employee实体的属性.对于小到中等大小的BLOB(和CLOB),您应该为数据创建一个单独的实体,并创建一个一对一的关系来代替该属性.例如,您可以创建Employee和Photograph实体,它们之间具有一对一的关系,其中Employee与Photograph之间的关系取代了Employee的photograph属性.这种模式最大化了对象错误的好处(参见Faulting和Uniquing).只有在实际需要时才会检索任何给定的照片(如果遍历关系).

但是,如果您能够将BLOB作为资源存储在文件系统上,并保持与这些资源的链接(例如URL或路径),那就更好了.然后,您可以在必要时加载BLOB.

注意:

我已将下面的逻辑移到完成处理程序中(参见上面的代码),我不再看到任何错误.如前所述,这个问题是关于是否有更高效的方法来使用Swift处理iOS中的大文件.

尝试处理生成的items数组以传递给UIActvityViewController时,使用以下逻辑:

if items.count> 0 {
let sharedActivityView = UIActivityViewController(activityItems:items,applicationActivities:nil)self.presentViewController(sharedActivityView,animated:true){() - > Void in // finished}}

我看到以下错误:通信错误:{count = 1,contents ="XPCErrorDescription"=> {length = 22,contents ="Connection interrupted"}}>(请注意,我正在寻找更好的设计,而不是回答此错误消息)

Abh*_*ert 20

性能取决于数据是否适合RAM.如果是,那么您应该使用打开NSData writeToURLatomically功能,这就是您正在做的事情.

Apple在"写入公共目录"时对此存在危险的说明在iOS上完全无关,因为没有公共目录.该部分仅适用于OS X.坦率地说,它也不是很重要.

因此,只要视频适合RAM(大约100MB是安全限制),您编写的代码就尽可能高效.

对于不适合RAM的文件,您需要使用流,否则您的应用会在将视频保留在内存中时崩溃.要从服务器下载大型视频并将其写入磁盘,您应该使用NSURLSessionDownloadTask.

通常,流式传输(包括NSURLSessionDownloadTask)将比数量级慢几个数量级NSData.writeToURL().所以除非你需要,否则不要使用流.所有操作NSData非常快,它完全能够处理大小为TB的文件,并且在OS X上具有出色的性能(iOS显然不能拥有那么大的文件,但它具有相同性能的同一类).


您的代码中存在一些问题.

这是错的:

let filePath = NSTemporaryDirectory() + named
Run Code Online (Sandbox Code Playgroud)

而是始终做:

let filePath = NSTemporaryDirectory().stringByAppendingPathComponent(named)
Run Code Online (Sandbox Code Playgroud)

但这也不理想,你应该避免使用路径(它们有缺陷且速度慢).而是使用这样的URL:

let tmpDir = NSURL(fileURLWithPath: NSTemporaryDirectory()) as NSURL!
let fileURL = tmpDir.URLByAppendingPathComponent(named)
Run Code Online (Sandbox Code Playgroud)

此外,您正在使用路径检查文件是否存在...不要这样做:

if NSFileManager.defaultManager().fileExistsAtPath( filePath ) {
Run Code Online (Sandbox Code Playgroud)

而是使用NSURL检查它是否存在:

if fileURL.checkResourceIsReachableAndReturnError(nil) {
Run Code Online (Sandbox Code Playgroud)

  • 在RAM限制方面,仅使用您的测试作为指导.可用的实际RAM量取决于可用硬件和硬件上运行的其他应用程序的状态.因此,如果您的视频"大约"100MB或更多,那么请不要将它们保存在RAM中 - 而是将其保存在流中.但是,如果他们在那之下,那么RAM和NSData(使用原子写入和后台线程与dispatch_async())是最有效的选择.您还可以考虑使用NSOperationQueue,并将最大并发操作设置为1,因为通常最好不要同时对两个文件进行操作. (2认同)

Tom*_* C. 6

最新解决方案(2018年)

另一个有用的可能性可能包括在填充缓冲区时(或者如果您使用了定时记录长度)使用闭包来附加数据并宣布数据流的结束.结合一些Photo API,这可以带来良好的结果.因此,在处理过程中可能会触发下面的一些声明性代码:

var dataSpoolingFinished: ((URL?, Error?) -> Void)?
var dataSpooling: ((Data?, Error?) -> Void)?
Run Code Online (Sandbox Code Playgroud)

在管理对象中处理这些闭包可能允许您简洁地处理任何大小的数据,同时保持内存受控.

将这一想法与使用递归方法相结合,将一些工作聚合到一个dispatch_group中,可能会有一些令人兴奋的可能性.

Apple文档指出:

DispatchGroup允许工作的聚合同步.您可以使用它们提交多个不同的工作项并跟踪它们何时完成,即使它们可能在不同的队列上运行.在完成所有指定任务之前无法进行此操作时,此行为很有用.

其他值得注意的解决方案(〜2016)

我毫不怀疑我会更多地改进这个,但这个主题足够复杂,需要单独的自我回答.我决定从其他答案中获取一些建议并利用NSStream子类.此解决方案基于在SampleCodeBank博客上发布的Obj-C 示例(NSInputStream inputStreamWithURL示例ios,2013年5月12日).

Apple文档指出,对于NSStream子类,您不必一次将所有数据加载到内存中.这是能够管理任何大小的多媒体文件(不超过可用磁盘或RAM空间)的关键.

NSStream是表示流的对象的抽象类.它的接口对所有Cocoa流类都是通用的,包括它的具体子类NSInputStream和NSOutputStream.

NSStream对象提供了一种以与设备无关的方式从各种媒体读取和写入数据的简便方法.您可以为位于内存,文件或网络(使用套接字)中的数据创建流对象,并且可以使用流对象而无需一次将所有数据加载到内存中.

文件系统编程指南

苹果公司的处理整个文件线性使用流在FSPG文章还规定,概念NSInputStreamNSOutputStream应该固有线程安全的.

文件处理,与流

进一步改进

此对象不使用流委派方法.其他改进的空间也很大,但这是我将采取的基本方法.iPhone的主要重点是启用大文件管理,同时通过缓冲区限制内存(TBD - 利用outputStream内存缓冲区).要明确的是,Apple确实提到了他们的便利功能,writeToURL仅用于较小的文件大小(但让我想知道为什么他们不处理较大的文件 - 这些不是边缘情况,注意 - 将文件提问为bug ).

结论

我将不得不进一步测试在后台线程上进行集成,因为我不想干扰任何NSStream内部排队.我有一些其他对象使用类似的想法通过网络管理非常大的数据文件.最好的方法是在iOS中保持尽可能小的文件大小,以节省内存并防止应用程序崩溃.API是在考虑到这些约束的情况下构建的(这就是为什么尝试无限视频不是一个好主意),所以我将不得不整体调整期望.

(Gist Source,检查要点是否有最新变化)

import Foundation
import Darwin.Mach.mach_time

class MNGStreamReaderWriter:NSObject {

    var copyOutput:NSOutputStream?
    var fileInput:NSInputStream?
    var outputStream:NSOutputStream? = NSOutputStream(toMemory: ())
    var urlInput:NSURL?

    convenience init(srcURL:NSURL, targetURL:NSURL) {
        self.init()
        self.fileInput  = NSInputStream(URL: srcURL)
        self.copyOutput = NSOutputStream(URL: targetURL, append: false)
        self.urlInput   = srcURL

    }

    func copyFileURLToURL(destURL:NSURL, withProgressBlock block: (fileSize:Double,percent:Double,estimatedTimeRemaining:Double) -> ()){

        guard let copyOutput = self.copyOutput, let fileInput = self.fileInput, let urlInput = self.urlInput else { return }

        let fileSize            = sizeOfInputFile(urlInput)
        let bufferSize          = 4096
        let buffer              = UnsafeMutablePointer<UInt8>.alloc(bufferSize)
        var bytesToWrite        = 0
        var bytesWritten        = 0
        var counter             = 0
        var copySize            = 0

        fileInput.open()
        copyOutput.open()

        //start time
        let time0 = mach_absolute_time()

        while fileInput.hasBytesAvailable {

            repeat {

                bytesToWrite    = fileInput.read(buffer, maxLength: bufferSize)
                bytesWritten    = copyOutput.write(buffer, maxLength: bufferSize)

                //check for errors
                if bytesToWrite < 0 {
                    print(fileInput.streamStatus.rawValue)
                }
                if bytesWritten == -1 {
                    print(copyOutput.streamStatus.rawValue)
                }
                //move read pointer to next section
                bytesToWrite -= bytesWritten
                copySize += bytesWritten

            if bytesToWrite > 0 {
                //move block of memory
                memmove(buffer, buffer + bytesWritten, bytesToWrite)
                }

            } while bytesToWrite > 0

            if fileSize != nil && (++counter % 10 == 0) {
                //passback a progress tuple
                let percent     = Double(copySize/fileSize!)
                let time1       = mach_absolute_time()
                let elapsed     = Double (time1 - time0)/Double(NSEC_PER_SEC)
                let estTimeLeft = ((1 - percent) / percent) * elapsed

                block(fileSize: Double(copySize), percent: percent, estimatedTimeRemaining: estTimeLeft)
            }
        }

        //send final progress tuple
        block(fileSize: Double(copySize), percent: 1, estimatedTimeRemaining: 0)


        //close streams
        if fileInput.streamStatus == .AtEnd {
            fileInput.close()

        }
        if copyOutput.streamStatus != .Writing && copyOutput.streamStatus != .Error {
            copyOutput.close()
        }



    }

    func sizeOfInputFile(src:NSURL) -> Int? {

        do {
            let fileSize = try NSFileManager.defaultManager().attributesOfItemAtPath(src.path!)
            return fileSize["fileSize"]  as? Int

        } catch let inputFileError as NSError {
            print(inputFileError.localizedDescription,inputFileError.localizedRecoverySuggestion)
        }

        return nil
    }


}
Run Code Online (Sandbox Code Playgroud)

代表团

这是一个类似的对象,我在后台的高级文件I/O文章中重写,Eidhof,C.,ObjC.io).只需进行一些调整,就可以模拟上面的行为.简单地将数据重定向到NSOutputStreamprocessDataChunk方法.

(要点来源 - 检查要点是否有最新变化)

import Foundation

class MNGStreamReader: NSObject, NSStreamDelegate {

    var callback: ((lineNumber: UInt , stringValue: String) -> ())?
    var completion: ((Int) -> Void)?
    var fileURL:NSURL?
    var inputData:NSData?
    var inputStream: NSInputStream?
    var lineNumber:UInt = 0
    var queue:NSOperationQueue?
    var remainder:NSMutableData?
    var delimiter:NSData?
    //var reader:NSInputStreamReader?

    func enumerateLinesWithBlock(block: (UInt, String)->() , completionHandler completion:(numberOfLines:Int) -> Void ) {

        if self.queue == nil {
            self.queue = NSOperationQueue()
            self.queue!.maxConcurrentOperationCount = 1
        }

        assert(self.queue!.maxConcurrentOperationCount == 1, "Queue can't be concurrent.")
        assert(self.inputStream == nil, "Cannot process multiple input streams in parallel")

        self.callback = block
        self.completion = completion

        if self.fileURL != nil {
            self.inputStream = NSInputStream(URL: self.fileURL!)
        } else if self.inputData != nil {
            self.inputStream = NSInputStream(data: self.inputData!)
        }

        self.inputStream!.delegate = self
        self.inputStream!.scheduleInRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
        self.inputStream!.open()
    }

    convenience init? (withData inbound:NSData) {
        self.init()
        self.inputData = inbound
        self.delimiter = "\n".dataUsingEncoding(NSUTF8StringEncoding)

    }

    convenience init? (withFileAtURL fileURL: NSURL) {
        guard !fileURL.fileURL else { return nil }

        self.init()
        self.fileURL = fileURL
        self.delimiter = "\n".dataUsingEncoding(NSUTF8StringEncoding)
    }

    @objc func stream(aStream: NSStream, handleEvent eventCode: NSStreamEvent){

        switch eventCode {
        case NSStreamEvent.OpenCompleted:
            fallthrough
        case NSStreamEvent.EndEncountered:
            self.emitLineWithData(self.remainder!)
            self.remainder = nil
            self.inputStream!.close()
            self.inputStream = nil

            self.queue!.addOperationWithBlock({ () -> Void in
                self.completion!(Int(self.lineNumber) + 1)
            })

            break
        case NSStreamEvent.ErrorOccurred:
            NSLog("error")
            break
        case NSStreamEvent.HasSpaceAvailable:
            NSLog("HasSpaceAvailable")
            break
        case NSStreamEvent.HasBytesAvailable:
            NSLog("HasBytesAvaible")

            if let buffer = NSMutableData(capacity: 4096) {
                let length = self.inputStream!.read(UnsafeMutablePointer<UInt8>(buffer.mutableBytes), maxLength: buffer.length)
                if 0 < length {
                    buffer.length = length
                    self.queue!.addOperationWithBlock({ [weak self]  () -> Void in
                        self!.processDataChunk(buffer)
                        })
                }
            }
            break
        default:
            break
        }
    }

    func processDataChunk(buffer: NSMutableData) {
        if self.remainder != nil {

            self.remainder!.appendData(buffer)

        } else {

            self.remainder = buffer
        }

        self.remainder!.mng_enumerateComponentsSeparatedBy(self.delimiter!, block: {( component: NSData, last: Bool) in

            if !last {
                self.emitLineWithData(component)
            }
            else {
                if 0 < component.length {
                    self.remainder = (component.mutableCopy() as! NSMutableData)
                }
                else {
                    self.remainder = nil
                }
            }
        })
    }

    func emitLineWithData(data: NSData) {
        let lineNumber = self.lineNumber
        self.lineNumber = lineNumber + 1
        if 0 < data.length {
            if let line = NSString(data: data, encoding: NSUTF8StringEncoding) {
                callback!(lineNumber: lineNumber, stringValue: line as String)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)