SharedPreferencesでStringSetを用いて追加保存する際、以下が起こることがある
- 1件しか保存されない
- 保存してないのに勝手にデータが保存されている
結論を先に書くと、
SharedPreferencesからgetStringSetする際、シャローコピーすれば解決するが
// java版 Set<String> set = new HashSet<>( getSharedPreferences("set",MODE_PRIVATE).getStringSet("data", new HashSet<>())); // Kotlin版 val set = getSharedPreferences("set", MODE_PRIVATE) .getStringSet("data", mutableSetOf())?.toMutableSet()
どういう理屈なのかをもう少し詳しくまとめる。
具体的にどんな事象が起こるのか
1件しか保存されない件
ボタンを押したら、SharedPreferencesからSet<String>を読み込んで、時刻を追加し保存するプログラムを例にする
fun onClick(view : View) { getSharedPreferences("set", Context.MODE_PRIVATE).run { val set = getStringSet("data", mutableSetOf()) Log.d("TAG", "data = $set") set?.add(LocalTime.now().toString()) edit().putStringSet("data2", set).apply() } }
このプログラムの実行結果は
4行目のログから、ボタンを押すたびに時間が増えていくことが確認できるが、
実はSharedPreferencesの書き込み(set.xml)は1件のみしか保存されていない。
そのため、アプリを終了させて再度起動させるとデータは1件のみしか残っていない。
保存してないのに勝手にデータが保存されている
ボタンを押したら、SharedPreferencesからSet<String>、intを読み込んで両方のデータを変更。
その後、intのデータのみ保存を行うプログラムを例にする
fun onClick(view : View) { getSharedPreferences("set", Context.MODE_PRIVATE).run { val set = getStringSet("data", mutableSetOf()) var num = getInt("num", 0) Log.d("TAG", "data = $set , num = $num") set?.add(LocalTime.now().toString()) edit().putInt("num", ++num).apply() } }Set<String>を保存するプログラムは書いてないのに、SharedPreferencesへの書き込み(set.xml)が行われている。
原因
Set<String>の参照が、SharedPreferences内と外で同じなのが原因。
SharedPreferences自体はインターフェースで、実態はSharedPreferencesImpl.javaである。
プリファレンスのファイルを読み込み、XMLをパースしMapに保存(loadFromDisk())して、getStringSet()時にはそのまま参照を渡していることが読み取れる。
※正直ここはバグな気がする。通常こういう設計をするなら、シャローコピーを渡す。
今回の2件の事象はこの参照が共有されていることで発生している。
1件しか保存されない件
SharedPreferencesは書き込み対象があるのかチェックは
Editで持ってるMapとSharedPreferencesでファイル読み込み時に作ったMapを突き合わせ、ValueのObjectが同じかどうかで判定している(commitToMemory)。
なので、put時にSharedPreferencesから取得したSetを渡すと、参照同値で「変更がなかった」と解釈される。
以上から保存処理を行っていないことが原因となる。
保存してないのに勝手にデータが保存されている
書き換えは部分的に書き換えているのではなく、SharedPreferencesのMapをEditのMapでマージして、SharedPreferencesのMapを全て書き換える動作となっている。
よって、SharedPreferencesのMapと参照を同じにしているSetに要素を加えると、
勝手に他のcommit(apply)時にSetが更新される
他にこのようになるデータはないか確認したところ、
保存できる型は全部で以下であり、StringSetだけが問題であることがわかる。
取得メソッド | 型 | 備考 |
---|---|---|
putBoolean | boolean | プリミティブ型なので問題なし |
putFloat | float | プリミティブ型なので問題なし |
putInt | int | プリミティブ型なので問題なし |
putLong | long | プリミティブ型なので問題なし |
putString | String | イミュータブルなので問題なし |
putStringSet | Set<String> | 外部からデータ書き換え可能 |
対策
つまり、SharedPreferences内と外で参照を変えてあげれば解決する。
よって、SharedPreferencesからデータを取ってくるとき、シャローコピーして互いに干渉することなく制御できる。
// java版 Set<String> set = new HashSet<>( getSharedPreferences("set",MODE_PRIVATE).getStringSet("data", new HashSet<>())); // Kotlin版 val set = getSharedPreferences("set", MODE_PRIVATE) .getStringSet("data", mutableSetOf())?.toMutableSet()
0 件のコメント:
コメントを投稿