[Résolu] Serialisation, Plist
Bonjour, à tous
Voilà ça fait un moment que je n'ai pas touché au développement iOS donc je suis un peu rouillé. J'ai cherché sur votre forum un "problème similaire" mais je n'ai rien trouvé.
Voilà le fonctionnement de l'application :
En fait je suis en train de faire une application assez basique qui se connecte à une page web, récupère un résultat (sous format xml) dans un NSData (à travers NSUrlConnection ...).
Voilà je voudrais sérialiser des objets (de la même classe : classe personnalisée) : mais j'ai eu toujours un difficultés au niveau de l'optimisation du code et de la mémoire.
Car moi mon raisonnement actuelle c'est
i = 0
pour chaque objet dans mon Array
sauvegarder(objet,"objet%d.xml",i)
i++
fin pour
Mais du coup si j'ai 1000 objets je vais devoir tous les sauvegarder. Ni aurais t'il pas un moyen de stocker je sais pas un NSDictionnary par exemple ? En gros les grouper dans un seul fichier serialiser. Dans une seul PLIST
PS : c'est ma troisième demande en moins de 20 minutes je m'en excuse mais bon ça servira à des personnes dans un futur proche
Réponses
Je t'invite à lire le Archives and Serialization Programming Guide qui explique très bien toutes les techniques pour sérialiser tout un arbre d'objets.
Pour résumer je pense que dans ton cas implémenter le protocole NSCoder est la solution. Et du coup une fois que tu auras un NSArray de tes objets à sérialiser tu utilises [NSKeyedArchiver archiveRootObject:tonArrayDObjets toFile] et basta !
Tout est prévu dans iOS pour archiver facilement des (graphes d')objets donc autant utiliser le système tout fait :-)
Merci.
Ok je me renseigne du coup.
Mais du coup que conseil-tu Property List, NSCoder, Core Data et pourquoi ?
En fait j'aime bien quand l'api est simple et clair et quand je vais sur une page comme le lien que tu m'a passé je me perd dans la doc (je comprend l'anglais : niveau moyen) mais je suis habitué à la java doc qui n'a rien à voir.Question d'habitude
Les PLIST c'est bien si tes données à stockées sont parmi les types autorisés dans les PLIST (String, Date, Data, ... je n'ai plus la liste complète en tête, elle est indiquée dans le PG, mais elle n'est pas bien longue).
Du coup pour archiver par exemple un NSDictionary c'est bien, mais pour archiver un NSObject avec des @property, faut le convertir en NSDictionary puis stocker ce NSDictionary dans un PLIST (et recréer le NSObject à partir du NSDictionary lors de la relecture). Par contre l'avantage c'est que c'est lisible ensuite par un éditeur de texte ou en ouvrant le fichier PLIST dans Xcode.
Du coup pour des données (préférences, listes de valeurs à charger comme des constantes dans ton appli, etc) c'est bien, mais pour un arbre d'objets plus complexes et qui sont plutôt des NSObject que des NSDictionary, c'est pas la solution adaptée.
Dans ce cas il faut mieux passer par NSKeyedArchiver / NSKeyedUnarchiver (et implémenter le protocole NSCoder sur tes classes). C'est pas dur (et très bien expliqué dans le PG) et iOS gère ensuite tout tout seul (sérialisation, sauvegarde dans le fichier, éviter les redondances en cas de cycle dans ton arbre d'objets comme A qui référence B qui référence C qui référence A, ...)
Après, tout cela c'est pour de la sérialisation sur disque de données, c'est plus orienté aussi pour quand tu veux sauvegarder des objets sur disque un peu comme tu sauvegarderais un document Word dans ton disque dur (niveau concept fonctionnel).
Mais si tu veux travailler avec un vrai modèle de données (MCD - Modèle de Conception de Données), que tu as pensé façon diagramme UML, donc qui gère déjà les relations entre les différents objets, qui gère les contraintes entre ces relations, si A peut être lié à plusieurs B ou à un seul, le fait que si A est détruit, est ce que les objets B qu'il contient / auxquels il est lié doivent être supprimés automatiquement aussi (relation de type "contenant") ou ont-ils leur vie propre (relation de type "référence"), et qui persiste entre les lancement de l'application (car en effet sous le capot il est sérialisé dans une base mais ça c'est un détail d'implémentation), alors oui CoreData est la solution qu'il te faut.
Vu la description de ton application et de ton fonctionnel, d'ailleurs c'est effectivement bien + CoreData que je te conseille, comme ça tu auras tous ses avantages, à savoir :
- Un éditeur de modèle sympa, avec une représentation graphique des différentes entités
- Une gestion des relations entre les entités, leur cardinalité, leur delete rule, etc
- Tout le mécanisme de validation des objets
- Le concept de "contexte", un peu comme des transactions SQL ou des branches GIT, permettant par exemple de créer un contexte CoreData le temps de parser ton JSON et de créer tes objets dans ce contexte, et de sauver tout le contexte d'un coup en base ensuite quand tu es sûr que ton JSON a fini d'être parsé et qu'il n'a pas d'erreur (ou de ne pas sauver le contexte et de le jeter à la poubelle si tu te rends compte à la fin de ton parsing que ton JSON est invalide, qui sait)
- Tu peux faire des requêtes sur tes données, un peu comme des requêtes SQL. Genre si tu manipules des Voitures et des Personnes, tu peux faire une requête pour demander toutes les personnes dont le nom commence par A, ou toutes les voitures qui appartiennent à des personnes de sexe masculin, etc.
- Et plein d'autres choses cool dans CoreData
L'avantage de CoreData, surtout si tu l'utilises avec MagicalRecord, c'est qu'une fois que tu as bien compris son principe de fonctionnement, tout le reste est géré pour toi. Toi tu manipules juste des objets (ce ne sont pas des NSObject mais des NSManagedObjects, mais bon c'est le même principe, ils ont des propriétés, tu les manipules comme tu manipules les objets métier que tu comptais créer si tu ne faisais pas de CoreData) et tu appelles "save" et ça sauve les données.Après, CoreData peut faire peur au début car il y a beaucoup de concepts à appréhender d'un coup, moi même j'ai mis du temps à m'y mettre. Jusqu'au jour où j'ai découvert MagicalRecord, qui m'a simplifié la vie à un point inimaginable, rendant l'utilisation de CoreData enfantine.
Je t'invite fortement à regarder ma présentation des CocoaHeads Rennes #13 à ce sujet, qui te permettra d'appréhender tout cela, et de commander à utiliser CoreData sans toute la complexité qu'il y a à apprendre autour avant, grâce à MagicalRecord qui cache toute sa complexité.
J'ai lu l'explication et je viens peut-être de comprendre vraiment l'avantage de CoreData
Donc ce que tu es en train de me dire c'est que je peux dire :
Par exemple la relation : Personne a N voitures et Une voiture appartient à 1 personne (pour simplifier)
Du coup je peux mettre une règle pour dire si je supprimer Personne : je supprimer les N voiture aussi ?
Sachant que j'ai de bonne connaissance en UML, SQL : du coup ça devrait pas être si difficile non ?
Bon je vais regarder la présentation CocoaHeads
_________________
MAJ :
Mais en revenant sur l'exemple des voitures et personnes :
Si je défini ma class person :
Et que je me contente de les stocker dans une NSArray puis de serialiser avec NSPropertySerialization
(Vu que sur la doc sur la page NSPropertySerialization Apple parle de NSData, NSString, NSArray, NSDictionary, NSDate)
Ce n'est pas plus simple au final ? Que de définir des règles etc ... ?
Oui.
Il y a les règles sur les suppressions. Le delete cascade par exemple, ou autre en fonction de tes besoins (si par exemple supprimer une voiture n'implique pas de supprimer la personne).
J'en avais déjà parlé rapidement ici :
http://forum.cocoacafe.fr/topic/12030-magicalrecord-delete-withwithout-cascade/#entry114172
J'avais quoté rapidement les 4 règles.
Ah carrément ok. Mais moi je n'aurai pas besoin de supprimer les voitures dans mon application (même si ce ne sont pas des voitures en réalité bref) mais je saurai amené à en rajouter par contre
PS : J'ai mis à jour mon commentaire au dessus
Bon je m'autorise à remonter le sujet :P
J'ai effectué un NSCoding donc avec les KeyArchive et KeyUnArchive de mon singleton (Cf l'exemple avec des Articles dans mon post précédent).
Voilà en gros mon singleton contient une liste NSMutableSet, plus particulièrement, d'Articles : et un article contient aussi un objet imaginez un objet Toto (c'est difficile je sais).
Donc j'ai dit que mon singleton pouvait :
Jusque la vous me suivez ?
Ensuite je me suis rappelé dans le passé avoir eu un problème : bref le truc c'était que je devais dire que mes objets (articles puis toto) doivent aussi implémenter les 2 méthodes encode et decode
C'est ce que j'ai fait : j'ai fait la même chose pour Article et la même chose pour Toto.
Mais je me retrouve avec cette jolie erreur quand je relance l'application. Sachant qu'à l'ouverture de l'application j'ouvre le fichier ou a été sauvegarder mon singleton (normalement j'ai fait un test et la fonction d'écriture me retourne que tout c'est bien passé mais pour la fonction de lecture c'est l'inverse)
Voilà comment je lis puis j'écris.
PS : Alors dans mon objet Article en théorie j'ai (15 NSString, 1 NSMutableString, 1 NSMutableDictionnary, 2 UIImage) et mon objet Toto (14 NSString et 1 BOOL)
​Si c'est un problème de taille mémoire j'aurai un peu la >:D
Ah oui vous allez me dire pourquoi sauvegarde-tu le singleton surement.
Car il contient bah mes articles qui ont été sélectionnés par l'utilisateur précédemment et je veux que quand il ouvre son appli il les retrouve. Rien de plus simple
Le classique (pour moi) : mais j'en suis sur c'est du à elles l'erreurs.
et à la lecture
Vu que ça renvoi un NSData peut-être ou surement ça fait n'importe quoi du coup mon code la
Avec Core Data tu aurais facilement pu sauver une UIImage de type transformable, qui effectivement permet des stocker des NSData à partir d'image. Autre détail en faveur de Core data, il ne me semble pas que tu puisses faire de sauvegarde incrémmentale avec NSCoding. En gros à chaque fois que tu sauves ça écrase puis re- sauvegarde toutes les données, ça va quand t'en en as 4/5, mais à partir du moment où il faut écraser et re-sauver des données lourdes comme des images, tes performances dans l'app peuvent s'en faire ressentir.
Oui je comprend pour la sauvegarde. Pour le moment je fait une sauvegarde par action effectuée. Mais c'est juste pour tester et débuger. Mais je ferai un sauvegarde quand l'utilisateur aura appuyé sur le bouton home pour sortir de l'application.
Bref. Du coup je suis forcé de passer par la sauvegarde ?
Personnellement ça me va car je les sauvegarde déjà sous le format jpg dans mon NSHomeDirectory/Documents donc du coup mon appli retrouve l'image associé à chaque objet.
Bref j'ai résolu mon problème déjà en enlevant mon UIIMage du NSCoding. Par la suite j'ai découvert qu'il y avait une boucle infini vraiment bizarre. Je faisais appel à ma fonction de lecture dans ma fonction init
Du coup j'ai plutôt fait appel à la fonction dans sharedInstance pour le moment. Et ça marche beaucoup mieux
Par contre je ne comprend pas un truc. Ca marche mais ça m'intrigue.
Juste en exécutant cette ligne à la lecture
dans ma classe Singleton fait une initialisation de mon objet ? Car c'est ce qu'il se passe là et je suis SURPRIS ???
Car moi je me voyais plus faire un truc du genre :
2) évidemment que quand tu unarchive ton archive ça va instancier et appeller le init (ou plutôt le initWithCoder) de chaque objet. C'est le but même de la désérialisation, c'est de recréer les objets en mémoire donc les réinstancier et restaurer les valeurs sauvegardées sur ces nouvelles instances ;-)
1) Ok je vais me renseigner sur ça : j'avais vu en plus la fonction PNG sur le net hier. Je vais me pencher dessus.
2) D'accord donc j'ai pas besoins d'écrire plein de ligne car le unArchiver fait appel au init automatiquement. Donc pas besoin de la méthode copy etc ...
Le NSKeyedUnarchiver va créer de nouvelles instances des classes que tu as archivées et va appeler son initialiseur spécialisé "initWithCoder:" (au lieu du init classique) pour te donner l'opportunité de restaurer les valeurs des propriétés que tu avais précédemment archivées.
Ces objets que le NSKeyedUnarchiver créé (réinstancie) c'est déjà des copies conformes des objets que tu avais sérialisés/archivés donc pourquoi tu courais ensuite faire ton truc chelou de copyFromObject ou je ne sais quoi ?!
T'as lu le Archives and Serialization Programming Guide ?
En fait j'ai lu sur NSHipster j'aurai du surement regarder la doc.
Oui ça j'ai compris que le keyUnarchiver fait appel à
Mais c'est juste que ça m'a surpris quand on encode on précise l'objet dans la fonction : Normal.
Mais quand on décode on ne lui passe pas l'objet en paramètre et il se débrouille pour savoir que c'est sur mon Singleton.
Un peu logique aussi si j'appel la fonction dans mon Singleton.m
Non, c'est juste que quand tu sérialises et crée ton archive, il stocke aussi dans l'archive le type (la classe) de l'objet, justement pour pouvoir la recréer tout seul quand tu désarchive.
Si tu veux plus de sécurité, et indiquer par code la classe que tu attends pour être sûr qu'il n'y aura pas de problème ou de crash (au cas où tu attendrais un NSArray mais qu'en fait dans l'archive c'est indiqué que c'est un NSDictionary par exemple, car un petit malin a altéré ton archive ou parce que tu as fait une erreur dans ton code), tu peux utiliser NSSecureCoding à la limite.
Sinon c'est plutôt une très mauvaise idée que de désérialiser ton Singleton lui-même dans le init de ce dernier (de désérialiser un objet A dans la méthode init de la classe A elle-même) : tu risques fort d'avoir une belle reccursivité (d'où ta boucle infinie plus haut d'ailleurs, j'imagine fort bien). Car dans le init de ton singleton, tu appelles NSKeyedUnarchiver, qui va créer une nouvelle instance de Singleton, qui va appeler son init, qui va appeler le NSKeyedUnarchiver, qui va créer une nouvelle instance de Singleton, qui va appeler init, ... etc
Non en général :
- Dans le cas où ce n'est pas un singleton, on sérialise tout l'objet, et on le recrée par exemple dans le application:DidFinishLaunchingWithOptions: ou un truc comme ça, ou désérialiser chaque objet dans l'objet parent qui est sensé le contenir (dans le init d'une classe "Garage", tu désérialiserais ton tableau de voitures pour l'affecter à sa "@property NSArray* voitures" par exemple), mais certainement pas dans le init dudit objet, sinon forcément... boucle infinie
- Dans le cas où c'est un singleton c'est un peu particulier, tu ne vas pas sérialiser le singleton lui-même car si tu veux ne garantir qu'il n'y ait qu'une seule instance de ton singleton (la sharedInstance), ça va pas le faire. En effet tu risques d'appeler la méthode sharedInstance (qui va s'assurer, au moyen d'un dispatch_once(...), qu'il n'initialise ton instance qu'une seule fois même si tu appelles sharedInstance 36 fois) d'un côté, et le NSKeyedUnarchiver lui il s'en fout de la méthode sharedInstance (ou sharedModel ou sharedService ou tu l'as appelé comme tu veux) lui il ne connait pas cette méthode et il va appeler directement initWithCoder, sas se soucier de l'unicité que tu souhaites préserver pour n'avoir qu'une seule instance. Au final tu vas te retrouver avec l'instance créée via sharedInstance, mais qui sera vide (propriétés non restaurées, juste créée comme la première fois), et une autre instance créée par le NSKeyedUnarchiver, avec ses propriétés restaurées, mais dont tu ne feras rien...
Donc ce qu'il faut dans ton cas c'est plutôt de sérialiser non pas le Singleton lui-même mais chacune de ses propriétés, et dans le init du Singleton restaurer chacune de ces propriétés. Mais pas sérialiser le Singleton lui-même, car il ne faudrait pas que le NSKeyedUnarchiver recrée une instance de ta classe Singleton, au risque de créer une instance qui n'a rien à voir (et au risque si tu mets le code appelant NSKeyedUnarchiver dans le init de ton Singleton lui-même, de faire une boucle infinie vu que dans ce cas le NSKeyedUnarchiver va réinstancier un objet Singleton et pas juste chacune des propriétés qu'il contient)Je n'utilise pas le dispatch_once(..) mais
Mais ça revient au même je pense.
Mais j'encode et je décode non pas le singleton en lui même mais ces propriété en faisant ça :
articleALire est une NSMutableSet.
Ahh ...
Sur self (Singelton)
du coup je dois faire plutôt ?
Car sinon tu archives self (donc ton instance de Singleton), donc tu archives l'instance de Singleton elle-même (qui va, pour s'archiver, appeler sa méthode encodeWithCoder: pour encoder ses propriétés sous-jacentes comme articleALire etc). Et si tu fais ça, ça veut dire qu'à l'inverse quand tu vas ensuite demander de désarchiver ton archive, il va se charger de recréer une instance de Singleton (puisque ce Singleton tu l'as inclus dans l'archive), menant au problème décrit plus haut.
Donc en effet, il faut plutôt que tu n'archives pas ton instance de Singleton dans ton archive, mais mettes seulement dans l'archive ses propriétés sous-jacentes, dans ton cas articleALire en effet.
Oui du coup c'est logique ce que tu dis. Ce que je faisais été assez dangereux et pas sécurisé. Du moins moins sécurisé que maintenant
Je regarderais le NSCodingSecure par la suite