# PreferencesDataStore と SharedPreferences 内部実装
SharedPreferences
- SharedPreferences.java
- SharedPreferencesImpl.java
コンストラクタ
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
synchronized 修飾子で他のスレッドからの同時呼び出されないようにしている。loadFromDisk
で内部に保存してある xml ファイルを読み込み、mMap の Map<String, Object> に展開。
@GuardedBy("mLock")
private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
Raise an explicit StrictMode onReadFromDisk for this thread, since the real read will be in a different thread and otherwise ignored by StrictMode.
実際の読み取りは別のスレッドで行われ、それ以外はStrictModeによって無視されるので、このスレッドのために明示的なStrictMode onReadFromDiskを発生させる。
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
基本的に値の読み込みメソッド内部では awaitLoadedLocked
が呼ばれている。mLoaded
は
loadFromDisk
からのみ true になるようなので、インスタンス生成時の startLoadFromDisk
のタイミングでファイル読み込みを終えるタイミングで mLoaded = true
になる?
mLoaded = false
の場合は awaitLoadedLocked
内部で mLoad.wait()
でスレッドを一時停止する。
再会は loadFromDisk
の finally 内部で mLock.notifyAll
があるのでここでスレッドを再開させていそう。
つまり、startLoadFromDisk
と loadFromDisk
の処理が終わらない限りは getString
などの処理は待機されるっぽい!?また、ファイル読み込みはインスタンス生成時のみに行われているっぽい!?
PreferencesDataStore
- androidx.datastore:preferences:1.0.0
- PreferenceDataStoreDelegate
- PreferenceDataStoreFactory
- androidx.datastore:preferences-core:1.0.0
- Preferences
preferencesDataStore の内部で PreferenceDataStoreSingletonDelegate が呼ばれ、DataStore のインスタンスが同期的に生成される。その際に PreferencesDataStoreFile の処理により、アプリ内部にファイルが作成される。
DataStore のインスタンスは PreferenceDataStoreFactory の create によって生成される。
Preferences 内部に MutableMap である preferencesMap がある。
SingleProcessDataStore.kt に readData
がある。 これは readDataOrHandleCorruption
から呼ばれている。さらにこれが readAndInit
から呼ばれている。
値の読み込み時は毎回ファイル読み込みしていそう。
private suspend fun readData(): T {
try {
FileInputStream(file).use { stream ->
return serializer.readFrom(stream)
}
} catch (ex: FileNotFoundException) {
if (file.exists()) {
throw ex
}
return serializer.defaultValue
}
}
internal suspend fun writeData(newData: T) {
file.createParentDirectories()
val scratchFile = File(file.absolutePath + SCRATCH_SUFFIX)
try {
FileOutputStream(scratchFile).use { stream ->
serializer.writeTo(newData, UncloseableOutputStream(stream))
stream.fd.sync()
// TODO(b/151635324): fsync the directory, otherwise a badly timed crash could
// result in reverting to a previous state.
}
if (!scratchFile.renameTo(file)) {
throw IOException(
"Unable to rename $scratchFile." +
"This likely means that there are multiple instances of DataStore " +
"for this file. Ensure that you are only creating a single instance of " +
"datastore for this file."
)
}
} catch (ex: IOException) {
if (scratchFile.exists()) {
scratchFile.delete() // Swallow failure to delete
}
throw ex
}
}
同期的に実行するために runBlocking
の使用が公式ドキュメントでも書かれているが、以下のように使用する場合は注意することやあまりお勧めしないと書かれている。
If you find yourself in a situation where you need to use this approach, do spend some time figuring out if it’s absolutely necessary to block the main thread. Think about how you could use the provided DataStore async alternatives or refactor your current code to avoid runBlocking(), for example by preloading the data asynchronously:
もしこの方法を使わなければならない状況になったら、メインスレッドをブロックすることが絶対に必要なのかどうか、時間をかけて考えてみてください。例えば、データを非同期でプリロードすることで、runBlocking()を回避することができます。
If you are not in a coroutine and you need to remain synchronous you can wrap the call in runBlocking. I do not recommend this. It is much better to move that code to coroutines.
コルーチン内でなく、同期を維持する必要がある場合、runBlockingで呼び出しをラップすることができます。しかし、これはお勧めしない。そのようなコードはコルーチンに移動させる方がずっと良い。