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

更新

我已经解决并消除了分散注意力的错误。请阅读整篇文章,如果还有任何问题,请随时发表评论

背景

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

NSData到底出了什么问题?

请注意以下讨论摘自NSData类参考(保存数据)。我确实会对我的临时目录执行写操作,但是我遇到问题的主要原因是,在处理大型文件时,我可以看到UI中存在明显的延迟。这种延迟正是因为NSData不是异步的(苹果文档注意到原子写入会导致大于1mb的“大”文件的性能问题)。因此,当处理大型文件时,NSData方法中的任何内部机制都会对其产生影响

我做了更多的挖掘,从苹果公司找到了这些信息……”这种方法非常适合将数据://URL转换为NSData对象,也可以用于同步读取短文件。如果需要读取潜在的大文件,请使用inputStreamWithURL:打开一个流,然后一次读取一个文件(NSData类引用,Objective-C,+dataWithContentsOfURL)。此信息似乎暗示,如果将writeToURL移动到后台线程(如@jtbandes所建议)不够,我可以尝试使用streams在后台线程上写入文件

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

而原子写入操作将由于
损坏或部分写入的文件,在以下情况下可能不合适:
写入临时目录、用户的主目录或其他
可公开访问的目录。任何时候您使用公开的
可访问的文件,则应将该文件视为不受信任的文件
潜在危险的资源。攻击者可能会破坏或损坏资源
这些文件。攻击者还可以使用硬文件或
符号链接,导致写入操作覆盖或损坏
其他系统资源

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

其他备选方案

在objc.io上有一篇关于并发编程的文章提供了关于“高级:后台文件I/O”的有趣选项。其中一些选项还涉及使用InputStream。苹果也有一些关于异步读取和写入文件的老参考。我发布这个问题是为了期待快速的替代方案

适当答案的示例

下面是一个可能满足这类问题的适当答案的示例

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

  1. 使用
    已写入数据的存储库。还设置一个委托
  2. 安排
    运行循环上的流对象并打开流
  3. 处理事件
    流对象向其委托报告
  4. 如果流对象
    已将数据写入内存,通过请求
    NSStreamDataWriteEntomeryStreamKey属性
  5. 当没有更多的时候
    要写入、处置流对象的数据

我正在寻找最熟练的算法,适用于写作
使用Swift、API甚至可能使用
C/ObjC就足够了。我可以将算法转换成适当的
Swift兼容结构

Nota Bene

我理解下面的信息错误。为了完整起见,将其包括在内。
问题是是否有更好的算法可以使用
用于将大文件写入具有保证的依赖序列的磁盘(例如NSOperation dependencies)。如果存在
请提供足够的信息(说明/样品),以便我
重建相关的Swift 2.0兼容代码)。请告知我是否
缺少任何有助于回答问题的信息

关于分机的注意事项

我已经向基本writeToURL添加了一个完成处理程序,以确保
不会发生意外的资源共享。使用该文件的依赖任务
永远不要面对比赛条件

扩展数据{
func writeToURL(名称:String,完成:(结果:Bool,url:NSURL?)->Void){
让filePath=NSTemporaryDirectory()+命名
//var成功:Bool=false
让tmpURL=NSURL(fileURLWithPath:filePath)
弱var weakSelf=自身
调度异步(调度获取全局队列(调度队列优先级默认为0){
//原子地写入URL
if weakSelf!.writeToURL(tmpURL,原子性:true){
如果NSFileManager.defaultManager().fileExistsAtPath(filePath){
完成(结果:true,url:tmpURL)
}否则{
完成(结果:false,url:tmpURL)
}
}
})
}
}

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

var items=[AnyObject]()
如果let video=myCustomClass.data{
//视频属于NSData类型
video.writeToURL(“shared.mp4”,完成:{(结果,url)->Void in
如果结果{
items.append(url!)
if items.count>0{
让sharedActivityView=UIActivityViewController(activityItems:items,applicationActivities:nil)
self.presentViewController(sharedActivityView,动画:true){()->中的Void
//完成
}
}
}
})
}

结论

关于核心数据性能的Apple Docs为处理内存压力和管理blob提供了一些很好的建议。这是一篇非常好的文章,提供了很多关于行为以及如何在应用程序中缓和大文件问题的线索。现在,尽管它只针对核心数据而不是文件,但原子写入警告确实告诉了m我认为我应该非常小心地实现以原子方式编写的方法

对于大文件,管理写入的唯一安全方法似乎是添加一个完成处理程序(到写入方法)以及在主线程上显示活动视图。是通过流还是通过修改现有API添加完成逻辑来实现这一点,取决于读者。我在过去已经完成了这两项工作,现在正在进行最佳性能测试

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

(核心数据性能文章中非常有用的片段)

减少内存开销

有时,您希望在服务器上使用托管对象
临时基础,例如,用于计算
特定属性。这会导致对象图形和内存
您可以通过以下方式减少内存开销
重新对不再需要的单个托管对象进行故障处理,或者
可以重置托管对象上下文以清除整个对象图。
您还可以使用通常适用于Cocoa编程的模式

您可以使用
NSManagedObjectContext的refreshObject:mergeChanges:method。此
清除其内存中属性值从而减少
它的内存开销。(注意,这与设置
属性值为nil如果
故障被触发,请参见故障和Uniquing。)

创建提取请求时,可以将IncludePropertyValues设置为NO>,以避免创建表示属性值的对象,从而减少内存开销。但是,通常只有在确定不需要实际属性数据或行缓存中已有信息的情况下,才应这样做,否则:你将招致多重损失
访问持久存储

您可以使用NSManagedObjectContext的重置方法删除与上下文关联的所有托管对象,并像刚创建它一样“重新开始”

发表评论