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

更新

我已经解决并删除了分心的错误。 如果还有任何问题,请阅读整篇文章,随时留下意见。

背景

我试图在iOS上使用Swift 2.0,GCD和完成处理程序将相对较大的文件(video)写入磁盘。 我想知道是否有更有效的方法来执行这项任务。 该任务需要在不阻塞主UI的情况下完成,同时使用完成逻辑,并确保操作尽可能快。 我有一个NSData属性的自定义对象,所以我目前正在NSData上使用扩展实验。 作为一个例子,备用解决scheme可能包括使用NSFilehandle或NSStreams以及某种forms的线程安全行为,从而比基于当前解决scheme的NSData writeToURL函数产生更快的吞吐量。

无论如何,NSData有什么问题?

请注意以下从NSData类参考( 保存数据 )的讨论。 我执行写入我的临时目录,但是我有一个问题的主要原因是,我可以看到在处理大型文件时在UI中明显的滞后。 这种滞后正是因为NSData不是asynchronous的(Apple Docs注意到primefaces写入会导致“大”文件的性能问题〜> 1mb)。 所以当处理大文件时,任何内部机制在NSData方法中都是有效的。

我做了一些更多的挖掘,并从苹果发现这个信息…“这种方法是理想的转换数据:/ / URL到NSData对象,也可用于同步阅读短文件如果您需要阅读可能的大文件 ,使用inputStreamWithURL:打开一个stream,然后一次读取一个文件。“ ( NSData类参考,Objective-C,+ dataWithContentsOfURL )。 这个信息似乎意味着如果将writeToURL移动到后台线程(如@jtbandes所build议的)是不够的,我可以尝试使用stream将文件写入后台线程。

NSData类及其子类提供了快速方便地将其内容保存到磁盘的方法。 为了尽量减less数据丢失的风险,这些方法提供了primefaces保存数据的选项。 primefaces写保证数据要么全部保存,要么完全失败。 primefaces写入通过将数据写入临时文件开始。 如果写入成功,则该方法将临时文件移动到其最终位置。

尽pipeprimefaces写入操作可以最大限度降低由于损坏或部分写入的文件而导致数据丢失的风险,但在写入临时目录,用户的主目录或其他可公开访问的目录时,它们可能并不合适。 任何时候您使用可公开访问的文件时,都应该将该文件视为不可信且潜在危险的资源。 攻击者可能会妥协或损坏这些文件。 攻击者也可以使用硬链接或符号链接replace文件,导致写操作覆盖或损坏其他系统资源。

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

其他select

关于并发编程objc.io上的一篇文章提供了有关“高级:背景下的文件I / O”的有趣选项。 一些选项也涉及到使用InputStream。 苹果公司也有一些旧的引用来读取和写入文件asynchronous 。 我正在发布这个问题,预计Swift的替代品。

一个合适的答案的例子

下面是可能满足这类问题的适当答案的一个例子。 (采取stream编程指南, 写入输出stream )

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

  1. 用写入数据的存储库创build并初始化一个NSOutputStream的实例。 还设置了一个委托。
  2. 在运行循环中调度stream对象并打开stream。
  3. 处理stream对象报告给它的委托的事件。
  4. 如果stream对象已将数据写入内存,则通过请求NSStreamDataWrittenToMemoryStreamKey属性获取数据。
  5. 当没有更多的数据要写入时,处理stream对象。

我正在寻找适用于使用Swift,APIs写入超大文件到iOS的最熟练的algorithm,或者甚至可能使用C / ObjC就足够了。 我可以将algorithm转换成适当的Swift兼容结构。

Nota Bene

我了解下面的信息错误。 这是包括完整性。 这个问题是问是否有一个更好的algorithm来使用保证的依赖关系序列(例如NSOperation依赖关系)将大文件写入磁盘。 如果有请提供足够的信息(描述/示例为我重build相关的Swift 2.0兼容的代码)。 请告知我是否缺less有助于回答问题的信息。

请注意扩展名

我已经为基本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) } } }) } } 

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

 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 } } } }) } 

结论

关于核心数据性能的Apple Docs在处理内存压力和pipe理BLOB方面提供了一些很好的build议。 这真的是一个文章,有很多线索的行为,以及如何缓和您的应用程序中的大文件的问题。 现在,虽然它是特定于核心数据而不是文件,但primefaces写作的警告确实告诉我,我应该实现方法,小心翼翼地写入primefaces。

