[Swift]Lecture aléatoire NSData dans un fichier

J'ai voulu faire la chose suivante : sauver un tableau de NSDatas dans un fichier, et ensuite relire une DATA spécifique sans recharger toutes les données en mémoire. Bref, faire ce qu'on appelait accès aléatoire à  une époque.


 


J'ai adopté ce format de fichier :


 


- Un entête  - la chaà®ne alphanumérique "Datas01.00".


- Un UInt32 contenant le nombre de NSData dans le fichier


- Une série d'UInt32 contenant la taille et l'offset des NSDatas dans le fichier


- Les NSDatas


 


J'ai une classe CreateStore pour créer un fichier de Datas.



let idFormat = "Datas01.00"

class CreateStore {
func create(array:[NSData], name:String) -> Bool {

// Tableau de NSRange
var arrayRange = [NSRange]()
var offset = 0
for data in array {
// Stockage taille et position
let size = data.length
var range = NSRange()
range.location = offset
range.length = size
arrayRange.append(range)
offset += size
}

let documentsURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
let fileURL = documentsURL.URLByAppendingPathComponent(name)

// Création fichier vide
let dataEmpty = " ".dataUsingEncoding(NSUTF8StringEncoding)
dataEmpty?.writeToURL(fileURL, atomically: false)

// Création fichier
let file = try? NSFileHandle(forWritingToURL: fileURL)
if let file = file {

// Enregistrement version du format
let dataFormat = idFormat.dataUsingEncoding(NSUTF8StringEncoding)
file.writeData(dataFormat!)

// Enregistrement nombre de NSData
let numbersOfData = UInt32(array.count)
file.writeUInt32(numbersOfData)

// Enregistrement taille et offset des NSDatas
for range in arrayRange {
let location = UInt32(range.location)
let length = UInt32(range.length)
file.writeUInt32(location)
file.writeUInt32(length)
}

// Enregistrement des NSData
for data in array {
file.writeData(data)
}

file.closeFile()
return true

}

return false

}

}


Exemple d'utilisation :



let array = ["Sushis", "Maki", "Wasabi Power", "Saumons", "avocats"]
let arrayDatas = encodeStrings(array)

let fileName = "sushis.bin"
let createFile = CreateStore()
createFile.create(arrayDatas, name: fileName)


encodeStriings est une moulinette pour transformer un tableau de String en tableau de NSDatas, pour le test.



func encodeStrings(array :[String]) -> [NSData] {
var arrayData = [NSData]()
for str in array {
let data = str.dataUsingEncoding(NSUTF8StringEncoding)
arrayData.append(data!)
}
return arrayData
}



 


