如何有效地写在后台线程大文件到磁盘(SWIFT)

问题描述 投票:33回答:3

更新

我已经解决了,并删除分散注意力的错误。请阅读全文后,并随时发表评论,如果有任何疑问依然存在。

背景

我试图写在使用雨燕2.0,GCD,并完成处理程序的iOS相对较大的文件(视频)到磁盘。我想知道是否有执行此任务更有效的方式。该任务需要同时使用完成逻辑,同时也确保操作快速发生,才可能进行,而不会阻塞主UI完成。我有一个NSData财产,所以我目前使用的NSData的扩展试验自定义对象。作为一个实例的替代的解决方案可能包括使用NSFilehandle或加上某种形式的线程安全行为导致比在其上的I类基础电流溶液NSData的writeToURL函数更快的吞吐量NSStreams。

有什么不对的NSData无论如何?

请注意,从NSData的类参考,(Saving Data)采取了以下的讨论。我做执行写入到我的temp目录不过,我有一个问题,主要的原因是,我可以在UI看到一个明显的滞后大文件的时候。这种滞后正是因为NSData的是不是异步(苹果文件指出,原子写入可以在“大”文件〜> 1MB导致性能问题)。因此,与大文件打交道时,一个是在任何的内部机制是NSData的方法中工作的摆布。

我做了一些更多的挖掘,发现了这个信息从苹果......“这种方法是理想的数据转换:// URL来NSData的对象,也可用于同步读取短文件如果你需要阅读可能很大的文件,使用inputStreamWithURL:打开一个流,然后读取文件中的一块在同一时间“。 (NSData Class Reference, Objective-C, +dataWithContentsOfURL)。这个信息似乎暗示我可以尝试使用流编写出该文件在后台线程,如果writeToURL移动到后台线程(由@jtbandes的建议)是不够的。

NSData的类及其子类提供了方法,其内容快速,轻松地保存到磁盘。为了最大限度地减少数据丢失的风险,这些方法提供原子保存数据的选项。原子写入保证数据是保存在其全部,或完全失效。原子写入开始通过将数据写入到一个临时文件。如果写入成功,则该方法移至临时文件到其最终位置。

虽然原子写操作尽量减少因腐败或部分写入文件中的数据丢失的风险,他们可能不写到一个临时目录,用户的主目录或其他可公开访问的目录时是合适的。您可公开访问的文件的工作任何时候,你应该把该文件作为一个不可信的和有潜在危险的资源。攻击者可能损害或损坏这些文件。攻击者还可以用硬或符号链接替换文件,导致您的写操作覆盖或损坏其他系统资源。

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

其他替代品

在objc.io对并行编程的一个article提供了有趣的选择“高级:文件I / O在后台”。一些选项包括使用一个InputStream为好。苹果也有一些较旧的引用reading and writing files asynchronously。我张贴在斯威夫特的替代预期这个问题。

一个合适的答案的例子

下面是可能满足这一类型的问题一个合适的答案的一个例子。 (取供流编程指南,Writing To Output Streams

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

  1. 创建并与写入的数据存储库初始化NSOutputStream的一个实例。还设置了委托。
  2. 安排在运行循环流对象并打开流。
  3. 处理该流对象报告其委托的事件。
  4. 如果流对象已写入的数据到存储器中,通过请求NSStreamDataWrittenToMemoryStreamKey属性获取数据。
  5. 当没有更多的数据写入,处置流对象。

我要寻找适用于使用斯威夫特,原料药,甚至可能是C / ObjC就足够写非常大的文件到iOS中最精通算法。我可以移调算法为适当斯威夫特兼容的结构。

诺塔Bene的

我理解下面的信息错误。它是出于完整性。 这个问题是询问是否有更好的算法,以用于写大文件到磁盘中的保障依赖序列(如依赖的NSOperation)。如果有请提供足够的信息(描述/样品为我重建相关夫特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
    }
}
        }
     })
}

结论

苹果Google文档Core Data Performance与内存压力处理和管理的BLOB提供一些好的建议。这确实是有很多线索的行为和如何管理您的应用程序中的大文件的问题的文章赫克。现在,虽然它是特定的核心数据,而不是文件,对原子书面警告不告诉我,我应该实现一个非常谨慎原子写入方法。

