web-dev-qa-db-ja.com

NSUserDefaultsはiOS 8では信頼できません

[NSUserDefaults standardUserDefaults]を使用してセッション情報を保存するアプリがあります。通常、この情報はアプリの起動時に確認され、アプリの終了時に更新されます。 iOS 8では信頼性が低いように見えることがわかりました。

私は現在iPad 2でテストしていますが、必要であれば他のデバイスでもテストできます。

時々、終了前に書き込まれたデータはアプリの起動時に保持されません。同様に、終了前に削除されたキーは、起動後に存在するように見える場合があります。

この問題を説明するために、次の例を作成しました。

- (void)viewDidLoad 
{
    [super viewDidLoad];

    NSData *_dataArchive = [[NSUserDefaults standardUserDefaults] 
                                            objectForKey:@"Session"];

    NSLog(@"Value at launch - %@", _dataArchive);

    NSString *testString = @"TESTSTRING";
    [[NSUserDefaults standardUserDefaults] setObject:testString 
                                           forKey:@"Session"];
    [[NSUserDefaults standardUserDefaults] synchronize];

    _dataArchive = [[NSUserDefaults standardUserDefaults] 
                     objectForKey:@"Session"];

    NSLog(@"Value after adding data - %@", _dataArchive);

    [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"Session"];
    [[NSUserDefaults standardUserDefaults] synchronize];

    _dataArchive = [[NSUserDefaults standardUserDefaults] 
                     objectForKey:@"Session"];

    NSLog(@"Value before exit - %@", _dataArchive);

    exit(0);
}

上記のコードを実行すると、私は(通常)以下の出力を取得します(これは私が期待するものです):

Value at launch - (null)
Value after adding data - TESTSTRING
Value after deleting data - (null)

次に、キーを削除する行をコメントアウトすると:

//[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"Session"];
//[[NSUserDefaults standardUserDefaults] synchronize];

アプリを3回実行すると、次のように表示されます。

Value at launch - (null)
Value after adding data - TESTSTRING
Value after deleting data - TESTSTRING

Value at launch - TESTSTRING
Value after adding data - TESTSTRING
Value before exit - TESTSTRING

Value at launch - TESTSTRING
Value after adding data - TESTSTRING
Value before exit - TESTSTRING

しかし、実際に表示される出力は次のとおりです。

Value at launch - (null)
Value after adding data - TESTSTRING
Value after deleting data - TESTSTRING

Value at launch - (null)
Value after adding data - TESTSTRING
Value after deleting data - TESTSTRING

Value at launch - (null)
Value after adding data - TESTSTRING
Value after deleting data - TESTSTRING

例えばアプリの終了時に値を更新していないようです。

[〜#〜] edit [〜#〜]:iOS 7.1.2を実行しているiPad 2で同じコードをテストしました。毎回正しく動作するようです。

TLDR-iOS 8では、[NSUserDefaults standardUserDefaults]は信頼性の低い動作をしますか?動作する場合、回避策/解決策はありますか?

32
HaemEternal

iOS 8では、NSUserDefaultsにいくつかの動作の変更が導入されました。 NSUserDefaults AP​​Iはほとんど変更されていませんが、動作はアプリケーションに関連する方法で変更されています。たとえば、-synchronizeの使用は推奨されていません(常に推奨されています)。ファイル調整などのFoundationおよびCoreFoundationの他の部分への追加変更、および共有コンテナーに関連する変更は、アプリケーションとNSUserDefaultsの使用に影響する場合があります。

このため、特にNSUserDefaultsへの書き込みが変更されました。書き込みには時間がかかり、アプリケーションのユーザーデフォルトストレージへのアクセスを競合する他のプロセスが存在する場合があります。アプリケーションの終了時にNSUserDefaultsに書き込もうとすると、いくつかのシナリオで書き込みがコミットされる前にアプリケーションが終了する場合があります。この例でexit(0)を使用して強制的に終了すると、この動作が刺激される可能性が非常に高くなります。通常、アプリケーションが終了すると、システムはクリーンアップを実行し、未処理のファイル操作が完了するのを待つことができます-exit()またはデバッガーを使用してアプリケーションを終了する場合、これは発生しません。

一般的に、NSUserDefaultsは、iOS 8で正しく使用すると信頼できます。

これらの変更については、 OS X 10.10のFoundationリリースノート に記載されています(現在、iOS 8用の個別のFoundationリリースノートはありません)。

14
quellish

IOS 8はNSUserDefaultsで文字列を設定することを好まないようです。保存する前に、文字列をNSDataにエンコードしてみてください。

保存するとき:

[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:testString] forKey:@"Session"];