对于大文件,pipe理写入的唯一安全方式似乎是添加到完成处理程序(写入方法)中,并在主线程上显示活动视图。 无论是通过stream还是通过修改现有API来添加完成逻辑,都取决于读者。 我已经完成了这两项工作,并正在进行最佳性能testing。

在此之前,我正在改变解决scheme,从Core Data中删除所有的二进制数据属性,并用stringreplace它们以在磁盘上保存资产URL。 我还利用Assets Library和PHAsset的内置function获取和存储所有相关的资产URL。 何时或如果我需要复制任何资产,我将使用标准API方法(在PHAsset /素材库中导出方法)和完成处理程序在主线程上通知用户完成状态。

(真正有用的核心数据性能文章片段)

减less内存开销

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

您可以使用NSManagedObjectContext的refreshObject:mergeChanges:方法来重新错误个别pipe理对象。 这具有清除其内存中属性值的效果,从而减less其内存开销。 (请注意,这与将属性值设置为零不相同 – 如果错误被触发,则会根据需要检索值 – 请参阅错误和取消分配。)

在创build提取请求时,可以将includesPropertyValues设置为NO>,以避免创build表示属性值的对象,从而减less内存开销。 但是,如果您确定不需要实际的属性数据,或者您已经在行caching中获得了信息,则通常只应该这样做,否则您将需要多次访问持久性存储。

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

如果您不打算使用Core Data的撤消function,则可以通过将上下文的撤消pipe理器设置为零来减less应用程序的资源需求。 这对于后台工作者线程以及大量import或批量操作可能特别有利。

最后,核心数据不会默认保持对托pipe对象的强引用(除非它们有未保存的更改)。 如果你在内存中有很多的对象,你应该确定拥有的引用。 被pipe理的对象通过关系保持彼此的强烈的引用,这可以很容易地创build强大的引用循环。 您可以通过重新创build对象来打破循环(同样使用NSManagedObjectContext的refreshObject:mergeChanges:方法)。

大数据对象(BLOB)

如果您的应用程序使用大型BLOB(“二进制大型对象”,如图像和声音数据),则需要注意将开销降到最低。 “小”,“适度”和“大”的确切定义是stream畅的,取决于应用程序的用法。 一个宽松的经验法则是,大小为千字节的对象大小适中,而大小为兆字节大小的对象大小适中。 一些开发人员在数据库中使用10MB的BLOBs取得了良好的性能。 另一方面,如果一个应用程序在表中有数百万行,甚至128个字节可能是一个“适度”大小的CLOB(字符大的对象),需要被归一化到一个单独的表中。

通常,如果您需要将BLOB存储在持久性存储中,则应该使用SQLite存储。 XML和二进制存储需要整个对象图驻留在内存中,而存储写入是primefaces的(参见Persistent Store Features),这意味着它们不能有效处理大数据对象。 SQLite可以扩展来处理超大型的数据库。 正确使用,SQLite为高达100GB的数据库提供了良好的性能,单行最多可以容纳1GB(尽pipe读取1GB数据到内存中是一个昂贵的操作,无论存储库有多高效)。

BLOB通常表示实体的属性 – 例如,照片可能是Employee实体的属性。 对于小到中等大小的BLOB(和CLOB),应该为数据创build一个单独的实体,并创build一对一的关系来代替该属性。 例如,您可以使用它们之间的一对一关系创build员工和照片实体,从员工到照片的关系将replace员工的照片属性。 这种模式最大限度地提高了对象断层的好处(参见断层和非断层)。 任何给定的照片只有在实际需要的情况下才能被检索(如果关系被遍历)。

但是,如果您能够将BLOBs作为资源存储在文件系统中,并维护这些资源的链接(如URL或path),则更好。 然后,您可以根据需要加载BLOB。

注意:

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

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

