Cómo escribir de manera eficiente files de gran tamaño en el disco en el hilo de background (Swift)

Actualizar

He resuelto y eliminado el error de distracción. Lea la publicación completa y no dude en dejar comentarios si quedan preguntas.

Fondo

Estoy intentando escribir files relativamente grandes (video) en el disco en iOS usando Swift 2.0, GCD y un manejador de finalización. Me gustaría saber si hay una forma más eficiente de realizar esta tarea. La tarea debe llevarse a cabo sin bloquear la interfaz de usuario principal, mientras se utiliza la lógica de finalización, y también se asegura de que la operación ocurra lo más rápido posible. Tengo objects personalizados con una propiedad NSData, por lo que actualmente estoy experimentando el uso de una extensión en NSData. Como ejemplo, una solución alternativa podría include el uso de NSFilehandle o NSStream junto con algún tipo de comportamiento seguro de subprocesss que produzca un performance mucho más rápido que la function NSData writeToURL en la que baso la solución actual.

¿Qué ocurre con NSData de todos modos?

Tenga en count la siguiente discusión tomada de la NSData Class Reference, ( Guardar datos ). Realizo grabaciones en mi directory temporal, sin embargo, la razón principal por la que estoy teniendo un problema es que puedo ver un retraso notable en la interfaz de usuario cuando se trata de files grandes. Este retraso es precisamente porque NSData no es asíncrono (y Apple Docs nota que las escrituras atómicas pueden causar problemas de performance en files "grandes" ~> 1 mb). Entonces, cuando se trata de files de gran tamaño, uno está a merced de cualquier mecanismo interno que esté funcionando dentro de los methods NSData.

Hice un poco más de búsqueda y encontré esta información de Apple … "Este método es ideal para convertir datos: // URL a objects NSData, y también se puede usar para leer files cortos sincrónicamente. Si necesita leer files potencialmente grandes , use inputStreamWithURL: para abrir una transmisión, lea el file una pieza a la vez ". ( NS Class Class Reference, Objective-C, + dataWithContentsOfURL ). Esta información parece implicar que podría intentar usar secuencias para escribir el file en un hilo de background si mover el writeToURL al hilo de background (como lo sugiere @jtbandes) no es suficiente.

La class NSData y sus subclasss proporcionan methods para save rápida y fácilmente sus contenidos en el disco. Para minimizar el riesgo de pérdida de datos, estos methods ofrecen la opción de save los datos atómicamente. Las escrituras atómicas garantizan que los datos se guarden en su totalidad, o falle por completo. La escritura atómica comienza escribiendo los datos en un file temporal. Si esta escritura tiene éxito, entonces el método mueve el file temporal a su location final.

Mientras que las operaciones de escritura atómica minimizan el riesgo de pérdida de datos debido a files corruptos o parcialmente escritos, pueden no ser apropiados cuando se escribe en un directory temporal, en el directory de inicio del usuario u otros directorys accesibles públicamente. Cada vez que trabaje con un file de acceso público, debe tratar ese file como un recurso no confiable y potencialmente peligroso. Un atacante puede comprometer o corromper estos files. El atacante también puede replace los files con enlaces rígidos o simbólicos, haciendo que sus operaciones de escritura sobrescriban o corrompan otros resources del sistema.

Evite utilizar writeToURL: atómicamente: método (y los methods relacionados) cuando trabaja dentro de un directory de acceso público. En su lugar, inicialice un object NSFileHandle con un descriptor de file existente y utilice los methods NSFileHandle para escribir de forma segura el file.

Otras alternativas

Un artículo sobre progtwigción simultánea en objc.io ofrece opciones interesantes sobre "Avanzado: E / S de file en segundo plano". Algunas de las opciones implican el uso de un InputStream también. Apple también tiene references antiguas para leer y escribir files de forma asíncrona . Estoy publicando esta pregunta en anticipación de las alternativas Swift.

Ejemplo de una respuesta adecuada

Aquí hay un ejemplo de una respuesta adecuada que podría satisfacer este tipo de preguntas. (Tomado para la Guía de progtwigción de secuencias, Escritura para flujos de salida )

El uso de una instancia de NSOutputStream para escribir en una secuencia de salida requiere varios pasos:

  1. Cree e inicialice una instancia de NSOutputStream con un repository para los datos escritos. También establezca un delegado.
  2. Programe el object de transmisión en un ciclo de ejecución y abra la transmisión.
  3. Maneje los events que el object de flujo reporta a su delegado.
  4. Si el object de flujo tiene datos escritos en la memory, obtenga los datos solicitando la propiedad NSStreamDataWrittenToMemoryStreamKey.
  5. Cuando no hay más datos para escribir, elimine el object de la secuencia.