随着大文件,管理书面唯一安全的方法似乎是在完成处理程序(以写入方法)进行添加,并显示在主线程中的活动视图。是否一个不与一个流或通过修改现有API添加完成逻辑是放在读写。我在过去所做的都和我在测试之中以获得最佳性能。

在那之前,我改变溶液去除从核心数据的所有二进制数据的属性,并用绳子保持在磁盘上的资源网址替换它们。我还利用内置的功能,从资产库和PHAsset抓住并存储所有相关资产的URL。当或者如果我需要复制的任何资产,我将使用与完成处理的标准API方法(上PHAsset /资产库导出的方法)来通知完成情况的用户在主线程上。

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

减少内存开销

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

您可以重新使用故障的NSManagedObjectContext的refreshObject一个单独的管理对象:mergeChanges:方法。这有清除其在内存中的属性值,从而减少它的内存开销的效果。 (请注意,这是不一样的属性值设置为零,该值将故障是否被激发,见断层和Uniquing按需检索。)

当创建可以设置includesPropertyValues为NO获取请求>通过避免对象的创建来表示属性值减少存储器开销。你通常应该只有这样做,但是,如果你确信,要么你将不再需要实际的属性数据,或者你已经在该行缓存中的信息,否则就会招致多次往返持久性存储。

您可以使用的NSManagedObjectContext的复位方法去除与上下文相关联的所有管理对象和如果你只是创造了它“重新开始”。需要注意的是与该上下文关联的任何管理对象将失效,所以你需要放弃对任何引用,并重新获取与该上下文中,你仍然有兴趣有关的任何物体。如果您遍历了很多对象,你可能需要使用本地autorelease池块,以确保临时对象被尽快释放。

如果你不打算使用核心数据的撤消功能,您可以通过上下文的撤消管理器设置为nil,降低应用程序的资源需求。这可能是后台工作线程特别有益,以及大型进口或批处理操作。

最后,核心数据不被默认保存到管理对象的强引用(除非他们有未保存的更改)。如果你有很多内存中的对象,你应该决定拥有引用。管理对象保持通过关系彼此强引用,它可以很容易地创建强参考周期。 (:mergeChanges:NSManagedObjectContext中的方法,通过使用refreshObject再次),您可以通过重新断层对象打破循环。

大数据对象(BLOB)

如果应用程序使用中的大BLOB(“二进制大对象”,如图像和声音数据),你需要照顾,以尽量减少开销。 “小”,“适度”和“大”的确切定义是流动的,取决于应用程序的使用。拇指的松散的规则是,在尺寸的千字节顺序对象是“温和”的大小和那些在尺寸兆字节的顺序是“大”尺寸。一些开发商已在数据库中取得了良好的业绩与10MB的BLOB。在另一方面,如果一个应用程序有几百万行的表,甚至是128个字节可能需要被归到一个单独的表中的“温和”大小的CLOB(字符大对象)。

一般来说,如果你需要存储BLOB的持久存储,你应该使用一个SQLite店。在XML和二进制店要求整个对象图驻留在内存和存储写是原子(见持久性存储功能),这意味着它们不能有效地与大数据对象处理。 SQLite的可扩展处理非常大的数据库。如果使用得当,SQLite的提供数据库多达100GB的良好表现,以及单排可容纳高达1GB(尽管当然读取1GB数据到内存中的是昂贵的操作无论多么高效的存储库)。

甲BLOB往往代表的实体 - 例如一个属性,照片可能是一个雇员实体的一个属性。对于小规模适中的BLOB(和CLOB),你应该为数据创建一个单独的实体,并创建替代属性的一对一的关系。例如,您可以创建员工和照片实体与它们之间的一个一对一的关系,从地方到员工照片的关系取代了员工的照片属性。这种模式最大化目标断层的(见断层和Uniquing)的好处。如果确实需要它任何给定的照片仅检索(如果关系遍历)。

这是更好的,但是,如果你能BLOB的存储为文件系统上的资源,并保持链接(如URL或路径),以这些资源。然后,您可以加载BLOB,以备不时之需。

注意:

我已移动逻辑下面到完成处理程序(见上面的代码)和I再也看不到任何错误。由于这个问题之前提到的是关于是否有要处理使用iOS版雨燕大文件更高性能的方式。

