2016-12-15 4 views
2

In meiner App möchte ich Benutzereinstellungen in einer PLIST-Datei für jeden Benutzer anmelden, ich schreibe one class called CCUserSettings, die fast die gleiche Schnittstelle wie NSUserDefaults hat und liest und schreibt eine PLIST-Datei im Zusammenhang mit der aktuellen Benutzer-ID. Es funktioniert, hat aber eine schlechte Leistung. Jedes Mal, wenn der Benutzer [[CCUserSettings sharedUserSettings] synchronize] anruft, schreibe ich eine NSMutableDictionary (die die Benutzereinstellungen behalten) in eine PLIST-Datei, der folgende Code zeigt synchronize von CCUserSettings unter Auslassung einiger trivialer Details.Wie läuft `NSUserDefaults 'so schnell?

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

Ich nehme an NSUserDefaults in Dateien schreiben soll, wenn wir [[NSUserDefaults standardUserDefaults] synchronize] nennen, aber es läuft wirklich schnell, ich schreibe ein demo zu testen, ist der Schlüssel Teil ist unten, laufen 1000 mal [[NSUserDefaults standardUserDefaults] synchronize] und [[CCUserSettings sharedUserSettings] synchronize] auf meinem iPhone6, das Ergebnis ist 0,45 Sekunden gegenüber 9,16 Sekunden.

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]); 

Wie das Ergebnis zeigt, NSUserDefaults ist fast 20 mal schneller als mein CCUserSettings. Jetzt beginne ich mich zu wundern, dass "NSUserDefaults wirklich jedes Mal in die PLIST-Dateien schreibt, wenn wir synchronize aufrufen?", Aber wenn nicht, wie kann es garantieren, dass die Daten zurück in die Datei geschrieben werden, bevor der Prozess beendet wird (wie es der Prozess erlaubt) jederzeit getötet werden)?

In diesen Tagen habe ich eine Idee, um meine CCUserSettings zu verbessern, es ist mmapMemory-mapped I/O. Ich kann einen virtuellen Speicher zu einer Datei zuordnen und jedes Mal, wenn der Benutzer synchronize aufruft, erstelle ich eine NSData Methode mit NSPropertyListSerialization dataWithPropertyList:format:options:error: und kopiere die Daten in diesen Speicher, das Betriebssystem schreibt den Speicher zurück in die Datei, wenn der Prozess beendet wird. Aber ich kann nicht eine gute Leistung erhalten, weil die Dateigröße nicht festgelegt ist, jedes Mal, wenn die Länge der Daten zunimmt, muss ich mmap einen virtuellen Speicher, ich glaube, die Operation ist zeitaufwendig.

Entschuldigung für meine überflüssigen Details, ich will nur wissen, wie NSUserDefaults arbeitet, um so gute Leistung zu erzielen, oder kann jemand gute Ratschläge haben, um meine CCUserSettings zu verbessern?

+1

Hier ist ein schöner Artikel über 'NSUserDefaults': http://dscoder.com /defaults.html. Sein Autor ist ein Ingenieur bei Apple, also ist es ziemlich sicher anzunehmen, dass er weiß, wovon er spricht :) – Losiowaty

+0

@Losiowaty Danke für deinen Link, aber ich denke, es spricht über die Implementierung in MacOx, denn es heißt "Einen Wert setzen wird (schließlich ist es asynchron und tritt einige Zeit später in einem anderen Prozess auf) schreibe das gesamte plist auf die Festplatte, egal wie klein die Änderung war. Wenn Sie NSUserDefaults ändern und Ihre App ohne "synchronize" beenden, werden die Einstellungen nicht in die Datei geschrieben, daher glaube ich nicht, dass es einen anderen Prozess gibt, der die Datei in iOS schreibt. – KudoCC

+0

Wenn Sie NSUserDefaults ändern und die App beenden, müssen Sie * sehr * schnell auf der Kill-Schaltfläche sein, um Daten zu verlieren. Ein paar Millisekunden oder so. Dies hat sich in iOS 8 geändert; vorher war es viel einfacher, Daten auf diese Weise zu verlieren. –

Antwort

2

Auf modernen Betriebssystemen (iOS 8+, macOS 10.10+) schreibt NSUserDefaults die Datei nicht, wenn Sie die Synchronisierung aufrufen. Wenn Sie die Methode -set * aufrufen, sendet sie eine asynchrone Nachricht an einen Prozess namens cfprefsd, der die neuen Werte speichert, eine Antwort sendet und die Datei zu einem späteren Zeitpunkt dann ausgibt. All -synchronize does wartet auf alle ausstehenden Nachrichten an cfprefsd, um Antworten zu erhalten.

(edit: Sie dies überprüfen können, wenn Sie mögen, durch einen symbolischen Haltepunkt auf xpc_connection_send_message_with_reply Einstellung und dann einen Benutzer Standardeinstellung)

+0

Danke für Ihre Antwort! Aber wenn die IO-Operation auf dem anderen Prozess passiert, warum habe ich dann die Einstellungen verloren, die ich gesetzt habe, bevor ich 'exit' aufgerufen habe? – KudoCC

+0

Die Tatsache, dass ich die Einstellungen verliere, die kurz vor dem Aufruf von "exit" gesetzt wurden, bedeutet nicht, dass Sie falsch sind. Vielleicht benötigt der cfprefsd-Prozess unseren Prozess, um eine andere Nachricht zu senden. Ich bin nur neugierig zu wissen:) Übrigens, woher wussten Sie das, können Sie einige Referenzen veröffentlichen. – KudoCC

+0

Es gibt zwei Gründe, die auftreten können. Der erste ist der einfachste: Der Nachrichtenversand ist asynchron, daher kann Ihr exit() -Aufruf geschehen, bevor die Nachricht den Prozess verlassen hat. Die zweite ist subtiler: cfprefsd muss Ihre Sandbox-Berechtigungen überprüfen, um sicherzustellen, dass Sie zu diesen Einstellungen gelangen. Wenn Sie Sandbox-Berechtigungen überprüfen, muss Ihr Prozess noch ausgeführt werden. –

2

Endlich komme ich mit einer Lösung, um die Leistung meiner CCUserSettings mit mmap zu verbessern, ich nenne es CCMmapUserSettings.

Voraussetzung

Die synchronize in CCUserSettings oder NSUserDefaults Methode schreibt auf die Festplatte die plist-Datei zurück, es bemerkenswerte Zeit kostet, aber wir müssen es in einigen Situationen wie aufrufen, wenn App in den Hintergrund geht. Trotzdem gehen wir das Risiko ein, die Einstellungen zu verlieren: Wir Apps können vom System getötet werden, weil es nicht genügend Speicher hat oder auf eine Adresse zugreift, für die es keine Berechtigung hat. Zu diesem Zeitpunkt können die Einstellungen nach dem letzten synchronize verlieren.

Wenn es einen Weg gibt, können wir die Datei auf die Festplatte schreiben, wenn der Prozess beendet wird, können wir die Einstellungen im Speicher die ganze Zeit ändern, es ist ziemlich schnell. Aber gibt es einen Weg, das zu erreichen?

Nun, ich finde einen, es ist mmap, mmap mappt eine Datei auf eine Region des Speichers. Wenn dies geschehen ist, kann auf die Datei wie ein Array im Programm zugegriffen werden. So können wir den Speicher ändern, als ob wir die Datei schreiben würden. Wenn der Prozess beendet wird, schreibt der Speicher in die Datei zurück.

Es gibt zwei Links unterstützt mich:

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

mmap, msync and linux process termination

Problem von mmap mit

Wie ich in meiner Frage erwähnt:

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.

Das Problem ist: jedes Mal, wenn die Länge der Daten zunimmt, muss ich mmap einen virtuellen Speicher, es ist zeitaufwändig Operation.

Lösung

Jetzt habe ich eine Lösung: immer eine größere Größe erstellen, als wir in dem Anfang 4 Bytes der Datei, die die reale Dateigröße benötigen und halten und die realen Daten nach dem 4 Bytes schreiben. Da die Datei größer ist als das, was wir brauchen, brauchen wir bei einem reibungslosen Anstieg der Daten nicht bei jedem Anruf von mmap Speicher synchronize. Es gibt noch eine weitere Einschränkung der Dateigröße: Die Dateigröße ist immer ein Vielfaches von MEM_PAGE_SIZE (in meiner App als 4096 definiert).

Die Synchronisierungs-Methode:

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

Ein Beispiel meine Gedanken beschreiben helfen: die plist Datengröße 5000 Bytes annehmen, wird die Gesamtzahl der Bytes Ich muss schreiben 4 + 5000 = 5004 I 4 schreiben Bytes vorzeichenlose ganze Zahl, deren Wert zuerst 5004 ist, dann schreibe die 5000 Bytes Daten. Die Gesamtdateigröße sollte 8192 (2 * MEM_PAGE_SIZE) betragen. Der Grund, warum ich eine größere Datei erstelle, ist, dass ich einen großen Puffer benötige, um die Zeit zu reduzieren, um Speicher neu zu memmatisieren.

Leistung

{ 
    [[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]); 
} 

Die Ausgabe lautet:

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

Es zeigt, dass CCMmapUserSettings läuft schneller als NSUserDefaults !!!

Ich bin nicht sicher

CCMmapUserSettings die Geräteeinstellungen auf meinem iPhone6 ​​gibt (iOS 10.1.1), aber ich wirklich nicht sicher, ob es auf allen iOS-Versionen funktioniert, weil ich keine offizielle bekommen haben Dokument, um sicherzustellen, dass der Speicher, der zum Abbilden der Datei verwendet wurde, sofort auf den Datenträger zurückgeschrieben wird, wenn der Prozess beendet wird. Wenn dies nicht der Fall ist, wird er auf den Datenträger geschrieben, bevor das Gerät herunterfährt?

Ich denke, ich muss das Systemverhalten über mmap studieren, wenn jemand von euch das weiß, bitte teilen. Vielen Dank.

+0

FWIW, der Aufruf von -synchronize auf NSUserDefaults ist in der Regel für iOS 8 und höher nicht erforderlich. Dadurch wird Ihr Programm nur verlangsamt (manchmal keine schlechte Sache, wenn Sie exit() aufrufen und warten müssen, bis die Daten sicher außer Betrieb sind). Die Verwendung von mmap() ist gefährlich, wenn das zugrundeliegende Volume ausgehängt werden kann, wenn Atomizität erforderlich ist, wenn eine Kernel-Panic auftritt oder wenn andere Prozesse mit der Datei umgehen können. Seien Sie vorsichtig :) –

+0

Wenn nur ein Prozess auf die Datei zugreifen kann, da er nur auf der iOS-Plattform läuft und nur ein Thread die 'mmap' ausführt und den Speicher ändert, ist das immer noch gefährlich? Ich mag es wirklich, weil es so schnell läuft !!! – KudoCC

+0

Sie haben immer noch einige Risiken, wenn das System während des Schreibens der Daten abstürzt, aber ja, wenn Sie die Verwendung genau so kontrollieren können, sind die Risiken der Verwendung von mmap sehr viel kleiner. Das typische Muster, um Schreibvorgänge robust gegen Systemabstürze zu machen, besteht darin, mkstemp() zu verwenden, um eine temporäre Datei zu erstellen, in diese zu schreiben, fsync und dann umzubenennen() sie über die Originaldatei. Wie du gesehen hast, ist das langsamer. –

Verwandte Themen