Estoy buscando el algorithm más competente que se aplica a la escritura de files extremadamente grandes a iOS utilizando Swift, API o, posiblemente, incluso C / ObjC sería suficiente. Puedo transponer el algorithm en construcciones compatibles Swift apropiadas.

Nota Bene

Entiendo el error informativo a continuación. Se incluye para completar. Esta pregunta es preguntar si existe o no un algorithm mejor para usar para escribir files grandes en el disco con una secuencia de dependencia garantizada (p. Ej., Dependencias NSOperation). Si existe, proporcione suficiente información (descripción / muestra para rebuild el código compatible Swift 2.0 pertinente). Indique si me falta alguna información que ayude a responder la pregunta.

Nota sobre la extensión

Agregué un controller de finalización a la base writeToURL para garantizar que no se produzca el intercambio involuntario de resources. Mis tareas dependientes que usan el file nunca deben enfrentar una condición de carrera.

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

Este método se utiliza para procesar los datos de objects personalizados de un controller utilizando:

 var items = [AnyObject]() if let video = myCustomClass.data { //video is of type NSData video.writeToURL("shanetworking.mp4", completion: { (result, url) -> Void in if result { items.append(url!) if items.count > 0 { let shanetworkingActivityView = UIActivityViewController(activityItems: items, applicationActivities: nil) self.presentViewController(shanetworkingActivityView, animated: true) { () -> Void in //finished } } } }) } 

Conclusión

Apple Docs en Core Data Performance proporciona algunos buenos consejos sobre cómo lidiar con la presión de la memory y la gestión de los BLOB. Este es realmente un gran artículo con muchas pistas sobre el comportamiento y cómo moderar el problema de los files de gran tamaño dentro de su aplicación. Ahora bien, aunque es específico de Core Data y no de files, la advertencia sobre escritura atómica me dice que debo implementar methods que escriben atómicamente con gran cuidado.

Con files grandes, la única forma segura de administrar la escritura parece estar agregando un controller de finalización (al método de escritura) y mostrando una vista de actividad en el hilo principal. Si uno hace eso con una secuencia o modifica una API existente para agregar lógica de finalización depende del lector. He hecho ambas cosas en el pasado y estoy en medio de las testings para get el mejor performance.

Hasta entonces, estoy cambiando la solución para eliminar todas las properties de datos binarys de Core Data y reemplazándolas por cadenas para mantener las URL de los activos en el disco. También estoy aprovechando la funcionalidad integrada de Assets Library y PHAsset para capturar y almacenar todas las URL de activos relacionadas. Cuando necesito copyr cualquier activo utilizaré methods estándar de API (methods de export en PHAsset / Asset Library) con manejadores de finalización para notificar al usuario sobre el estado final en el hilo principal.

(Fragmentos realmente útiles del artículo de Core Data Performance)

Reducción de la memory general

En ocasiones, se desea utilizar temporalmente los objects administrados, por ejemplo, para calcular un valor promedio para un atributo en particular. Esto hace que crezca el gráfico de object y el consumo de memory. Puede networkingucir la sobrecarga de memory volviendo a fallar los objects gestionados individuales que ya no necesita, o puede restablecer un context de object gestionado para borrar un gráfico de object completo. También puede usar patrones que se aplican a la progtwigción de cocoa en general.

Puede volver a fallar un object administrado individual utilizando el object refreshObject de NSManagedObjectContext: mergeChanges: method. Esto tiene el efecto de borrar sus valores de propiedad en memory, networkinguciendo así su sobrecarga de memory. (Tenga en count que esto no es lo mismo que establecer los valores de las properties en nil: los valores se recuperarán bajo demanda si se activa el fallo; consulte Error y Deshabilitación).

Cuando crea una request de búsqueda, puede establecer includesPropertyValues ​​en NO> para networkingucir la sobrecarga de memory al evitar la creación de objects para representar los valores de la propiedad. Normalmente, solo debería hacerlo, sin embargo, si está seguro de que no necesitará los datos de properties reales o ya tiene la información en el caching de filas, de lo contrario incurrirá en múltiples viajes a la tienda persistente.

Puede usar el método de reinicio de NSManagedObjectContext para eliminar todos los objects gestionados asociados con un context y "comenzar de nuevo" como si lo hubiera creado. Tenga en count que cualquier object gestionado asociado con ese context se invalidará y, por lo tanto, deberá descartar cualquier reference y volver a search los objects asociados con ese context en el que todavía está interesado. Si itera sobre una gran cantidad de objects, es posible que necesite utilizar bloques de agrupación de autorelease locales para garantizar que los objects temporales se desasignen tan pronto como sea posible.

Si no tiene intención de usar la funcionalidad Deshacer de Core Data, puede networkingucir los requisitos de resources de la aplicación configurando el administrador de deshacer del context en nil. Esto puede ser especialmente beneficioso para los subprocesss de trabajo en segundo plano, así como para operaciones de import o por lotes grandes.

Finalmente, Core Data no mantiene de forma pnetworkingeterminada references fuertes a los objects administrados (a less que tengan cambios no guardados). Si tiene muchos objects en la memory, debe determinar las references propias. Los objects gestionados mantienen fuertes references entre sí a través de las relaciones, que pueden crear fácilmente ciclos de reference fuertes. Puede romper ciclos volviendo a criticar objects (de nuevo usando el object refresh: mergeChanges: método de NSManagedObjectContext).

Grandes objects de datos (BLOB)

Si su aplicación utiliza grandes BLOB ("Binary Large OBjects", como image y datos de sonido), debe tener cuidado de minimizar los gastos generales. La definición exacta de "pequeño", "modesto" y "grande" es fluido y depende del uso de una aplicación. Una regla general es que los objects en el order de kilobytes son de un tamaño "modesto" y aquellos en el order de megabytes de tamaño son "grandes". Algunos desarrolladores han logrado un buen performance con 10MB BLOB en una database. Por otro lado, si una aplicación tiene millones de filas en una tabla, incluso 128 bytes pueden ser un CLOB (Object Large Character) de tamaño "modesto" que necesita normalizarse en una tabla separada.

En general, si necesita almacenar BLOB en una tienda persistente, debe usar una tienda SQLite. Los almacenes XML y binarys requieren que todo el gráfico de object resida en la memory y que las escrituras de almacenamiento sean atómicas (consulte Características de almacenamiento persistente), lo que significa que no tratan de manera eficiente con objects de datos de gran tamaño. SQLite puede escalar para manejar bases de datos extremadamente grandes. Utilizado correctamente, SQLite proporciona un buen performance para bases de datos de hasta 100 GB, y una sola fila puede contener hasta 1 GB (aunque, por supuesto, leer 1 GB de datos en la memory es una operación costosa, sin importar cuán eficiente sea el repository).

Un BLOB a menudo representa un atributo de una entidad; por ejemplo, una fotografía puede ser un atributo de una entidad de Empleado. Para los pequeños y modestos BLOBs (y CLOBs), debe crear una entidad separada para los datos y crear una relación de uno a uno en lugar del atributo. Por ejemplo, puede crear entidades de Empleado y Fotografía con una relación de uno a uno entre ellas, donde la relación de Empleado a Fotografía reemplaza el atributo de fotografía del Empleado. Este patrón maximiza los beneficios de la falla de objects (ver Fallas y Desynchronization). Cualquier fotografía dada solo se recupera si realmente se necesita (si la relación se atraviesa).

Sin embargo, es mejor si puede almacenar BLOB como resources en el sistema de files y para mantener enlaces (tales como URL o routes) a esos resources. A continuación, puede cargar un BLOB como y cuando sea necesario.

Nota:

He movido la lógica a continuación al controller de finalización (ver el código anterior) y ya no veo ningún error. Como se mencionó antes, esta pregunta trata sobre si existe o no una manera más efectiva de procesar files grandes en iOS usando Swift.

Al intentar procesar la matriz de elementos resultante para pasar a un UIActvityViewController, utilizando la siguiente lógica:

si items.count> 0 {
deje shanetworkingActivityView = UIActivityViewController (activityItems: items, applicationActivities: nil) self.presentViewController (shanetworkingActivityView, animated: true) {() -> Void in // finished}}

Veo el siguiente error: Error de comunicaciones: {count = 1, contents = "XPCErrorDescription" => {length = 22, contents = "Connection interrupted"}}> (tenga en count que estoy buscando un mejor layout, no un respuesta a este post de error)

El performance depende de si los datos se ajustan a la RAM. Si lo hace, entonces debería usar NSData writeToURL con la característica atomically activada, que es lo que está haciendo.

Las notas de Apple acerca de que esto es peligroso cuando "escriben en un directory público" son completamente irrelevantes en iOS porque no hay directorys públicos. Esa sección solo se aplica a OS X. Y, francamente, tampoco es realmente importante.

Por lo tanto, el código que ha escrito es lo más eficiente posible, siempre que el video se ajuste a la RAM (unos 100MB serían un límite seguro).

Para los files que no encajan en la memory RAM, debe utilizar una transmisión o su aplicación se bloqueará mientras mantiene el video en la memory. Para download un video grande de un server y escribirlo en el disco, debe usar NSURLSessionDownloadTask .

En general, la transmisión (incluido NSURLSessionDownloadTask ) tendrá órdenes de magnitud más lentas que NSData.writeToURL() . Por lo tanto, no use una transmisión a less que lo necesite. Todas las operaciones en NSData son extremadamente rápidas, es perfectamente capaz de manejar files de varios terabytes de tamaño con un excelente performance en OS X (obviamente, iOS no puede tener files tan grandes, pero es la misma class con el mismo performance).


Hay algunos problemas en su código.

Esto está mal:

 let filePath = NSTemporaryDirectory() + named 

En cambio, siempre haga lo siguiente:

 let filePath = NSTemporaryDirectory().stringByAppendingPathComponent(named) 

Pero tampoco es ideal, debes evitar usar routes (son cochambrosas y lentas). En su lugar, use una URL como esta:

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

Además, está utilizando una ruta para comprobar si el file existe … no haga esto:

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

En su lugar, use NSURL para verificar si existe:

 if fileURL.checkResourceIsReachableAndReturnError(nil) { 

Solución actual

No tengo dudas de que refinaré esto un poco más, pero el tema es lo suficientemente complejo como para justificar una auto-respuesta por separado. Decidí tomar algunos consejos de las otras respuestas y aprovechar las subclasss de NSStream. Esta solución se basa en una muestra de Obj-C ( NSInputStream inputStreamWithURL ejemplo ios , 2013, 12 de mayo) publicada en el blog SampleCodeBank .

La documentation de Apple señala que con una subclass NSStream NO es necesario cargar todos los datos en la memory al mismo time . Esa es la key para poder gestionar files multimedia de cualquier tamaño (sin exceder el disco disponible o el espacio RAM).

NSStream es una class abstracta para objects que representan secuencias. Su interfaz es común a todas las classs de flujo de Cocoa, incluidas sus subclasss concretas NSInputStream y NSOutputStream.

Los objects NSStream proporcionan una manera fácil de leer y escribir datos desde y hacia una variedad de medios de una manera independiente del dispositivo. Puede crear objects de flujo para datos ubicados en la memory, en un file o en una networking (usando sockets), y puede usar los objects de transmisión sin cargar todos los datos en la memory de una vez.

Guía de progtwigción de sistema de files

El file de procesamiento de todo un file de Apple utilizando el artículo de Streams en el FSPG también proporcionó la idea de que NSInputStream y NSOutputStream deberían ser intrínsecamente seguros.

file-processing-with-streams

Más refinamientos

Este object no utiliza methods de delegación de flujo. Mucho espacio para otros refinamientos también, pero este es el enfoque básico que tomaré. El foco principal en el iPhone es habilitar la administración de files de gran tamaño a la vez que restringe la memory a través de un buffer ( TBD – Aproveche el buffer de memory outputStream ). Para ser claros, Apple menciona que sus funciones de conveniencia que writeToURL son solo para tamaños de file más pequeños (pero me hace preguntarme por qué no se ocupan de los files más grandes – Estos no son casos de borde, nota – presentarán la pregunta como un error )

Conclusión

Tendré que probar más para integrarme en un hilo de background ya que no quiero interferir con ninguna NSStream interna de NSStream . Tengo algunos otros objects que usan ideas similares para administrar files de datos extremadamente grandes a través del cable. El mejor método es mantener el tamaño de file lo más pequeño posible en iOS para conservar la memory y evitar lockings de la aplicación. Las API se crean teniendo en count estas limitaciones (por lo que intentar un video ilimitado no es una buena idea), así que tendré que adaptar las expectativas en general.

( Fuente de Gist , consultar gist para conocer los últimos cambios)

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

Delegación

Aquí hay un object similar que reescribí de un artículo sobre Advanced File I / O en segundo plano , Eidhof, C, ObjC.io ). Con solo algunos retoques, se podría hacer para emular el comportamiento anterior. Simplemente networkingirija los datos a un NSOutputStream en el método NSOutputStream .

( Fuente de Gist – Verificar gist para conocer los últimos cambios)

 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.EndEncountenetworking: 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.ErrorOccurnetworking: 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) } } } } 

Debería considerar utilizar NSStream (NSOutputStream/NSInputStream) . Si va a elegir este enfoque, tenga en count que el bucle de ejecución del subprocess de background deberá iniciarse (ejecutarse) explícitamente.

NSOutputStream tiene un método llamado outputStreamToFileAtPath:append: que es lo que puede estar buscando.

Pregunta similar:

Escribir una cadena en un NSOutputStream en Swift