Open10

# PreferencesDataStore と SharedPreferences 内部実装

TatsukiTatsuki

SharedPreferences

  • SharedPreferences.java
  • SharedPreferencesImpl.java
TatsukiTatsuki

コンストラクタ

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    startLoadFromDisk();
}
TatsukiTatsuki
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

synchronized 修飾子で他のスレッドからの同時呼び出されないようにしている。loadFromDisk で内部に保存してある xml ファイルを読み込み、mMap の Map<String, Object> に展開。

TatsukiTatsuki
@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を発生させる。

TatsukiTatsuki
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}
TatsukiTatsuki

基本的に値の読み込みメソッド内部では awaitLoadedLocked が呼ばれている。mLoaded
loadFromDisk からのみ true になるようなので、インスタンス生成時の startLoadFromDisk のタイミングでファイル読み込みを終えるタイミングで mLoaded = true になる?

mLoaded = false の場合は awaitLoadedLocked 内部で mLoad.wait() でスレッドを一時停止する。
再会は loadFromDisk の finally 内部で mLock.notifyAll があるのでここでスレッドを再開させていそう。

つまり、startLoadFromDiskloadFromDisk の処理が終わらない限りは getString などの処理は待機されるっぽい!?また、ファイル読み込みはインスタンス生成時のみに行われているっぽい!?

TatsukiTatsuki

PreferencesDataStore

  • androidx.datastore:preferences:1.0.0
    • PreferenceDataStoreDelegate
    • PreferenceDataStoreFactory
  • androidx.datastore:preferences-core:1.0.0
    • Preferences
TatsukiTatsuki

preferencesDataStore の内部で PreferenceDataStoreSingletonDelegate が呼ばれ、DataStore のインスタンスが同期的に生成される。その際に PreferencesDataStoreFile の処理により、アプリ内部にファイルが作成される。

DataStore のインスタンスは PreferenceDataStoreFactory の create によって生成される。

Preferences 内部に MutableMap である preferencesMap がある。

SingleProcessDataStore.kt に readData がある。 これは readDataOrHandleCorruption から呼ばれている。さらにこれが readAndInit から呼ばれている。

値の読み込み時は毎回ファイル読み込みしていそう。

TatsukiTatsuki
    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
        }
    }
TatsukiTatsuki

同期的に実行するために 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で呼び出しをラップすることができます。しかし、これはお勧めしない。そのようなコードはコルーチンに移動させる方がずっと良い。

参考