Réponses

  • Le fichier se lit avec la classe DatasStore. Exemple :



    let fileName = "sushis.bin"
    let store = DatasStore(fileName: fileName)
    if let store = store {
    // Lecture nombre objets
    print (store.count)
    // Lecture NSData numéro 2
    let data = store.getData(2)
    let str = String(data: data!, encoding: NSUTF8StringEncoding)
    print (str)
    }



    Le source de la classe :



    class DatasStore {

    var count = 0
    private var fileStore:NSFileHandle?
    private var offsetArrayRanges = 0
    private var offsetArrayDatas = 0

    init?(fileName:String) {
    let documentsURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
    let fileURL = documentsURL.URLByAppendingPathComponent(fileName)

    fileStore = try? NSFileHandle(forReadingFromURL: fileURL)
    if let fileStore = fileStore {
    // Vérification format
    let sizeEntete:Int = (idFormat.dataUsingEncoding(NSUTF8StringEncoding)?.length)!
    let format = fileStore.readDataOfLength(sizeEntete)
    let idString = String(data: format, encoding: NSUTF8StringEncoding)
    guard idString == idFormat else {
    print (fileName, " : format de fichier inconnu..")
    return nil
    }

    // Lecture nombre Datas
    self.count = (Int)(fileStore.readUInt32())
    print ("self.count : ", self.count)

    // Début table des Ranges (aprés le format et le nombre d'objets)
    self.offsetArrayRanges = sizeEntete + sizeof(UInt32)
    // Début table des Datas
    self.offsetArrayDatas = self.offsetArrayRanges + self.count*(2*sizeof(UInt32))

    } else { return nil }

    }

    // Fermeture du fichier
    deinit {
    fileStore?.closeFile()
    }

    func getData(n:Int) -> NSData? {
    if n>=self.count {
    return nil
    }

    // Lecture taille et Offset
    let offsetRange = UInt64(self.offsetArrayRanges + 2*n*sizeof(UInt32))
    fileStore?.seekToFileOffset(offsetRange)

    let location:Int? = Int((fileStore?.readUInt32())!)
    let length:Int? = Int((fileStore?.readUInt32())!)

    if let location = location, length = length {
    let offsetData = UInt64(self.offsetArrayDatas + location)
    fileStore?.seekToFileOffset(offsetData)
    let data = fileStore?.readDataOfLength(length)
    if let data = data {
    return data
    }

    }
    return nil
    }
    }

  • DrakenDraken Membre
    février 2016 modifié #3

    ça marche, mais je me pose des questions sur le fonctionnement du système.


     


    Pour commencer, je maintiens ouvert en permanence un NSFileHandle ne disparaissant qu'à  la destruction de l'objet DatasStore (en utilisant le deinit). Je me demande s'il ne serait pas plus approprié d'ouvrir et de fermer le fichier à  chaque lecture d'un objet. Mais cela risque peut-être d'augmenter le temps de lecture des données. Quoi que iOS doit certainement garder un cache sur les fichiers utilisés récemment.


     


    C'est un premier jet. Vous avez certainement des remarques à  faire sur le code et ma manière de procéder.


     


    Quid des fichiers ouverts quand iOS plonge une application en background, avant de la réactiver ? Je présume qu'il faut fermer les fichiers pour éviter les problèmes (ce que mon code ne gère pas du tout pour le moment).


     


    Il faut aussi que j'ajoute un NSCache pour éviter de recharger inutilement des données.


     


    EDIT : Comme je l'ai dis dans un autre post, l'idée est d'utiliser un ViewCollection pour afficher une série de fiches, sans charger la totalité des informations en mémoire, juste ce qui est nécessaire pour une fiche bien précise.

  • Oups, j'ai oublié d'ajouter le source des extensions facilitant la convention UInit32 <=> NSData et la lecture/écriture de UInt32 dans un fichier binaire.



    extension NSData {
    var uint32: UInt32 {
    let pointer = UnsafePointer<UInt32>(self.bytes)
    let buffer = UnsafeBufferPointer<UInt32>(start:pointer, count:1)
    let array = [UInt32](buffer)
    return array[0]
    }
    }

    extension UInt32 {
    var data:NSData {
    let array:[UInt32] = [self]
    let data = NSData(bytes: array, length: sizeof(UInt32))
    return data
    }
    }

    extension NSFileHandle {
    func writeUInt32(value:UInt32) {
    let data = value.data
    writeData(data)
    }
    }

    extension NSFileHandle {
    func readUInt32() -> UInt32 {
    let data = self.readDataOfLength(sizeof(UInt32))
    return data.uint32
    }
    }

  • Ouais. ça a l'air de tourner comme code mais j'ai une question: pourquoi ?


     


    - Si tu me réponds que c'est pour permettre une lecture aléatoire dans des gros fichiers je te dirai d'utiliser du SQLite qui fera du bien meilleur boulot.


    - Si tu me réponds que Swift a été porté sur ZxSpectrum je te dirai que c'est cool ! 


     


    ApreÌ€s pour ce qui est du produit en dehors de ces considérations je trouve dommage que dans ta quête du stockage le plus restreint possible tu te retrouve à  stocker n fois les headers de NSData. 


     


    En plus ça c'est vraiment pour les données statiques. Ajout/modification s'apparentera à  frapper aux portes de l'enfer.


     


    D'autant que, vu ce que tu veux faire, CoreData avec son lazy-loading fera un bien meilleur travail et sera plus facile à  maintenir/mettre à  jour. Ou Realm aussi, je m'y suis un peu moins intéressé. Je pense qu'il est bon en 2016 de s'orienter vers ce genre de technologies plutôt que de vouloir conserver la compatibilité DOS.... ( ;))




  •  


    - Si tu me réponds que c'est pour permettre une lecture aléatoire dans des gros fichiers je te dirai d'utiliser du SQLite qui fera du bien meilleur boulot.


     




    Probablement, mais je ne connait rien à  SQLite. Et je n'ai pas envie de passer des semaines ou des mois à  apprendre une nouvelle technologie, pour une seule chose : accéder de manière aléatoire à  un gros fichier de NSDATA. 


     


     




    ApreÌ€s pour ce qui est du produit en dehors de ces considérations je trouve dommage que dans ta quête du stockage le plus restreint possible tu te retrouve à  stocker n fois les headers de NSData. 




    Quelque que soit le système de stockage il faut bien conserver les headers des DATA, pour les lire de manière aléatoire. Et chaque header n'est stocké qu'une seule fois dans le fichier, pas n fois. Il y a aussi des headers dans un NSDictonary, même si tu n'y a pas accés.


     


     




    En plus ça c'est vraiment pour les données statiques.


     




    C'est prévu pour des données purement statique, généré sur Mac, puis stocké sur un serveur, afin de pouvoir être téléchargé. Ou peut-être bien stocké dans un inapp-purchase.


     


     




     


    D'autant que, vu ce que tu veux faire, CoreData avec son lazy-loading fera un bien meilleur travail et sera plus facile à  maintenir/mettre à  jour. Ou Realm aussi, je m'y suis un peu moins intéressé.




     


    Si j'ai bien compris CoreData permet de gérer une persistance des données au sein d'une application, pas de gérer plusieurs bases de données (des fichiers différents), pouvant être récupérés au fur et à  mesure du temps.


     


     




    Je pense qu'il est bon en 2016 de s'orienter vers ce genre de technologies plutôt que de vouloir conserver la compatibilité DOS.... ( ;))




     


    Oui, si je voulais devenir développeur iOS professionnel, acquérir pleins de connaissances sur des technologies et vivre de mon travail en créant des tas d'applications, ce qui n'est pas mon but. 

  • Oui j'entends bien tout ça. Mais y'a quand meÌ‚me un truc qui me chiffonne un peu, là  c'est un peu comme si tu nous disait: "J'ai 20 meÌ€tres de bois à  abattre mais je vais le faire à  la hache uniquement, j'suis pas bucheron professionnel moi.".


     


    Si on part du principe que tu fais ça par passion rien ne t'empêche d'apprendre au passage. Mais quand je vois l'énergie que tu emploi dans tes recherche tu peux employer cette énergie pour apprendre.


     


    La seule différence au final est que tu devras sortir de ta zone de confort. Mais je ne t'apprends rien en te disant que tu trouvera toujours de l'aide ici et plus facilement pour des technologies nouvelles que pour les fichiers binaires ou indexés avec un backend en Cobol.


     


    Et au passage si tu veux utiliser Realm ou CoreData pour un cas aussi simple que le tien c'est pas des semaines qu'il va te falloir mais deux/trois aprem/soirées. T'as les connaissances suffisantes pour tout comprendre et c'est dommage que tu passe à  coÌ‚té de ça. D'autant que le SDK iOS n'est pas adapté aux choses que tu veux faire.


     


    Voilà  mon avis, je ne t'embêterai pas plus, promis.


  • Si j'ai bien compris CoreData permet de gérer une persistance des données au sein d'une application, pas de gérer plusieurs bases de données (des fichiers différents), pouvant être récupérés au fur et à  mesure du temps.


    Il est possible de travailler sur plusieurs bases Core Data simultanément ou de merger des données d'une base dans une autre, mais ce n'est pas trivial.
  • DrakenDraken Membre
    février 2016 modifié #9


    Oui j'entends bien tout ça. Mais y'a quand meÌ‚me un truc qui me chiffonne un peu, là  c'est un peu comme si tu nous disait: "J'ai 20 meÌ€tres de bois à  abattre mais je vais le faire à  la hache uniquement, j'suis pas bucheron professionnel moi.".


     




    Combien de bois doit abattre un bucheron professionnel pour acquérir son titre pendant sa formation ? 


     


     


     


     




    Il est possible de travailler sur plusieurs bases Core Data simultanément ou de merger des données d'une base dans une autre, mais ce n'est pas trivial.




     


    B)

  • D'autant que le SDK iOS n'est pas adapté aux choses que tu veux faire.


    Que ce soit Swift ou Foundation, les deux sont tout à  fait adaptés. Cf. la présence de NSFileHandle ou de UnsafePointer<>, etc.


    Les données étant statiques, je trouve que ce n'est pas un mauvais choix de ne pas utiliser Core Data.
    Dans les critères de choix, les adhérences à  des librairies qui en font plus que nécessaire doivent être prises en compte.


  •  


    J'ai voulu faire la chose suivante : sauver un tableau de NSDatas dans un fichier, et ensuite relire une DATA spécifique sans recharger toutes les données en mémoire. Bref, faire ce qu'on appelait accès aléatoire à  une époque.


     




     


    Tu peux peut être tirer partie des options de gestion de mémoire virtuelle lors de la création du NSData représentant ton fichier (DataReadingMappedIfSafe, DataReadingMappedAlways). Si tu conserves la structure de fichier que tu as défini cela ne va pas enlever la gestion des offsets quit doit être réaliser, mais cela peut éviter d'avoir à  utiliser les NSFileHandle.

  • Tu veux dire, lire l'intégralité du fichier dans un NSData en mode mémoire virtuelle avec ces deux attributs, puis utiliser subdataWithRange pour récupérer les données ?


     


    Je ne vois pas trop la différence avec NSFileHandle. Dans les deux cas, le système maintient un fichier ouvert en permanence. 

  • Pour le moment, je vais me contenter d'ouvrir et de fermer le fichier à  chaque lecture d'un NSData, en conservant son URL. Je verrais bien si c'est suffisant à  l'usage. 



    class DatasStore {

    var count = 0
    private var offsetArrayRanges = 0
    private var offsetArrayDatas = 0
    private var fileURL = NSURL()

    init?(fileName:String) {
    let documentsURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
    fileURL = documentsURL.URLByAppendingPathComponent(fileName)

    let fileStore = try? NSFileHandle(forReadingFromURL: fileURL)
    if let fileStore = fileStore {
    // Vérification format
    let sizeEntete:Int = (idFormat.dataUsingEncoding(NSUTF8StringEncoding)?.length)!
    let format = fileStore.readDataOfLength(sizeEntete)
    let idString = String(data: format, encoding: NSUTF8StringEncoding)
    guard idString == idFormat else {
    print (fileName, " : format de fichier inconnu..")
    return nil
    }

    // Lecture nombre Datas
    self.count = (Int)(fileStore.readUInt32())

    // Début table des Ranges (aprés le format et le nombre d'objets)
    self.offsetArrayRanges = sizeEntete + sizeof(UInt32)
    // Début table des Datas
    self.offsetArrayDatas = self.offsetArrayRanges + self.count*(2*sizeof(UInt32))
    fileStore.closeFile()
    } else { return nil }

    }

    func getData(n:Int) -> NSData? {
    if n>=self.count {
    return nil
    }

    let fileStore = try? NSFileHandle(forReadingFromURL: self.fileURL)
    if let fileStore = fileStore {
    // Lecture taille et Offset
    let offsetRange = UInt64(self.offsetArrayRanges + 2*n*sizeof(UInt32))
    fileStore.seekToFileOffset(offsetRange)

    let location:Int? = Int(fileStore.readUInt32())
    let length:Int? = Int(fileStore.readUInt32())

    if let location = location, length = length {
    let offsetData = UInt64(self.offsetArrayDatas + location)
    fileStore.seekToFileOffset(offsetData)
    let data:NSData? = fileStore.readDataOfLength(length)
    fileStore.closeFile()
    if data != nil {
    return data
    }
    }
    }
    return nil
    }
    }

  • AliGatorAliGator Membre, Modérateur
    J'ai l'impression que tu cherches à  vraiment réinventer la roue...

    A force de vouloir faire toi-même ce stockage dans un fichier de façon la plus compact possible à  la main octet par octet (dans l'autre thread) et maintenant à  le relire de manière aléatoire... et à  devoir tout re-coder toi-même du coup et surtout à  ensuite te casser les dents sur des problèmes (File Handle toujours ouvert, pas de Mapping, ...) qui ont été résolus depuis belle lurette par les systèmes existants...

    Tu te prends bien le chou tout ça pour éviter d'apprendre un truc existant ou pour éviter d'utiliser un truc déjà  existant et éprouvé comme Realm (où il n'y a aucun besoin de s'y connaà®tre en BDD, SQL ou autre pour l'utiliser) ou autre.

    Tu vas perdre énormément plus de temps à  réinventer la roue toi-même + rencontrer des bugs que tu n'avais pas anticipés et des problématiques auxquelles tu n'avais pas pensé de prime abord (NSFileHandle open, ...), alors que tu passerais bien moins de temps, et en plus ce temps serait bien plus bénéfique, à  apprendre un framework qui fait déjà  le taf (je te parle de Realm car il est 100x plus simple à  prendre en main que CoreData ou SQLLite, mais ce n'est pas forcément la seule solution possible), et en plus tu en profiterais pour apprendre de nouvelles choses...
  • * se planque derrière un tas de sable *

  • @Ali : realm.io c'est forcément que sur le device ? (ou il est possible d'utiliser ca un peu comme Parse)


  • Pour l'instant realm n'est pas dispo sur Linux (et n'est pas complètement open-source), ce qui limite les utilisations côté serveur (tu peux l'utiliser sur un serveur sur OSX avec un back-end perso).

    Je pense qu'ils doivent être en train de faire un portage sous linux pour une utilisation avec swift.

    Après je ne sais pas si realm est adapté pour une utilisation sur serveur avec potentiellement beaucoup de connexions en parallèle.
  • AliGatorAliGator Membre, Modérateur
    Realm est effectivement surtout pensé pour un usage similaire à  SQLite (mais en beaucoup plus performant !! Je peux le confirmer on a une app qui gère des millions de produits la diff entre CoreData/SQLite et Realm est juste ouf), c'est à  dire embarqué sur le mobile.

    Donc effectivement ça n'a pas à  l'origine été créé pour faire du clouding à  la Parse. Si tu as besoin d'une base serveur pars plutôt sur des concurrents à  Parse (genre Firebase & co).

    Par contre si tu as besoin d'une base locale, genre pour gérer du cache et mode hors ligne, ou pour sérialiser des données et les enregistrer sur le disque de ton device pour les réouvrir plus tard, etc... alors Realm est exactement fait pour.
Connectez-vous ou Inscrivez-vous pour répondre.