web-dev-qa-db-fra.com

Comment écrire efficacement de gros fichiers sur le disque sur le thread d'arrière-plan (Swift)

Mise à jour

J'ai résolu et supprimé l'erreur gênante. Veuillez lire l'intégralité du message et n'hésitez pas à laisser des commentaires si des questions subsistent.

Contexte

J'essaie d'écrire des fichiers relativement volumineux (vidéo) sur le disque sur iOS en utilisant Swift 2.0, GCD et un gestionnaire de complétion. Je voudrais savoir s'il existe un moyen plus efficace d'effectuer cela task. La tâche doit être effectuée sans bloquer l'interface utilisateur principale, tout en utilisant la logique d'achèvement et en veillant également à ce que l'opération se déroule aussi rapidement que possible. J'ai des objets personnalisés avec une propriété NSData, donc j'expérimente actuellement en utilisant une extension sur NSData. À titre d'exemple, une autre solution pourrait inclure l'utilisation de NSFilehandle ou NSStreams couplé à une certaine forme de comportement thread-safe qui se traduit par un débit beaucoup plus rapide que la fonction NSData writeToURL sur laquelle je base la solution actuelle.

Quel est le problème avec NSData de toute façon?

Veuillez noter la discussion suivante tirée de la référence de classe NSData, ( Saving Data ). J'effectue des écritures dans mon répertoire temporaire, mais la principale raison pour laquelle j'ai un problème est que je peux voir un décalage notable dans l'interface utilisateur lorsque je traite des fichiers volumineux. Ce décalage est précisément dû au fait que NSData n'est pas asynchrone (et Apple Docs note que les écritures atomiques peuvent entraîner des problèmes de performances sur les "gros" fichiers ~> 1 Mo). miséricorde de tout mécanisme interne à l'œuvre dans les méthodes NSData.

J'ai creusé un peu plus et j'ai trouvé ces informations d'Apple ... "Cette méthode est idéale pour convertir des données: // URL en objets NSData, et peut également être utilisée pour lire des fichiers courts de manière synchrone. Si vous devez lire des fichiers potentiellement volumineux , utilisez inputStreamWithURL: pour ouvrir un flux, puis lisez le fichier morceau par morceau. " ( Référence de classe NSData, Objective-C, + dataWithContentsOfURL ). Ces informations semblent impliquer que je pourrais essayer d'utiliser des flux pour écrire le fichier sur un thread d'arrière-plan si le déplacement de writeToURL vers le thread d'arrière-plan (comme suggéré par @jtbandes) n'est pas suffisant.

La classe NSData et ses sous-classes fournissent des méthodes pour enregistrer rapidement et facilement leur contenu sur le disque. Pour minimiser le risque de perte de données, ces méthodes offrent la possibilité d'enregistrer les données de manière atomique. Les écritures atomiques garantissent que les données sont soit enregistrées dans leur intégralité, soit qu'elles échouent complètement. L'écriture atomique commence par l'écriture des données dans un fichier temporaire. Si cette écriture réussit, la méthode déplace le fichier temporaire vers son emplacement final.

Bien que les opérations d'écriture atomique minimisent le risque de perte de données en raison de fichiers corrompus ou partiellement écrits, elles peuvent ne pas être appropriées lors de l'écriture dans un répertoire temporaire, le répertoire de base de l'utilisateur ou d'autres répertoires accessibles au public. Chaque fois que vous travaillez avec un fichier accessible au public, vous devez traiter ce fichier comme une ressource non fiable et potentiellement dangereuse. Un attaquant peut compromettre ou corrompre ces fichiers. L'attaquant peut également remplacer les fichiers par des liens matériels ou symboliques, provoquant l'écrasement ou la corruption de vos autres ressources système par vos opérations d'écriture.

Évitez d'utiliser la méthode writeToURL: atomically: (et les méthodes associées) lorsque vous travaillez dans un répertoire accessible au public. Au lieu de cela, initialisez un objet NSFileHandle avec un descripteur de fichier existant et utilisez les méthodes NSFileHandle pour écrire en toute sécurité le fichier.

Autres alternatives

