LoginSignup
17
9

More than 1 year has passed since last update.

SharedPreferencesがUIスレッドをブロックするってマジ!?

Last updated at Posted at 2021-12-12

SharedPreferencesって変更差分はメモリ上で保持していて、最初の読み出しはどうしようもないだろうけど、そのあとはapplyとか使っていれば安全だと思っていた時期が俺にもありました。しかしDataStoreの説明で

SharedPreferences には、UI スレッドで呼び出しても安全に見える同期 API がありますが、これは実際にはディスク I/O オペレーションを行います。さらに、apply() は fsync() で UI スレッドをブロックします。fsync() 呼び出しの保留は、サービスの開始と停止、およびアプリケーションの任意の場所でアクティビティが開始または停止するたびにトリガーされます。UI スレッドは、apply() によってスケジュールされた保留中の fsync() 呼び出しによってブロックされます。多くの場合、ANR の発生元になります。

な、なんだってー

マジかよ!?apply使ってれば大丈夫なんじゃ?ってなったので、実際どういうことなのかを調べてみることにしました。

SharedPreferencesの実装

SharedPreferencesそのものはinterfaceで中身がありません。

ContextImplにSharePreferencesのインスタンスを作っている処理があります。

ContextImpl.java
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    // At least one application in the world actually passes in a null
    // name.  This happened to work because when we generated the file name
    // we would stringify it to "null.xml".  Nice.
    if (mPackageInfo.getApplicationInfo().targetSdkVersion <
            Build.VERSION_CODES.KITKAT) {
        if (name == null) {
            name = "null";
        }
    }

    File file;
    synchronized (ContextImpl.class) {
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();
        }
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode);
}

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            checkMode(mode);
            if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                if (isCredentialProtectedStorage()
                        && !getSystemService(UserManager.class)
                                .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                    throw new IllegalStateException("SharedPreferences in credential encrypted "
                            + "storage are not available until after user is unlocked");
                }
            }
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        // If somebody else (some other process) changed the prefs
        // file behind our back, we reload it.  This has been the
        // historical (if undocumented) behavior.
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

@GuardedBy("ContextImpl.class")
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
}

SharedPreferencesImplというまんまの名前のクラスが実装のようです。また、ファイル名をKeyとしてキャッシュされており、同じファイル名のSharedPreferencesは同じインスタンスが使われるようになっていることも分かりますね。

SharedPreferencesImplの実装は以下になります。これを読み込んでいきましょう。

初回読み出し

SharedPreferencesは永続化のためにXMLファイルにデータを書き出しているため、当然最初に読み出しが必要です。まずは読み出し周りがどうなっているのかを調べて見ましょう。
コンストラクタ周りの実装は以下のようになっていて、コンストラクタの時点でスレッドが起動し、そのスレッドでファイルの読み出しが始まります。このスレッドからXMLファイルの読み出しとパース、結果をMap<String, Object>に格納しています。

SharedPreferencesImpl.java
@UnsupportedAppUsage
SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    startLoadFromDisk();
}

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

getSharedPreferencesをUIスレッドでコールしており、その時点でまだSharedPreferencesのインスタンスを確保できていなかったとしても、読み出し処理は独立したスレッドで行われるので、この時点ではまだUIスレッドをブロックすることはありません。

続いて、データの読み出しを見てみましょう。同期が必要なのでmLockのsynchronizedで囲まれています。

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

データの読み出し自体はMap<String, Object>から読み出して、キャストしているので、オンメモリで行われ、ブロックする要素はないですね。awaitLoadedLockedを調べてみましょう。

SharedPreferencesImpl.java
@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);
    }
}

はい、コンストラクタで呼び出した読み出しスレッドの完了を待っています。このスレッドではIO処理を行っているわけではありませんが、IO処理を行っているスレッドの完了を待っているので、ここでUIスレッドがブロックされます。このタイミングでThreadPolicyのReadFromDiskを通知していますね。

読み出し処理によるUIスレッドのブロック

