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

时间:2015-08-12 12:41:53

标签: ios swift multithreading large-files large-data

更新

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

背景

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

无论如何,NSData出了什么问题?

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

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

  

NSData类及其子类提供快速和   轻松将其内容保存到磁盘。为了将数据丢失的风险降至最低,   这些方法提供了以原子方式保存数据的选项。原子   写保证数据可以全部保存,也可以保存   彻底失败了。通过将数据写入a来开始原子写入   临时文件。如果此写入成功,则该方法移动   临时文件到最终位置。

     

虽然原子写操作可以最大限度地降低数据丢失的风险   腐败或部分写入的文件,它们可能不适合   写入临时目录,用户的主目录或其他   公共可访问目录。任何时候你公开工作   可访问文件,您应该将该文件视为不受信任的文件   潜在的危险资源。攻击者可能会妥协或腐败   这些文件。攻击者也可以用硬盘或硬盘替换文件   符号链接,导致您的写操作被覆盖或损坏   其他系统资源。

     

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

其他替代方案

objc.io上的一个article并发编程提供了有关选项"高级:背景中的文件I / O"。一些选项也涉及使用InputStream。 Apple也有一些较早的reading and writing files asynchronously引用。我发布这个问题是为了期待Swift的替代品。

正确答案的示例

以下是可能满足此类问题的适当答案的示例。 (采用流编程指南,Writing To Output Streams

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

  1. 使用a创建并初始化NSOutputStream的实例 写入数据的存储库。还设置了一个代表。
  2. 安排     在运行循环上流对象并打开流。
  3. 处理事件     流对象向其委托报告。
  4. 如果是流对象     已将数据写入内存,通过请求获取数据     NSStreamDataWrittenToMemoryStreamKey属性。
  5. 什么时候没有     要写入的数据,处理流对象。
  6.   

    我正在寻找适用于写作的最熟练的算法   使用Swift,API甚至可能是iOS的极大文件   C / ObjC就足够了。我可以将算法转换成适当的   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)
                            }
                        }
                })
    
            }
        }
    

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

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

    结论

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

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

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

    (Core Data Performance文章中非常有用的摘录)

      

    减少内存开销

         

    有时您希望在a上使用托管对象   临时基础,例如计算a的平均值   特殊属性。这会导致您的对象图和内存   消费,增长。您可以减少内存开销   重新断开您不再需要的个人管理对象,或者您   可以重置托管对象上下文以清除整个对象图。   您也可以使用适用于Cocoa编程的模式。

         

    您可以使用重新排除单个托管对象的故障   NSManagedObjectContext的refreshObject:mergeChanges:方法。这有   清除其内存属性值从而减少的影响   它的内存开销。 (请注意,这与设置不同   属性值为nil - 如果需要,将按需检索值   错误被触发 - 见Faulting和Uniquing。)

         

    创建获取请求时,您可以将includesPropertyValues设置为NO>通过避免创建表示属性值的对象来减少内存开销。但是,如果您确定要么不需要实际的属性数据,要么已经有行缓存中的信息,那么通常只能这样做,否则会产生多个   前往持久性商店。

         

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

         

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

         

    最后,Core Data默认不会保持强大   对托管对象的引用(除非它们有未保存的更改)。如果   你在内存中有很多对象,你应该确定拥有   引用。托管对象保持对彼此的强引用   通过关系,可以轻松创建强有力的参考   周期。您可以通过重新错误的对象来打破循环(再次使用   refreshObject:mergeChanges:NSManagedObjectContext的方法。)

         

    大型数据对象(BLOB)

         

    如果您的应用程序使用大型BLOB(" Binary Large OBjects"例如   图像和声音数据),您需要注意尽量减少开销。   “小”,“适度”和“大”的确切定义是流动的   取决于应用程序的用法。一个宽松的经验法则是   大小为千字节的对象是“适度”大小的   那些大小为兆字节的大小是“大”的。一些   开发人员使用10MB BLOB实现了良好的性能   数据库。另一方面,如果应用程序有数百万行   一个表,甚至128个字节可能是一个"适度的"大小CLOB(字符   需要将规范化为单独的表的大型对象。

         

    通常,如果您需要将BLOB存储在持久存储中,那么   应该使用SQLite商店。 XML和二进制存储需要   整个对象图驻留在内存中,而存储写入是原子的(参见   持久存储功能)这意味着它们不能有效   处理大型数据对象。 SQLite可以扩展以进行极端处理   大型数据库。正确使用,SQLite提供了良好的性能   数据库最高可达100GB,单行最多可容纳1GB(尽管如此)   当然,将1GB数据读入内存是一项昂贵的操作   无论存储库有多高效。)

         

    BLOB通常表示实体的属性 - 例如,a   照片可能是Employee实体的属性。小到   适度大小的BLOB(和CLOB),您应该创建一个单独的实体   为数据和创建一个一对一的关系代替   属性。例如,您可以创建员工和照片   它们之间具有一对一关系的实体,其中   从员工到照片的关系取代了员工   照片属性。这种模式最大化了对象的好处   错误的(见错误和Uniquing)。任何给定的照片都是   如果确实需要(如果遍历关系)则检索。

         

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

    注意:

      

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

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

    if items.count> 0 {
    让sharedActivityView = UIActivityViewController(activityItems:items,applicationActivities:nil) self.presentViewController(sharedActivityView,animated:true){() - >无效 //完成} }

    我看到以下错误:通讯错误:{count = 1, contents =" XPCErrorDescription" => {length = 22,内容="连接中断" }> (请注意,我正在寻找更好的设计,而不是这个错误消息的答案)

3 个答案:

答案 0 :(得分:20)

性能取决于数据是否适合RAM。如果确实如此,那么您应该在启用了NSData writeToURL功能的情况下使用atomically,这就是您正在做的事情。

Apple在关于写入公共目录时会发现这种情况很危险。在iOS上完全不相关,因为没有公共目录。该部分仅适用于OS X.坦率地说,它在那里也不是很重要。

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

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

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

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

这是错误的:

let filePath = NSTemporaryDirectory() + named

相反总是这样做:

let filePath = NSTemporaryDirectory().stringByAppendingPathComponent(named)

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

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

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

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

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

if fileURL.checkResourceIsReachableAndReturnError(nil) {

答案 1 :(得分:5)

最新解决方案(2018)

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

var dataSpoolingFinished: ((URL?, Error?) -> Void)?
var dataSpooling: ((Data?, Error?) -> Void)?

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

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

Apple docs声明:

  

DispatchGroup允许工作的聚合同步。您可以   使用它们提交多个不同的工作项并跟踪它们   一切都完整,即使它们可能在不同的队列上运行。这个   当所有的进展都无法取得进展时,行为可能会有所帮助   指定的任务已完成。

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

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

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

  

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

     

NSStream对象提供了一种简单的方法来读取和写入数据   来自各种媒体的独立于设备的方式。你可以创建   流对象位于内存,文件或网络中的数据   (使用套接字),您可以使用流对象而不加载所有   将数据一次性存入内存。

文件系统编程指南

Apple在FSPG中的Processing an Entire File Linearly Using Streams文章也提供了NSInputStreamNSOutputStream本身应该是线程安全的概念。

file-processing-with-streams

进一步改进

此对象不使用流委派方法。其他改进的空间也很大,但这是我将采取的基本方法。 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
    }


}

这是一个类似的对象,我在Advanced File I/O in the background,Eidhof,C。,ObjC.io上的一篇文章中重写了这篇文章。只需进行一些调整,就可以模拟上面的行为。只需将数据重定向到NSOutputStream方法中的processDataChunk即可。

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

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

答案 2 :(得分:2)

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

NSOutputStream有一个名为outputStreamToFileAtPath:append:的方法,你可能正在寻找它。

类似的问题:

Writing a String to an NSOutputStream in Swift