更新
我已经解决并移除了令人分心的错误。请阅读整篇文章,如有任何问题,请随意留言。
背景
我正在尝试使用Swift 2.0、GCD和完成处理程序将相对较大的文件(视频)写入iOS磁盘。我想知道是否有更有效的方法来执行此任务。该任务需要在不阻塞主UI的情况下完成,并使用完成逻辑,同时确保操作尽可能快地进行。我有带有NSData属性的自定义对象,因此我目前正在尝试使用NSData的扩展。例如,另一种解决方案可能包括使用NSFilehandle或NSStreams,再加上某种形式的线程安全行为,从而比我当前基于NSData writeToURL函数的解决方案具有更快的吞吐量。
NSData有什么问题?
请注意以下讨论摘自NSData类参考文档(保存数据)。我确实会向我的临时目录写入内容,但我遇到问题的主要原因是在处理大文件时可以明显感受到UI的延迟。这种延迟正是因为NSData不是异步的(苹果文档指出原子写操作可能会对“大”文件(> 1MB)造成性能问题)。因此,在处理大文件时,我们需要依赖于NSData方法内部的机制。我进行了更深入的调查,并从苹果公司找到了这些信息..."使用此方法将 data:// URL 转换为 NSData 对象非常理想,也可以用于同步读取短文件。如果需要读取可能很大的文件,请使用 inputStreamWithURL: 打开流,然后分段读取文件"(NSData Class Reference, Objective-C, +dataWithContentsOfURL)。这些信息似乎暗示我可以尝试使用流在后台线程上将文件写出,如果将 writeToURL 移动到后台线程(如 @jtbandes 建议)不足以解决问题。
NSData类及其子类提供了快速和简便的方法将它们的内容保存到磁盘上。为了最大程度地减少数据丢失的风险,这些方法提供了原子保存数据的选项。原子写入保证数据要么完全保存,要么完全失败。原子写开始时将数据写入一个临时文件中。如果此写操作成功,则该方法将临时文件移动到其最终位置。虽然原子写操作最大限度地减少了由于损坏或部分写入的文件而导致的数据丢失的风险,但在写入临时目录、用户主目录或其他公共可访问目录时可能不适用。任何时候当您使用公共可访问文件时,都应将该文件视为不受信任的和潜在危险的资源。攻击者可能会破坏或损坏这些文件。攻击者还可以替换这些文件以硬链接或符号链接的形式,导致您的写操作覆盖或损坏其他系统资源。
在公共可访问目录内工作时,请避免使用writeToURL:atomically:方法(及相关方法)。相反,使用现有文件描述符初始化NSFileHandle对象,并使用NSFileHandle方法安全地写入文件。 其他替代方案
有一篇关于并发编程的文章Concurrent Programming在objc.io上提供了有趣的选项,其中之一是“高级:后台文件I/O”。其中一些选项还涉及使用InputStream。苹果公司也有一些旧的参考资料异步读写文件。我发布这个问题是为了期待Swift的替代方案。
适当回答的示例
以下是一个适当的回答示例,可能会满足这类问题。(取自流编程指南,写入输出流)
使用NSOutputStream实例向输出流写入数据需要几个步骤:
- 创建并初始化一个NSOutputStream实例,用于写入数据的存储库。同时设置代理。
- 将流对象安排在运行循环中并打开流。
- 处理流对象向其代理报告的事件。
- 如果流对象已将数据写入内存,请通过请求NSStreamDataWrittenToMemoryStreamKey属性来获取数据。
- 当没有更多数据可写时,销毁流对象。
Nota Bene
我理解下面的信息错误。为了完整起见,它被包含在内。这个问题正在询问是否有更好的算法可用于使用Swift、API或可能甚至是C/ObjC将大型文件写入iOS。如果有,请提供足够的信息(描述/样本),以便我重建相关的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提供了一些处理内存压力和管理BLOBs的好建议。这是一篇非常棒的文章,提供了许多关于行为和如何控制应用程序中大文件问题的线索。尽管它是特定于Core Data而不是文件的,但对原子写入的警告告诉我,我应该非常谨慎地实现具有原子性的写入方法。
对于大文件,管理写入的唯一安全方式似乎是添加完成处理程序(到写入方法中)并在主线程上显示活动视图。无论读者是使用流还是通过修改现有API添加完成逻辑来完成此操作都可以。我过去已经两种方式都尝试过,并正在测试以获得最佳性能。
直到那时,我将更改解决方案,从Core Data中删除所有二进制数据属性,并用字符串替换它们以保存磁盘上的资源URL。我还利用了来自Assets Library和PHAsset的内置功能来获取和存储所有相关的资源URL。如果需要复制任何资源,则会使用标准API方法(PHAsset / Asset Library上的导出方法),并使用完成处理程序在主线程上通知用户完成状态。 (来自Core Data性能文章的非常有用的代码片段)减少内存开销
有时您可能想临时使用托管对象,例如计算特定属性的平均值。这会导致对象图和内存消耗增加。您可以通过重新使不再需要的单个托管对象失效或重置托管对象上下文以清除整个对象图来减少内存开销。您还可以使用适用于Cocoa编程的模式。
您可以使用NSManagedObjectContext的refreshObject:mergeChanges:方法使单个托管对象失效。这样可以清除其内存中的属性值,从而减少内存开销。(请注意,这与将属性值设置为nil不同 - 如果触发了故障,则将按需检索值 - 请参见故障和唯一性。)
当您创建获取请求时,可以将includesPropertyValues设置为NO以避免创建表示属性值的对象,从而减少内存开销。但是,只有在确定要么不需要实际属性数据,要么已经拥有行缓存中的信息时,才应该这样做,否则您将会产生多次访问持久存储的开销。
您可以使用NSManagedObjectContext的reset方法删除与上下文关联的所有托管对象,并“重新开始”,就像刚刚创建它一样。请注意,任何与该上下文关联的托管对象都将无效,因此您需要丢弃任何引用并重新获取与您仍然感兴趣的该上下文相关的任何对象。如果您迭代了大量对象,则可能需要使用本地自动释放池块,以确保尽快释放临时对象。
如果您不打算使用Core Data的撤消功能,则可以通过将上下文的撤消管理器设置为nil来减少应用程序的资源需求。对于后台工作线程以及大型导入或批处理操作,这可能特别有益。
最后,Core Data默认情况下不会保留托管对象的强引用(除非它们具有未保存的更改)。如果您有很多对象在内存中,请确定拥有引用。托管对象通过关系彼此保持强引用,这很容易创建强引用循环。您可以通过重新使对象失效(再次使用NSManagedObjectContext的refreshObject:mergeChanges:方法)来打破循环。
大数据对象(BLOBs)
如果您的应用程序使用大型BLOB(“二进制大对象”例如图像和声音数据),则需要注意最小化开销。 “小”,“适度”和“大”的确切定义是流体的,并取决于应用程序的使用情况。粗略的经验法则是,大小在千字节左右的对象属于“适度”大小,而大小在兆字节左右的对象则属于“大”大小。一些开发人员已经在数据库中使用10MB BLOB获得了良好的性能。另一方面,如果应用程序在表中有数百万行,则即使128字节也可能是需要规范化到单独表中的“适度”大小CLOB(字符大对象)。
通常,如果您需要将BLOB存储在持久存储中,则应使用SQLite存储。 XML和二进制存储需要整个对象图驻留在内存中,并且存储写入是原子的(请参见持久存储功能),这意味着它们无法有效地处理大型数据对象。 SQLite可以扩展以处理极其大型的数据库。正确使用,SQLite为高达100GB的数据库提供良好的性能,单个行可以容纳高达1GB的数据(尽管将1GB的数据读入内存是一个昂贵的操作,无 注意:
我已将下面的逻辑移入完成处理程序中(请参见上面的代码),我不再看到任何错误。如前所述,这个问题是关于在iOS中使用Swift处理大文件是否有更高效的方法。
当尝试处理生成的项目数组以传递给UIActvityViewController时,使用以下逻辑:
如果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 =“连接中断”}}>(请注意,我正在寻找更好的设计,而不是这个错误消息的答案)
writeToURL
上有完成处理程序的原因。dispatch_async
不是必需的。此外,您不必检查文件是否存在,只需检查writeToURL
的结果即可。 - SulthanwriteToURL
比任何其他方法慢。如果操作确实涉及将大量数据写入文件,则无法避免它。也许您可以重新考虑并尽早开始编写数据。 - jtbandes