以上の調査から、SharedPreferencesの保存したファイルの読み出し、パース処理は独立したスレッドで実行される。ただし、データの読み出し処理は読み出しスレッドの完了を待つ必要があり、読み出し処理の完了を知る手段もないため、読み出し処理が完了するまでの間にデータを読み出そうとするとIO処理の完了までUIスレッドがブロックされることが分かりました。
ただし、一回読み出しが完了してしまえばブロックされる要素がなくなるためUIスレッドをブロックする可能性があるのは初回の読み出しのみということになります。

書き出し処理

続いて、書き出し処理を調べて見ましょう。SharedPreferencesの変更のためにはSharedPreferences.edit()をコールして、Editorを取得し、Editorに対して変更を行います。

SharedPreferencesImpl.java
@Override
public Editor edit() {
    // TODO: remove the need to call awaitLoadedLocked() when
    // requesting an editor.  will require some work on the
    // Editor, but then we should be able to do:
    //
    //      context.getSharedPreferences(..).edit().putString(..).apply()
    //
    // ... all without blocking.
    synchronized (mLock) {
        awaitLoadedLocked();
    }

    return new EditorImpl();
}

実体はEditorImplですね。

SharedPreferencesImpl.java
public final class EditorImpl implements Editor {
    private final Object mEditorLock = new Object();

    @GuardedBy("mEditorLock")
    private final Map<String, Object> mModified = new HashMap<>();

    @GuardedBy("mEditorLock")
    private boolean mClear = false;

    @Override
    public Editor putString(String key, @Nullable String value) {
        synchronized (mEditorLock) {
            mModified.put(key, value);
            return this;
        }
    }

EditorImplの中では、SharedPreferencesで保持しているMapを変更するのではなく、変更要素だけを一旦保存していく仕組みですね。
実際の反映はapply()をコールした場合ですね。(commit()については、apply()の完了待ち合わせが行われるだけなので割愛)
ここが本題なので丁寧に追っていきましょう。

SharedPreferencesImpl.java
@Override
public void apply() {
    final long startTime = System.currentTimeMillis();

    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }

                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    // Okay to notify the listeners before it's hit disk
    // because the listeners should always get the same
    // SharedPreferences instance back, which has the
    // changes reflected in memory.
    notifyListeners(mcr);
}

apply メモリー上への変更の反映

まずは、commitToMemory()メモリー上の変化の反映をしているみたいですね。排他のための処理も含まれるためややこしいです。
OnSharedPreferenceChangeListenerをコールする処理があったりしますが、これに関しては、へー、このタイミングでコールされるんだー程度で流します。

SharedPreferencesImpl.java
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    boolean keysCleared = false;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String, Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        // We optimistically don't make a deep copy until
        // a memory commit comes in when we're already
        // writing to disk.
        if (mDiskWritesInFlight > 0) {
            // We can't modify our mMap as a currently
            // in-flight write owns it.  Clone it before
            // modifying it.
            // noinspection unchecked
            mMap = new HashMap<String, Object>(mMap);
        }
        mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            keysModified = new ArrayList<String>();
            listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }

        synchronized (mEditorLock) {
            boolean changesMade = false;

            if (mClear) {
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.clear();
                }
                keysCleared = true;
                mClear = false;
            }

            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                // "this" is the magic value for a removal mutation. In addition,
                // setting a value to "null" for a given key is specified to be
                // equivalent to calling remove on that key.
                if (v == this || v == null) {
                    if (!mapToWriteToDisk.containsKey(k)) {
                        continue;
                    }
                    mapToWriteToDisk.remove(k);
                } else {
                    if (mapToWriteToDisk.containsKey(k)) {
                        Object existingValue = mapToWriteToDisk.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mapToWriteToDisk.put(k, v);
                }

                changesMade = true;
                if (hasListeners) {
                    keysModified.add(k);
                }
            }

            mModified.clear();

            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }

            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
            listeners, mapToWriteToDisk);
}

まず最初にやっているのが、先行の書き込み処理とのコンフリクト対策ですね。