Un article sur la programmation simultanée sur objc.io fournit des options intéressantes sur "Avancé: E/S de fichiers en arrière-plan". Certaines des options impliquent également l'utilisation d'un InputStream. Apple a également des références plus anciennes à lecture et écriture de fichiers de manière asynchrone . Je poste cette question en prévision de Swift alternatives).

Exemple de réponse appropriée

Voici un exemple de réponse appropriée qui pourrait satisfaire ce type de question. (Pris pour le Stream Programming Guide, Writing To Output Streams )

L'utilisation d'une instance NSOutputStream pour écrire dans un flux de sortie nécessite plusieurs étapes:

  1. Créez et initialisez une instance de NSOutputStream avec un référentiel pour les données écrites. Définissez également un délégué.
  2. Planifiez l'objet de flux sur une boucle d'exécution et ouvrez le flux.
  3. Gérez les événements que l'objet de flux rapporte à son délégué.
  4. Si l'objet de flux a écrit des données dans la mémoire, obtenez les données en demandant la propriété NSStreamDataWrittenToMemoryStreamKey.
  5. Lorsqu'il n'y a plus de données à écrire, supprimez l'objet de flux.

Je recherche l'algorithme le plus compétent qui s'applique à l'écriture de fichiers extrêmement volumineux sur iOS à l'aide de Swift, d'API ou peut-être même de C/ObjC. Je peux transposer l'algorithme en constructions compatibles Swift compatibles.

Nota Bene

Je comprends l'erreur d'information ci-dessous. Il est inclus pour être complet. Cette question demande s'il existe ou non un meilleur algorithme à utiliser pour écrire de gros fichiers sur le disque avec une séquence de dépendances garantie (par exemple les dépendances NSOperation). S'il y en a, veuillez fournir suffisamment d'informations (description/échantillon pour que je puisse reconstruire un code compatible Swift 2.0 compatible). Veuillez indiquer si je manque des informations qui pourraient aider à répondre à la question.

Remarque sur l'extension

J'ai ajouté un gestionnaire de complétion à la base writeToURL pour garantir qu'aucun partage de ressources involontaire ne se produit. Mes tâches dépendantes qui utilisent le fichier ne doivent jamais faire face à une condition de concurrence critique.

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

        }
    }

Cette méthode est utilisée pour traiter les données d'objets personnalisés à partir d'un contrôleur en utilisant:

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

Conclusion

Le Apple Docs on Core Data Performance fournit de bons conseils sur la gestion de la pression mémoire et la gestion des BLOBs. C'est vraiment un sacré article avec beaucoup d'indices pour comportement et comment atténuer le problème des fichiers volumineux dans votre application. Maintenant, bien qu'il soit spécifique aux données de base et non aux fichiers, l'avertissement sur l'écriture atomique me dit que je dois implémenter des méthodes qui écrivent atomiquement avec beaucoup de soin.

Avec des fichiers volumineux, le seul moyen sûr de gérer l'écriture semble être d'ajouter un gestionnaire d'achèvement (à la méthode d'écriture) et d'afficher une vue d'activité sur le thread principal. Que ce soit avec un flux ou en modifiant une API existante pour ajouter une logique d'achèvement, cela dépend du lecteur. J'ai fait les deux dans le passé et je suis en train de tester les meilleures performances.

Jusque-là, je change la solution pour supprimer toutes les propriétés de données binaires de Core Data et les remplacer par des chaînes pour contenir les URL des actifs sur le disque. J'utilise également les fonctionnalités intégrées de la bibliothèque de ressources et de PHAsset pour récupérer et stocker toutes les URL de ressources associées. Quand ou si j'ai besoin de copier des actifs, j'utiliserai des méthodes API standard (méthodes d'exportation sur PHAsset/Asset Library) avec des gestionnaires d'achèvement pour informer l'utilisateur de l'état terminé sur le thread principal.

