2016-12-15 1 views
2

Dans mon application, je veux enregistrer les paramètres utilisateur dans un fichier plist pour chaque utilisateur se connecte, j'écris one class called CCUserSettings qui a presque la même interface que NSUserDefaults et il lit et écrit un fichier plist lié à l'ID de l'utilisateur actuel. Cela fonctionne mais a de mauvaises performances. Chaque fois que l'utilisateur appelle [[CCUserSettings sharedUserSettings] synchronize], j'écris un NSMutableDictionary (qui garde les paramètres utilisateur) dans un fichier plist, le code ci-dessous montre synchronize de CCUserSettings en omettant quelques détails triviaux.Comment `NSUserDefaults synchronize` peut-il fonctionner aussi vite?

- (BOOL)synchronize { 
    BOOL r = [_settings writeToFile:_filePath atomically:YES]; 
    return r; 
} 

Je suppose que NSUserDefaults devrait écrire dans des fichiers lorsque nous appelons [[NSUserDefaults standardUserDefaults] synchronize], mais il fonctionne très vite, j'écris un demo pour tester, la partie clé est ci-dessous, exécution 1000 fois [[NSUserDefaults standardUserDefaults] synchronize] et [[CCUserSettings sharedUserSettings] synchronize] sur mon iPhone6, le résultat est de 0.45 secondes contre 9.16 secondes.

NSDate *begin = [NSDate date]; 
for (NSInteger i = 0; i < 1000; ++i) { 
    [[NSUserDefaults standardUserDefaults] setBool:(i%2==1) forKey:@"key"]; 
    [[NSUserDefaults standardUserDefaults] synchronize]; 
} 
NSDate *end = [NSDate date]; 
NSLog(@"synchronize seconds:%f", [end timeIntervalSinceDate:begin]); 


[[CCUserSettings sharedUserSettings] loadUserSettingsWithUserId:@"1000"]; 
NSDate *begin = [NSDate date]; 
for (NSInteger i = 0; i < 1000; ++i) { 
    [[CCUserSettings sharedUserSettings] setBool:(i%2==1) forKey:@"_boolKey"]; 
    [[CCUserSettings sharedUserSettings] synchronize]; 
} 
NSDate *end = [NSDate date]; 
NSLog(@"CCUserSettings modified synchronize seconds:%f", [end timeIntervalSinceDate:begin]); 

Comme le montre le résultat, NSUserDefaults est presque 20 fois plus rapide que mon CCUserSettings. Maintenant, je commence à me demander si "NSUserDefaults écrit vraiment dans les fichiers plist à chaque fois que nous appelons la méthode synchronize?", Mais si ce n'est pas le cas, comment peut-il garantir que les données sont réécrites dans le fichier? être tué à tout moment)?

Ces jours je viens avec une idée pour améliorer mon CCUserSettings, il est mmapMemory-mapped I/O. Je peux mapper une mémoire virtuelle dans un fichier et chaque fois que l'utilisateur appelle synchronize, je crée une méthode NSData avec NSPropertyListSerialization dataWithPropertyList:format:options:error: et copier les données dans cette mémoire, le système d'exploitation va écrire la mémoire dans le fichier lorsque le processus se termine. Mais je ne peux pas obtenir une bonne performance parce que la taille du fichier n'est pas fixe, chaque fois que la longueur des données augmente, je dois mmap une mémoire virtuelle, je crois que l'opération prend du temps.

Désolé pour mes détails redondants, je veux juste savoir comment NSUserDefaults fonctionne pour atteindre de si bonnes performances, ou quelqu'un peut-il avoir de bons conseils pour améliorer mon CCUserSettings?

+1

Voici un bel article sur les 'NSUserDefaults': http://dscoder.com /defaults.html. Son auteur est un ingénieur chez Apple, donc c'est assez sûr de supposer qu'il sait de quoi il parle :) – Losiowaty

+0

@Losiowaty Merci pour votre lien, mais je pense qu'il parle de la mise en œuvre en mac ox, car il dit "Définir une valeur sera (finalement, il est asynchrone et se produit quelque temps plus tard dans un autre processus) écrire tout le plist sur le disque, peu importe la taille du changement. " Si vous modifiez NSUserDefaults et que vous tuez votre application sans 'synchronize', les paramètres ne seront pas écrits dans le fichier, donc je ne pense pas qu'il existe un autre processus qui écrit le fichier dans iOS. – KudoCC

+0

Si vous modifiez NSUserDefaults et que vous tuez l'application, vous devrez être * très * rapide sur le bouton kill pour perdre des données. Quelques millisecondes environ. Cela a changé dans iOS 8; avant cela, il était beaucoup plus facile de perdre des données de cette façon. –

Répondre

2

Sur les systèmes d'exploitation modernes (iOS 8+, macOS 10.10+), NSUserDefaults n'écrit pas le fichier lorsque vous appelez la synchronisation. Lorsque vous appelez des méthodes -set *, il envoie un message asynchrone à un processus appelé cfprefsd, qui stocke les nouvelles valeurs, envoie une réponse, puis écrit ultérieurement le fichier. All -synchronize does attend tous les messages en attente à cfprefsd pour recevoir des réponses.