SharedPreferencesImpl.java
// We optimistically don't make a deep copy until
// a memory commit comes in when we're already
// writing to disk.
if (mDiskWritesInFlight > 0) {
    // We can't modify our mMap as a currently
    // in-flight write owns it.  Clone it before
    // modifying it.
    // noinspection unchecked
    mMap = new HashMap<String, Object>(mMap);
}
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;

書き込み処理が別に走っている場合、そちらが参照しているデータを変更してしまうと不整合が発生してしまいますので、mMapのコピーを作成し、参照の差し替えを行っています。先行の書き込み処理はmapToWriteToDiskに保持しているコピー元のmMapを参照しているため、以降の処理が先行の処理に影響を与えないようにしているわけですね。ただ、コピーはコストが高いのであくまでコンフリクトの可能性がある場合だけってことですね。

その次はListenerの処理なので飛ばして、mEditorLockの中ですね。

SharedPreferencesImpl.java
if (mClear) {
    if (!mapToWriteToDisk.isEmpty()) {
        changesMade = true;
        mapToWriteToDisk.clear();
    }
    keysCleared = true;
    mClear = false;
}

clearがコールされていた場合ですね。空にしてしまいます。
続いて、Editorに対して行われた変更のうち、反映すべきものを調べています。

SharedPreferencesImpl.java
for (Map.Entry<String, Object> e : mModified.entrySet()) {
    String k = e.getKey();
    Object v = e.getValue();
    // "this" is the magic value for a removal mutation. In addition,
    // setting a value to "null" for a given key is specified to be
    // equivalent to calling remove on that key.
    if (v == this || v == null) {
        if (!mapToWriteToDisk.containsKey(k)) {
            continue;
        }
        mapToWriteToDisk.remove(k);
    } else {
        if (mapToWriteToDisk.containsKey(k)) {
            Object existingValue = mapToWriteToDisk.get(k);
            if (existingValue != null && existingValue.equals(v)) {
                continue;
            }
        }
        mapToWriteToDisk.put(k, v);
    }

    changesMade = true;
    if (hasListeners) {
        keysModified.add(k);
    }
}

v == thisremove()をコールした場合にvalueにthisを格納して、削除フラグとして利用しているようですね。nullをputされた場合も削除として扱うようです。
削除の場合ですでに値がない場合、書き込みの場合で格納されている値と同じ場合、何も行いません。余計な書き込みをしないようにSharedPreferencesにすでにある値と同じかどうかをアプリでチェックする必要はなさそうです。
また、clearの処理が先頭で行われているので、editのあとputしてからclearした場合、先のputの結果はclearによって消されることはなく書き込まれるようですね。普通そういうことはしないと思いますが。

SharedPreferencesImpl.java
        mModified.clear();

        if (changesMade) {
            mCurrentMemoryStateGeneration++;
        }

        memoryStateGeneration = mCurrentMemoryStateGeneration;
    }
}
return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
        listeners, mapToWriteToDisk);

最後に、このEditorによって変更が行われる場合、世代をインクリメントして最後に結果をMemoryCommitResultという形で返しています。

Mapのコピーなどやや重い処理をsynchronizedで囲っているとはいえ、全部オンメモリでの操作なのでここまではANRが発生するほどの要素はなさそうですね。

apply 書き込み処理のリクエスト

メモリー上への反映の後の処理を見ていきましょう、またapply()に戻ります。

SharedPreferencesImpl.java
@Override
public void apply() {
    final long startTime = System.currentTimeMillis();

    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }

                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    // Okay to notify the listeners before it's hit disk
    // because the listeners should always get the same
    // SharedPreferences instance back, which has the
    // changes reflected in memory.
    notifyListeners(mcr);
}

awaitCommitpostWriteRunnableという2つのRunnableを作っています。
これらは、MemoryCommitResult.setDiskWriteResult()がコールされたことを確認するためのCoundDownLatchを待つための処理です。
awaitCommitQueuedWorkFinisherとして登録されます。postWriteRunnableawaitCommitを実行後、QueuedWorkから削除する処理が追加されています。
通常であれば書き込みからsetDiskWriteResultのコール、ラッチの待ち合わせとシーケンシャルに行われるので無駄な待ち合わせに見えますね(伏線)。
この部分は置いておいて先を読んでいきましょう。
最終的にenqueueDiskWriteをコールしています。いよいよ書き込み処理に近づいてきましたね。postWriteRunnableは名前の通り、書き込み処理が終わったらコールされる処理のようですね。