当试图处理所得到的物品阵列要传递给UIActvityViewController,使用以下逻辑:

如果items.count> 0 { 让sharedActivityView = UIActivityViewController(activityItems:物品,applicationActivities:无)self.presentViewController(sharedActivityView,动画:真){() - >空隙在//成品}}

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

ios swift multithreading large-files large-data
3个回答
20
投票

性能取决于用是否在RAM中的数据相符。如果是这样,那么你应该使用NSData writeToURLatomically功能开启,这是你在做什么。

苹果的这个是很危险的时候“写入公共目录”笔记是完全不相干的iOS上,因为没有公共目录。这部分仅适用于OS X.坦率地说,这不是真正重要的有两种。

所以,你写的代码,只要视频RAM适合(约100MB将是一个安全限度)尽可能高效。

对于不适合在RAM中的文件,你需要使用一个流或在按住视频存储您的应用程序会崩溃。从服务器下载大量的视频,并将其写入到磁盘,你应该使用NSURLSessionDownloadTask

一般来说,流(包括NSURLSessionDownloadTask)将大小比NSData.writeToURL()慢几个数量级。所以,除非你需要不使用流。在NSData所有的操作都非常快,这是完全能够与在大小数TB与OS X的性能优良的文件处理的(iOS版显然无法具有文件大,但它是相同的类具有相同的性能)。


有代码中的几个问题。

这是错误的:

let filePath = NSTemporaryDirectory() + named

相反,始终做到:

let filePath = NSTemporaryDirectory().stringByAppendingPathComponent(named)

但是,这并不理想要么,你应该避免使用路径(他们是越野车和慢)。相反,使用这样的网址:

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

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

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

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

if fileURL.checkResourceIsReachableAndReturnError(nil) {

6
投票

最新解决方案(2018)

另一种有用的可能性可能包括使用每当缓冲器被填充(或如果已使用的记录的定时的长度),以追加数据,并且还公布数据的流的端部的封闭件。在一些照片的API相结合,这可能导致好的结果。因此,像下面的一些声明式的代码可以在加工过程中被解雇:

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

在处理你的管理对象,这些罩可以让你简洁处理任何大小的数据,同时保持在控制之下的记忆。

夫妇这一想法与使用该聚合件作品为单一dispatch_group并可能会有一些令人兴奋的可能性递归方法。

苹果文档的状态:

DispatchGroup允许工作的总同步。你可以用它们来提交多个不同的工作项目和跟踪时,他们都完成,尽管他们可能会在不同的队列中运行。此行为可能是有益的,当不能取得进展,直到所有的特定任务已完成。

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

我毫不怀疑,我会完善这一多一些,但话题是十分复杂,需要一个独立的自我答案。我决定采取从其他答案了一些建议,并充分利用NSStream子类。该解决方案是基于一个OBJ-C sample(NSInputStream inputStreamWithURL例如IOS,2013年,5月12日)贴过上SampleCodeBank博客。

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

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

NSStream对象提供一种简单的方法来读取和写入数据和从在与设备无关的方式各种各样的媒体。您可以为位于内存中的数据流对象,在文件中,或在网络上(使用套接字),你可以使用流对象而不加载所有数据到内存中一次。

文件系统编程指南

苹果在FSPG Processing an Entire File Linearly Using Streams文章还提供了NSInputStream概念和NSOutputStream应固有线程安全的。

file-processing-with-streams

进一步改进

这个对象不使用流代理使用的方法。的空间,其他大量的改进,以及但是这是基本的方法,我会抓住。主要焦点在iPhone上正在使大文件管理,同时经由缓冲器约束存储器(TBD - 利用OutputStream的内存缓冲区)。需要明确的是,苹果并未提到​​他们方便的功能是writeToURL仅适用于较小的文件大小(但让我不知道他们为什么不照顾较大的文件 - 这些不是边缘的情况下,说明 - 将文件问题作为一个错误)。

结论

我将不得不进一步测试在后台线程整合,因为我不想和任何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
投票

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

NSOutputStream有一个名为outputStreamToFileAtPath:append:方法,它是什么,你可能会寻找。

类似的问题:

Writing a String to an NSOutputStream in Swift

© www.soinside.com 2019 - 2024. All rights reserved.