(edit: vous pouvez vérifier, si vous le souhaitez, en mettant un point d'arrêt symbolique sur xpc_connection_send_message_with_reply puis définir un utilisateur par défaut)

+0

Merci pour votre réponse! Mais si l'opération d'E/S se produit sur l'autre processus, pourquoi ai-je perdu les paramètres définis juste avant d'appeler 'exit'? – KudoCC

+0

Le fait que je perds les paramètres définis juste avant d'appeler 'exit' ne signifie pas que vous êtes faux, peut-être que le processus cfprefsd nécessite que notre processus envoie un autre message. Je suis juste curieux de savoir:) BTW comment saviez-vous cela, pouvez-vous poster quelques références. – KudoCC

+0

Il y a deux raisons qui peuvent survenir. Le premier est le plus simple: l'envoi de message est asynchrone, de sorte que votre appel exit() peut avoir lieu avant que le message ne l'ait fait sortir de votre processus. La seconde est subtler: cfprefsd doit vérifier les permissions de votre sandbox pour s'assurer que vous êtes autorisé à accéder à ces préférences. La vérification des autorisations du bac à sable nécessite que votre processus soit toujours en cours d'exécution. –

2

Enfin, je viens avec une solution pour améliorer les performances de mon CCUserSettings avec mmap, je l'appelle CCMmapUserSettings.

Condition préalable

Le synchronize dans CCUserSettings ou méthode NSUserDefaults écrit le fichier plist retour sur le disque, il coûte du temps notable, mais nous devons l'appeler dans certaines situations comme lorsque l'application passe en arrière-plan. Même si nous prenons le risque de perdre les paramètres: nous les applications peuvent être tuées par le système parce qu'il manque de mémoire ou accède à une adresse à laquelle il n'a pas permission, à ce moment les paramètres que nous avons définis après la dernière synchronize peuvent perdre.

S'il existe un moyen d'écrire le fichier sur le disque lorsque le processus est terminé, nous pouvons modifier les paramètres en mémoire en permanence, c'est plutôt rapide. Mais y a-t-il un moyen d'y parvenir?

Eh bien, j'en trouve un, c'est mmap, mmap mappe un fichier sur une région de mémoire. Lorsque cela est fait, le fichier peut être consulté comme un tableau dans le programme. Nous pouvons donc modifier la mémoire comme si nous écrivions le fichier. Lorsque le processus se termine, la mémoire réécrira dans le fichier.

Il y a deux liens me soutiennent:

Does the OS (POSIX) flush a memory-mapped file if the process is SIGKILLed?

mmap, msync and linux process termination

problème de l'utilisation de mmap

Comme je l'ai mentionné dans ma question:

These days I come up with an idea to improve my CCUserSettings, it is mmap Memory-mapped I/O. I can map a virtual memory to a file and every time user calls synchronize, I create a NSData with NSPropertyListSerialization dataWithPropertyList:format:options:error: method and copy the data into that memory, operating system will write memory back to file when process exits. But I may not get a good performance because the file size is not fixed, every time the length of data increases, I have to remmap a virtual memory, I believe the operation is time consuming.

Le problème est: chaque fois que la longueur des données augmente, je dois mmap une mémoire virtuelle, il prend du temps.

Solution

Maintenant, j'ai une solution: créer toujours une plus grande taille que nous avons besoin et garder la taille réelle du fichier au début 4 octets du fichier et écrire les données réelles après les 4 octets. Comme le fichier est plus grand que ce dont nous avons besoin, lorsque les données augmentent régulièrement, nous n'avons pas besoin de mmap mémoire à chaque appel de synchronize. Il y a une autre restriction sur la taille du fichier: la taille du fichier est toujours multiple de MEM_PAGE_SIZE (défini comme 4096 dans mon application).

La méthode de synchronisation:

- (BOOL)synchronize { 
    if (!_changed) { 
     return YES; 
    } 
    NSData *data = [NSPropertyListSerialization dataWithPropertyList:_settings format:NSPropertyListXMLFormat_v1_0 options:0 error:nil]; 
    // even if data.length + sizeof(_memoryLength) is a multiple of MEM_PAGE_SIZE, we need one more page. 
    unsigned int pageCount = (unsigned int)(data.length + sizeof(_memoryLength))/MEM_PAGE_SIZE + 1; 
    unsigned int fileSize = pageCount * MEM_PAGE_SIZE; 
    if (fileSize != _memoryLength) { 
     if (_memory) { 
      munmap(_memory, _memoryLength); 
      _memory = NULL; 
      _memoryLength = 0; 
     } 

     int res = ftruncate(fileno(_file), fileSize); 
     if (res == -1) { 
      // truncate file error 
      fclose(_file); 
      _file = NULL; 
      return NO; 
     } 
     // re-map the file 
     _memory = (unsigned char *)mmap(NULL, fileSize, PROT_READ|PROT_WRITE, MAP_SHARED, fileno(_file), 0); 
     _memoryLength = (unsigned int)fileSize; 
     if (_memory == MAP_FAILED) { 
      _memory = NULL; 
      fclose(_file); 
      _file = NULL; 
      return NO; 
     } 
#ifdef DEBUG 
     NSLog(@"memory map file success, size is %@", @(_memoryLength)); 
#endif 
    } 

    if (_memory) { 
     unsigned int length = (unsigned int)data.length; 
     length += sizeof(length); 
     memcpy(_memory, &length, sizeof(length)); 
     memcpy(_memory+sizeof(length), data.bytes, data.length); 
    } 
    return YES; 
} 

Un exemple vous aidera à décrire ma pensée: supposons que la taille des données plist est de 5000 octets, les octets au total que je dois écrire est 4 + 5000 = 5004. Je vous écris 4 octets nombre entier non signé dont la valeur est 5004 en premier, puis écrire les données de 5000 octets. La taille totale du fichier doit être 8192 (2 * MEM_PAGE_SIZE). La raison pour laquelle je crée un fichier plus volumineux est que j'ai besoin d'un gros tampon pour réduire le temps nécessaire pour re-mmap la mémoire.

Performance

{ 
    [[CCMmapUserSettings sharedUserSettings] loadUserSettingsWithUserId:@"1000"]; 
    NSDate *begin = [NSDate date]; 
    for (NSInteger i = 0; i < 1000; ++i) { 
     [[CCMmapUserSettings sharedUserSettings] setBool:(i%2==1) forKey:@"_boolKey"]; 
     [[CCMmapUserSettings sharedUserSettings] synchronize]; 
    } 
    NSDate *end = [NSDate date]; 
    NSLog(@"CCMmapUserSettings modified synchronize seconds:%f", [end timeIntervalSinceDate:begin]); 
} 

{ 
    [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"key"]; 
    NSDate *begin = [NSDate date]; 
    for (NSInteger i = 0; i < 1000; ++i) { 
     [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"key"]; 
     [[NSUserDefaults standardUserDefaults] synchronize]; 
    } 
    NSDate *end = [NSDate date]; 
    NSLog(@"NSUserDefaults not modified synchronize seconds:%f", [end timeIntervalSinceDate:begin]); 
} 

{ 
    NSDate *begin = [NSDate date]; 
    for (NSInteger i = 0; i < 1000; ++i) { 
     [[NSUserDefaults standardUserDefaults] setBool:(i%2==1) forKey:@"key"]; 
     [[NSUserDefaults standardUserDefaults] synchronize]; 
    } 
    NSDate *end = [NSDate date]; 
    NSLog(@"NSUserDefaults modified synchronize (memory not change) seconds:%f", [end timeIntervalSinceDate:begin]); 
} 

La sortie est:

CCMmapUserSettings modified synchronize seconds:0.037747 
NSUserDefaults not modified synchronize seconds:0.479931 
NSUserDefaults modified synchronize (memory not change) seconds:0.402940 

Il montre que CCMmapUserSettings fonctionne plus vite que NSUserDefaults !!!

Je ne suis pas sûr

CCMmapUserSettings passe les réglages de l'appareil sur mon iPhone6 ​​(iOS 10.1.1), mais je ne sais pas vraiment si cela fonctionne sur toutes les versions iOS parce que je ne l'ai pas obtenu un fonctionnaire document pour s'assurer que la mémoire utilisée pour mapper le fichier sera réécrite sur le disque immédiatement lorsque le processus se termine, si ce n'est pas le cas, sera-t-il écrit sur le disque avant que l'appareil ne s'éteigne?

Je pense que je dois étudier le comportement du système à propos de mmap, si quelqu'un d'entre vous le sait, s'il vous plaît partager. Merci beaucoup.

+0

FWIW, appel -synchronize sur NSUserDefaults est généralement inutile sur iOS 8 et versions ultérieures. Cela ne fera que ralentir votre programme (parfois pas une mauvaise chose, si vous êtes sur le point d'appeler exit() et devez attendre que les données soient hors processus). L'utilisation de mmap() est dangereuse si le volume sous-jacent peut être démonté, si une atomicité est nécessaire, si une panique du noyau se produit ou si d'autres processus peuvent perturber le fichier. Faites attention :) –

+0

Si un seul processus peut accéder au fichier car il ne fonctionne que sur la plate-forme iOS et qu'un seul thread fait le 'mmap' et modifie la mémoire, est-ce encore dangereux? J'aime vraiment ça parce que ça marche si vite !!! – KudoCC

+0

Vous avez encore des risques si le système se bloque au milieu de l'écriture des données, mais oui, si vous pouvez contrôler l'utilisation avec précaution comme ça, les risques d'utiliser mmap sont beaucoup plus petits. Le modèle typique pour rendre les écritures robustes contre les plantages système consiste à utiliser mkstemp() pour créer un fichier temporaire, écrire dessus, fsync, puis le renommer() par-dessus le fichier d'origine. Malheureusement, comme vous l'avez vu, c'est plus lent. –