enqueueDiskWrite

enqueueDiskWriteの中を読んでいきましょう。

SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

最初にpostWriteRunnableのnull判定をしています。commitの場合、これがnullになるのですね。
次にwriteToDiskRunnableというRunnableを作っています。writeToFileという書き込み処理を行い、終わったら、commitToMemoryでインクリメントしたmDiskWritesInFlightをデクリメントして、書き込み処理中を解除、postWriteRunnableを実行するといった流れです。
このRunnableはapplyの場合、queueに積まれますが、commitの場合で、他に書き込み処理が実行されていない場合、このスレッド上でrun()をコールしてしまいます。applyの書き込み処理が走っている場合は、そちらの書き込みより先に実行してしまうと問題が起こるのでQueueに積まれます。

先にwriteToFileの中を見てみましょう。
書き込む必要が無い場合はスキップするとか、書き込み中の処理中断時用のバックアップファイルをつくっているとか、ありますが、やっていることは、データをXMLフォーマットで書き出しているだけです。そして一部抜粋。

SharedPreferencesImpl.java
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
str.close();

FileUtils.sync()単にファイルに書き出すだけでなくfsyncも実行して書き込みを確定していますね。これをUIスレッドで実行するのであれば、確かに最初の説明通りです。

では、QueuedWork.queueの中を見てみましょう。

QueuedWork.java
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static void queue(Runnable work, boolean shouldDelay) {
    Handler handler = getHandler();

    synchronized (sLock) {
        sWork.add(work);

        if (shouldDelay && sCanDelay) {
            handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
        } else {
            handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
        }
    }
}

shouldDelayapplyのときtrue、commitのときfalseで、applyの場合に100msのディレイがあるという違いはありますが、やっていることは同じです。sWorkというqueue(実体はLinkedList)にRunnableを追加して、Handlerにメッセージを投げています。

Handlerを作っている箇所が以下

QueuedWork.java
@UnsupportedAppUsage
private static Handler getHandler() {
    synchronized (sLock) {
        if (sHandler == null) {
            HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                    Process.THREAD_PRIORITY_FOREGROUND);
            handlerThread.start();

            sHandler = new QueuedWorkHandler(handlerThread.getLooper());
        }
        return sHandler;
    }
}

新規にHandlerThreadをつくって、そのLooperを使うQueuedWorkHandlerのインスタンスをHandlerとしている。

QueuedWorkHandlerとは以下で、先ほどのメッセージを受けてprocessPendingWorkを実行するだけのHandlerですね。

QueuedWork.java
private static class QueuedWorkHandler extends Handler {
    static final int MSG_RUN = 1;

    QueuedWorkHandler(Looper looper) {
        super(looper);
    }

    public void handleMessage(Message msg) {
        if (msg.what == MSG_RUN) {
            processPendingWork();
        }
    }
}

processPendingWorkは以下のようにsWorkの中身を全部実行するだけ。

QueuedWork.java
private static void processPendingWork() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }

    synchronized (sProcessingWork) {
        LinkedList<Runnable> work;

        synchronized (sLock) {
            work = sWork;
            sWork = new LinkedList<>();
            // Remove all msg-s as all work will be processed now
            getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
        }

        if (work.size() > 0) {
            for (Runnable w : work) {
                w.run();
            }
            if (DEBUG) {
                Log.d(LOG_TAG, "processing " + work.size() + " items took " +
                        +(System.currentTimeMillis() - startTime) + " ms");
            }
        }
    }
}

以上!

UIスレッドをブロックしている犯人

あれ?書き込み処理はUIスレッドで実行してないし、UIスレッド待たせてないじゃん? って思いましたが違います。