読むとき:

NSData *_data = [[NSUserDefaults standardUserDefaults] objectForKey:@"Session"];
NSString *_dataArchive = [NSKeyedUnarchiver unarchiveObjectWithData:_data];

お役に立てれば。

9
Igor

Gnasher729が言ったように、exit()を呼び出さないでください。 iOS8のNSUserDefaultsには問題があるかもしれませんが、exit()を呼び出すだけでは機能しません。

NSUserDefaultsに関するDavid Smithのコメントが表示されます( https://Gist.github.com/anonymous/8950927 ):

アプリを異常終了する(メモリプレッシャーの強制終了、クラッシュ、Xcodeでの停止)は、git reset --hard HEADのようなもので、終了します

3
n-b

NSUserDefaultsは、standardUserDefaultsに依存する代わりにスイート名を使用してインスタンスを作成するときに、iOS 8.4で適切に動作することがわかりました。

NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"MySuiteName"];

2
Thomas Verbeek

私はiOS 8で同じ問題を抱えており、私のために働いた唯一の解決策は、exit()関数の実行をいくつかの期間(例:0.1秒)遅らせることです:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC / 10), dispatch_get_main_queue(), ^{ exit(0); });

または、メソッドを作成してからperformSelector:withObject:afterDelay:を使用して呼び出します

- (void)exitApp {
   exit(0);
}

[self performSelector:@selector(exitApp) withObject:nil afterDelay:0.1];
1
Basem Saadawy

Foundation Framework Referenceで見つけましたが、役に立つと思います:

NSUserDefaultsクラスは、float、double、integer、Boolean、URLなどの一般的な型にアクセスするための便利なメソッドを提供します。デフォルトオブジェクトは、プロパティリスト、つまり、NSData、NSString、NSNumber、NSDate、NSArray、またはNSDictionaryのインスタンス(またはコレクションの場合はインスタンスの組み合わせ)でなければなりません。 他の種類のオブジェクトを保存する場合、通常はNSDataのインスタンスを作成するためにそれをアーカイブする必要があります。詳細については、「設定と設定のプログラミングガイド」を参照してください。

0
wm.p1us

私は同じ問題に直面しました。私は電話で解決しました

[[NSUserDefaults standardUserDefaults] synchronize];

呼び出す前に

[[NSUserDefaults standardUserDefaults] stringForKey:@"my_key"]

設定後だけでなく、取得する前にsynchronizeを呼び出す必要があることがわかります。

0
fnc12

メインスレッドでのみNSUserDefaultsを変更することで、同様の問題を解決しました。

0
Radu Simionescu

これはエンタープライズアプリであり、App Storeアプリではないため、以下を試すことができます。

@interface UIApplication()
-(void) _terminateWithStatus:(int)status;
@end

次に呼び出します:

[UIApplication.sharedApplication _terminateWithStatus:0];

文書化されていないAPIを使用しているため、iOSの以前または将来のバージョンでは動作しない可能性があります。

0
EricS

他の人がexit()を使用して指摘し、一般的にアプリを終了することはiOSでは本当に悪い考えです。

しかし私はおそらくあなたが何に対処しなければならないか知っています。エンタープライズアプリケーションも開発しましたが、iOSではすべてのルールとベストプラクティスに違反しているとクライアントに納得させようとしましたが、彼らはある時点でアプリを閉じることを主張しました。

Exit()の代わりに、次のコードを使用しました。

UIApplication *app = [UIApplication sharedApplication];
[app performSelector:@selector(suspend)];

名前が示すように、ユーザーがホームボタンを押したかのようにアプリを一時停止するだけです。したがって、保存方法は正しく終了できる場合があります。

ただし、特定のケースに対してこのソリューションをテストしたことはありません。サスペンドで十分かどうかはわかりませんが、私たちにとってはうまくいきました。

0
micromanc3r

これは、シミュレーターのバグです。このバグは、デバイス上のiOS8 beta4以前にも存在しますが、デバイス上ではこのバグは解決されていますが、現在シミュレーター上に存在しています。シミュレーターのディレクトリ構造も変更されています。 iOS8デバイスでも正常に動作します。

0
Waseem Shah