(Extraits vraiment utiles de l'article Core Data Performance)

Réduction de la surcharge de la mémoire

Il arrive parfois que vous souhaitiez utiliser des objets gérés de manière temporaire, par exemple pour calculer une valeur moyenne pour un attribut particulier. Cela entraîne une augmentation de votre graphique d'objet et de la consommation de mémoire. Vous pouvez réduire la surcharge de mémoire en remodelant les objets gérés individuels dont vous n'avez plus besoin, ou vous pouvez réinitialiser un contexte d'objet géré pour effacer un graphique d'objet entier. Vous pouvez également utiliser des modèles qui s'appliquent à la programmation Cocoa en général.

Vous pouvez redéfinir un objet géré individuel à l'aide de la méthode refreshObject: mergeChanges: de NSManagedObjectContext. Cela a pour effet d'effacer ses valeurs de propriété en mémoire, réduisant ainsi sa surcharge de mémoire. (Notez que ce n'est pas la même chose que de définir les valeurs de propriété sur zéro — les valeurs seront récupérées sur demande si l'erreur est déclenchée — voir Défaillance et Uniquing.)

Lorsque vous créez une demande d'extraction, vous pouvez définir includesPropertyValues ​​sur NO> pour réduire la surcharge de mémoire en évitant la création d'objets pour représenter les valeurs de propriété. Cependant, vous ne devez généralement le faire que si vous êtes sûr que vous n'aurez pas besoin des données de propriété réelles ou que vous avez déjà les informations dans le cache de lignes, sinon vous encourrez plusieurs déplacements vers le magasin persistant.

Vous pouvez utiliser la méthode de réinitialisation de NSManagedObjectContext pour supprimer tous les objets gérés associés à un contexte et "recommencer" comme si vous veniez de le créer. Notez que tout objet géré associé à ce contexte sera invalidé, et vous devrez donc supprimer toutes les références et récupérer à nouveau les objets associés à ce contexte dans lequel vous êtes toujours intéressé. Si vous parcourez de nombreux objets, vous devrez peut-être utiliser des blocs de pool de libération automatique locaux pour vous assurer que les objets temporaires sont désalloués dès que possible.

Si vous n'avez pas l'intention d'utiliser la fonctionnalité d'annulation de Core Data, vous pouvez réduire les besoins en ressources de votre application en définissant le gestionnaire d'annulation du contexte sur zéro. Cela peut être particulièrement avantageux pour les threads de travail en arrière-plan, ainsi que pour les opérations d'importation ou de traitement par lots volumineuses.

Enfin, Core Data ne conserve pas par défaut de références fortes aux objets gérés (sauf s'ils ont des modifications non enregistrées). Si vous avez beaucoup d'objets en mémoire, vous devez déterminer les références propriétaires. Les objets gérés maintiennent des références fortes les uns aux autres grâce à des relations, ce qui peut facilement créer des cycles de référence solides. Vous pouvez interrompre les cycles en remodelant les objets (à nouveau en utilisant la méthode refreshObject: mergeChanges: de NSManagedObjectContext).

Grands objets de données (BLOB)

Si votre application utilise de gros BLOB ("grands objets binaires" tels que des données d'image et de son), vous devez prendre soin de minimiser les frais généraux. La définition exacte de "petit", "modeste" et "grand" est fluide et dépend de l'utilisation d'une application. Une règle générale est que les objets de l'ordre de kilo-octets sont de taille "modeste" et ceux de l'ordre de mégaoctets sont de "grande" taille. Certains développeurs ont obtenu de bonnes performances avec des BLOB de 10 Mo dans une base de données. D'un autre côté, si une application contient des millions de lignes dans une table, même 128 octets peuvent être un CLOB (Character Large OBject) de taille "modeste" qui doit être normalisé dans une table distincte.

En général, si vous devez stocker des BLOB dans un magasin persistant, vous devez utiliser un magasin SQLite. Les magasins XML et binaires nécessitent que le graphique d'objet entier réside en mémoire et les écritures de magasin sont atomiques (voir Fonctions de magasin persistantes), ce qui signifie qu'elles ne traitent pas efficacement les gros objets de données. SQLite peut évoluer pour gérer des bases de données extrêmement volumineuses. Correctement utilisé, SQLite offre de bonnes performances pour les bases de données jusqu'à 100 Go, et une seule ligne peut contenir jusqu'à 1 Go (bien que la lecture de 1 Go de données en mémoire soit une opération coûteuse, quelle que soit l'efficacité du référentiel).

Un BLOB représente souvent un attribut d'une entité. Par exemple, une photographie peut être un attribut d'une entité Employé. Pour les BLOB de taille petite à modeste (et les CLOB), vous devez créer une entité distincte pour les données et créer une relation un à la place de l'attribut. Par exemple, vous pouvez créer des entités Employé et Photographie avec une relation un à un entre elles, où la relation Employé/Photographie remplace l'attribut de photographie de l'employé. Ce modèle maximise les avantages de la défaillance d'objet (voir Défaillance et uniquage). Toute photographie donnée n'est récupérée que si elle est réellement nécessaire (si la relation est traversée).

Cependant, il est préférable de pouvoir stocker des BLOB en tant que ressources sur le système de fichiers et de conserver des liens (tels que des URL ou des chemins) vers ces ressources. Vous pouvez ensuite charger un BLOB au fur et à mesure des besoins.

Remarque:

J'ai déplacé la logique ci-dessous dans le gestionnaire d'achèvement (voir le code ci-dessus) et je ne vois plus d'erreur. Comme mentionné précédemment, cette question vise à savoir s'il existe ou non un moyen plus performant de traiter des fichiers volumineux dans iOS à l'aide de Swift.

Lorsque vous tentez de traiter le tableau d'éléments résultant pour le transmettre à un UIActvityViewController, en utilisant la logique suivante:

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

Je vois l'erreur suivante: Erreur de communication: {count = 1, contents = "XPCErrorDescription" => {longueur = 22, contents = "Connexion interrompue"}}> (veuillez noter, je recherche un meilleur design, pas un réponse à ce message d'erreur)

34
Tommie C.

Les performances dépendent du fait que les données tiennent ou non dans la RAM. Si c'est le cas, vous devez utiliser NSData writeToURL Avec la fonction atomically activée, ce que vous faites.

Les notes d'Apple sur le fait que cela soit dangereux lors de "l'écriture dans un répertoire public" sont complètement hors de propos sur iOS car il n'y a pas de répertoires publics. Cette section ne s'applique qu'à OS X. Et franchement, ce n'est pas vraiment important non plus.

Ainsi, le code que vous avez écrit est aussi efficace que possible tant que la vidéo tient dans RAM (environ 100 Mo serait une limite sûre).

Pour les fichiers qui ne tiennent pas dans la RAM, vous devez utiliser un flux ou votre application se bloquera tout en conservant la vidéo en mémoire. Pour télécharger une grande vidéo à partir d'un serveur et l'écrire sur le disque, vous devez utiliser NSURLSessionDownloadTask.

En général, le streaming (y compris NSURLSessionDownloadTask) sera plus lent que NSData.writeToURL(). N'utilisez donc pas de flux sauf si vous en avez besoin. Toutes les opérations sur NSData sont extrêmement rapides, il est parfaitement capable de traiter des fichiers de plusieurs téraoctets avec d'excellentes performances sur OS X (iOS ne peut évidemment pas avoir des fichiers aussi gros , mais c'est la même classe avec les mêmes performances).


Il y a quelques problèmes dans votre code.

C'est faux:

let filePath = NSTemporaryDirectory() + named

Au lieu de cela, faites toujours:

let filePath = NSTemporaryDirectory().stringByAppendingPathComponent(named)

Mais ce n'est pas idéal non plus, vous devez éviter d'utiliser des chemins (ils sont bogués et lents). Utilisez plutôt une URL comme celle-ci:

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

De plus, vous utilisez un chemin pour vérifier si le fichier existe ... ne faites pas ceci:

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

Utilisez plutôt NSURL pour vérifier s'il existe:

if fileURL.checkResourceIsReachableAndReturnError(nil) {
20
Abhi Beckert

Dernière solution (2018)

Une autre possibilité utile pourrait inclure l'utilisation d'une fermeture chaque fois que le tampon est rempli (ou si vous avez utilisé une durée d'enregistrement temporisée) pour ajouter les données et également pour annoncer la fin du flux de données. En combinaison avec certaines des API Photo, cela pourrait conduire à de bons résultats. Ainsi, du code déclaratif comme ci-dessous pourrait être déclenché pendant le traitement:

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

La gestion de ces fermetures dans votre objet de gestion peut vous permettre de gérer de manière succincte des données de toute taille tout en gardant la mémoire sous contrôle.

Associez cette idée à l'utilisation d'une méthode récursive qui agrège les travaux en un seul groupe dispatch_ et il pourrait y avoir des possibilités intéressantes.

L'état des documents Apple:

DispatchGroup permet une synchronisation globale du travail. Vous pouvez les utiliser pour soumettre plusieurs éléments de travail différents et suivre quand ils sont tous terminés, même s'ils peuvent s'exécuter sur différentes files d'attente. Ce comportement peut être utile lorsque la progression ne peut pas être effectuée tant que toutes les tâches spécifiées ne sont pas terminées.

Autres solutions remarquables (~ 2016)

Je ne doute pas que je vais affiner cela un peu plus, mais le sujet est suffisamment complexe pour justifier une auto-réponse distincte. J'ai décidé de prendre quelques conseils des autres réponses et de tirer parti des sous-classes NSStream. Cette solution est basée sur un Obj-C exemple ( NSInputStream inputStreamWithURL exemple ios, 2013, 12 mai) publié sur le blog SampleCodeBank .

La documentation Apple note qu'avec une sous-classe NSStream vous n'avez PAS à charger toutes les données en mémoire à la fois . C'est la clé pour pouvoir gérer des fichiers multimédias de toute taille (ne dépassant pas le disque disponible ou RAM espace).

NSStream est une classe abstraite pour les objets représentant des flux. Son interface est commune à toutes les classes de flux Cocoa, y compris ses sous-classes concrètes NSInputStream et NSOutputStream.

Les objets NSStream offrent un moyen simple de lire et d'écrire des données vers et depuis une variété de supports de manière indépendante du périphérique. Vous pouvez créer des objets de flux pour les données situées en mémoire, dans un fichier ou sur un réseau (à l'aide de sockets), et vous pouvez utiliser des objets de flux sans charger toutes les données en mémoire à la fois.

Guide de programmation du système de fichiers

L'article Traitement d'un fichier entier à l'aide de flux d'Apple dans le FSPG a également fourni l'idée que NSInputStream et NSOutputStream devraient être intrinsèquement thread-safe.

file-processing-with-streams

Améliorations supplémentaires

Cet objet n'utilise pas de méthodes de délégation de flux. Beaucoup de place pour d'autres raffinements également, mais c'est l'approche de base que je vais adopter. L'objectif principal de l'iPhone est de permettre la gestion des fichiers volumineux tout en contraignant la mémoire via un tampon ( TBD - Tirez parti du tampon en mémoire outputStream ). Pour être clair, Apple mentionne que leurs fonctions pratiques qui écriventToURL ne sont que pour des fichiers de plus petite taille (mais je me demande pourquoi ils ne prennent pas soin des fichiers plus volumineux - ce ne sont pas des cas Edge , note - déposera la question en tant que bug).

Conclusion

Je vais devoir tester plus avant l'intégration sur un thread d'arrière-plan car je ne veux pas interférer avec les files d'attente internes de NSStream. J'ai d'autres objets qui utilisent des idées similaires pour gérer des fichiers de données extrêmement volumineux sur le fil. La meilleure méthode consiste à conserver des tailles de fichier aussi petites que possible dans iOS pour conserver la mémoire et éviter les plantages d'applications. Les API sont construites en tenant compte de ces contraintes (c'est pourquoi tenter une vidéo illimitée n'est pas une bonne idée), donc je vais devoir adapter les attentes dans l'ensemble.

( Gist Source , Vérifiez Gist pour les dernières modifications)

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
    }


}

Délégation

Voici un objet similaire que j'ai réécrit à partir d'un article sur Advanced File I/O en arrière-plan , Eidhof, C., ObjC.io ). Avec seulement quelques ajustements, cela pourrait être fait pour émuler le comportement ci-dessus. Redirigez simplement les données vers un NSOutputStream dans la méthode processDataChunk.

( Gist Source - Vérifiez Gist pour les dernières modifications)

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)
            }
        }
    }
}
6
Tommie C.

Vous devriez envisager d'utiliser NSStream (NSOutputStream/NSInputStream). Si vous choisissez cette approche, gardez à l'esprit que la boucle d'exécution du thread d'arrière-plan devra être démarrée (exécutée) de manière explicite.

NSOutputStream a une méthode appelée outputStreamToFileAtPath:append: c'est ce que vous cherchez peut-être.

Question similaire:

Écriture d'une chaîne dans un NSOutputStream dans Swift

2
Anand