waitToFinishというメソッドがあって、processPendingWorkをこのスレッド内で実行しています。
しかも、StrictModeを回避しているという。
Finisherというのはここで出てきます。HandlerThreadの方で実行が進んでいる場合に、その完了を待つために使われているわけですね。当然、そちらで実行されていても、このメソッドのコールスレッドがブロックされることになります。

QueuedWork.java
public static void waitToFinish() {
    long startTime = System.currentTimeMillis();
    boolean hadMessages = false;
    Handler handler = getHandler();
    synchronized (sLock) {
        if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
            // Delayed work will be processed at processPendingWork() below
            handler.removeMessages(QueuedWorkHandler.MSG_RUN);
            if (DEBUG) {
                hadMessages = true;
                Log.d(LOG_TAG, "waiting");
            }
        }
        // We should not delay any work as this might delay the finishers
        sCanDelay = false;
    }

    StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
    try {
        processPendingWork();
    } finally {
        StrictMode.setThreadPolicy(oldPolicy);
    }

    try {
        while (true) {
            Runnable finisher;
            synchronized (sLock) {
                finisher = sFinishers.poll();
            }
            if (finisher == null) {
                break;
            }
            finisher.run();
        }
    } finally {
        sCanDelay = true;
    }

    synchronized (sLock) {
        long waitTime = System.currentTimeMillis() - startTime;
        if (waitTime > 0 || hadMessages) {
            mWaitTimes.add(Long.valueOf(waitTime).intValue());
            mNumWaits++;

            if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
                mWaitTimes.log(LOG_TAG, "waited: ");
            }
        }
    }
}

このメソッドはActivityThreadからコールされています。

ActivityThread.java
@Override
public void handleStopActivity(ActivityClientRecord r, int configChanges,
        PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
    r.activity.mConfigChangeFlags |= configChanges;

    final StopInfo stopInfo = new StopInfo();
    performStopActivityInner(r, stopInfo, true /* saveState */, finalStateRequest,
            reason);

    if (localLOGV) Slog.v(
        TAG, "Finishing stop of " + r + ": win=" + r.window);

    updateVisibility(r, false);

    // Make sure any pending writes are now committed.
    if (!r.isPreHoneycomb()) {
        QueuedWork.waitToFinish();
    }

    stopInfo.setActivity(r);
    stopInfo.setState(r.state);
    stopInfo.setPersistentState(r.persistentState);
    pendingActions.setStopInfo(stopInfo);
    mSomeActivitiesChanged = true;
}

一例はhandleStopActivityです。performStopActivityInnerをコールしているように、ActivityのonStopを実行しているメソッドですね。他にもhandleServiceArgs(ServiceのonStartCommandを実行)、handleStopService(ServiceのonDestroyを実行)などでコールされていますね。

fsync() 呼び出しの保留は、サービスの開始と停止、およびアプリケーションの任意の場所でアクティビティが開始または停止するたびにトリガーされます。

ギャー、そういうことだったのかー

まとめ

まとめると

  • SharedPreferencesのインスタンスが作られると、独立したスレッドでXMLの読み出しが行われる。

    • 読み出しが完了する前にSharedPreferencesアクセスすると呼び出しスレッドがブロックされる
  • SharedPreferencesを変更すると、ワーカースレッドでXMLの書き込みが遅延実行される

    • apply/commitを複数回コールすると、その回数分XMLの全書き込みが行われる
    • ActivityのonStop・ServiceのonStartCommand/onDestroyなどのタイミングで
      • 書き込み処理が実行中だった場合、完了までUIスレッドをブロックする
      • キューに書き込みタスクが残っている場合、UIスレッドで残タスクを実行する

といったところでしょうか。

SharedPreferencesがどういう動きをしているのかよく分かりましたね。
SharedPreferencesは、ANRの危険はありつつも、今までずっと使われてきた仕組みなので、使い続けることが悪いというわけでもなく、絶対にDataStoreに移行しなければならないというものでもないと思います。
使い続けるならこの辺の特性を理解した上で、下手な使い方はしないように注意していきましょう。

以上です。

17
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
9