如果items.count> 0 {
let sharedActivityView = UIActivityViewController(activityItems:items,applicationActivities:nil)self.presentViewController(sharedActivityView,animated:true){() – > void in // finished}}

我看到以下错误:通信错误:{计数= 1,内容=“XPCErrorDescription”=> {长度= 22,内容=“连接中断”}}>(请注意,我正在寻找一个更好的devise,而不是回答这个错误信息)

性能取决于数据是否适合RAM。 如果是这样,那么你应该使用NSData writeToURLatomicallyfunction打开,这就是你在做什么。

当“写入公共目录”时,苹果关于这个问题的注意事项是非常危险的,因为没有公共目录。 这部分只适用于OS X.坦率地说,这也不是那么重要。

所以,只要video适合RAM(大约100MB是安全的限制),你写的代码就尽可能高效。

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

一般来说,stream(包括NSURLSessionDownloadTask )将比NSData.writeToURL()NSData.writeToURL()数量级。 所以不要使用stream,除非你需要。 在NSData上的所有操作都非常快,它完全能够处理多个TB的文件,并且在OS X上具有出色的性能(iOS显然不能具有较大的文件,但是具有相同性能的类)。


你的代码中有几个问题。

这是错误的:

 let filePath = NSTemporaryDirectory() + named 

反而总是这样做:

 let filePath = NSTemporaryDirectory().stringByAppendingPathComponent(named) 

但是,这也不是很理想,你应该避免使用path(他们是越野车和慢)。 而是使用这样的URL:

 let tmpDir = NSURL(fileURLWithPath: NSTemporaryDirectory()) as NSURL! let fileURL = tmpDir.URLByAppendingPathComponent(named) 

此外,你正在使用一个path来检查文件是否存在…不要这样做:

 if NSFileManager.defaultManager().fileExistsAtPath( filePath ) { 

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

 if fileURL.checkResourceIsReachableAndReturnError(nil) { 

当前解决scheme

我毫不怀疑,我会进一步细化这个话题,但这个话题足够复杂,需要单独的自我回答。 我决定采取其他答案的一些build议,并利用NSStream子类。 此解决scheme基于SampleCodeBank博客上的Obj-C 示例 ( NSInputStream inputStreamWithURL示例ios ,2013年5月12日)。

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

NSStream是表示stream的对象的抽象类。 它的接口对于所有Cocoastream类都是通用的,包括其具体的子类NSInputStream和NSOutputStream。

NSStream对象提供了一种简单的方法,以独立于设备的方式读写数据到各种媒体。 您可以为位于内存,文件或networking(使用套接字)的数据创buildstream对象,并且可以使用stream对象,而无需将所有数据一次加载到内存中。

文件系统编程指南

Apple在线处理整个文件使用Streams FSPG中的文章也提供了NSInputStreamNSOutputStream应该是线程安全的概念。

文件处理,与流

进一步细化

该对象不使用stream委托方法。 还有很多其他的改进空间,但这是我将采取的基本方法。 iPhone的主要焦点是启用大文件pipe理,同时通过缓冲区限制内存( TBD – 利用outputStream内存缓冲区 )。 要清楚的是,苹果确实提到,他们的便捷函数writeToURL只适用于较小的文件大小(但是让我想知道为什么他们不处理大文件 – 这些不是边缘情况,注意 – 将问题作为一个错误)。

结论

我将不得不在后台线程上进一步testing,因为我不想干扰任何NSStream内部队列。 我有一些其他的对象使用类似的想法,通过电线pipe理非常大的数据文件。 最好的方法是在iOS中保持文件大小尽可能小,以节省内存并防止应用程序崩溃。 这些API是考虑到这些限制(这就是为什么试图无限制的video不是一个好主意),所以我将不得不调整预期总体。

( 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 } } 

代表团

这里有一个类似的对象,我从一篇关于高级文件I / O的文章中重写,Eidhof,C., ObjC.io )。 只需稍作调整即可模拟上述行为。 只需将数据redirect到processDataChunk方法中的NSOutputStream

(主要来源 – 检查最新变化的要点)

 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) } } } } 

你应该考虑使用NSStream (NSOutputStream/NSInputStream) 。 如果你要select这种方法,请记住后台线程运行循环将需要显式启动(运行)。

NSOutputStream有一个名为outputStreamToFileAtPath:append:的方法outputStreamToFileAtPath:append:这就是你可能要找的东西。

类似的问题:

在Swift中将一个string写入一个NSOutputStream

Interesting Posts