Android SharedPreferences源码分析

SharedPreferences源码分析

官方提供的保存键值对的方式,

支持存储类型

  1. null
  2. String
  3. 基本数据类型
  4. 基本数据类型或String数组
  5. list
  6. map
  7. set

类型源码:XmlUtils#readThisValueXml

文件实例

用法

// 读
context.getSharedPreferences("test", Context.MODE_PRIVATE).getString("test", "defValue");
// 写
context.getSharedPreferences("test", Context.MODE_PRIVATE).edit().putString("key", "value").commit();
context.getSharedPreferences("test", Context.MODE_PRIVATE).edit().putString("key", "value").apply();

Context

CountDownLatch用法示例

// chatgpt生成
import java.util.concurrent.CountDownLatch;

public class MyThread extends Thread {
    private CountDownLatch latch;

    public MyThread(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        // 执行一些操作

        // 操作完成后,调用countDown()方法减少latch的计数器
        latch.countDown();
    }
}

public class MainActivity extends AppCompatActivity {
    private CountDownLatch latch;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        int threadCount = 3; // 设置线程数量

        // 创建CountDownLatch,设置计数器为线程数量
        latch = new CountDownLatch(threadCount);

        // 启动线程
        for (int i = 0; i < threadCount; i++) {
            MyThread thread = new MyThread(latch);
            thread.start();
        }

        try {
            // 调用await()方法阻塞主线程,直到计数器变为0
            latch.await();
            // 所有线程操作完成后,继续执行主线程的代码
            // ...
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

getSharedPrefrences

从上面Context的结构可以看出,Context.getSharedPreferences都会调用到ContextImpl.getSharedPreferences,也就是:

sSharedPrefsCache到mSharedPrefsPaths

@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) {
        		// 检查mode,不允许MODE_WORLD_READABLE或MODE_WORLD_WRITEABLE
            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();
    // 以包名作为key从静态变量map中尝试取出SharedPreferencesImpl实例
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
}

mode支持传入四种

MODE_PRIVATE
MODE_WORLD_READABLE
MODE_WORLD_WRITEABLE
MODE_MULTI_PROCESS

传入MODE_WORLD_READABLE或者MODE_WORLD_WRITEABLE抛出异常:

private void checkMode(int mode) {
    if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
        if ((mode & MODE_WORLD_READABLE) != 0) {
            throw new SecurityException("MODE_WORLD_READABLE no longer supported");
        }
        if ((mode & MODE_WORLD_WRITEABLE) != 0) {
            throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
        }
    }
}

MODE_MULTI_PROCESS注释中描述用于target sdk Android2.3以上,有些Android版本不生效且认为多进程不应该提供这种机制被弃用。

总结:

静态变量sSharedPrefsCache以包名做key

private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl» sSharedPrefsCache;

ContextImpl成员变量mSharedPrefsPaths以"app路径/shared_prefs/name.xml"做key

private ArrayMap<String, File> mSharedPrefsPaths;

从读开始,既没有写复杂,又衔接上写部分的内容

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

涉及到的变量:

mFile:sp对应xml文件

mBackupFile:写入sp后保存

mMode:模式,上面说过

mLoaded:标识有没有加载过

mMap:存储着要取出来的键值对

mThrowable:保存从磁盘加载时的异常

mLock:用来加锁的对象

startLoadFromDisk:读取xml文件解析到mMap中,保存文件上次修改时间和文件大小

所有类型流程都是类似的,解析xml文件得到类型,以String为例:

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

sp构造方法中异步读取xml文件并尝试解析,有可能get时还没有结束,这时要等到结束才能继续下一步(不然从mMap取不到)

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

整理一下:

  1. mMap中存着所有的键值对

保存一个键值对时首先调用edit()方法:

@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对象,EditorImpl中只有三个成员变量。分别是:

  • mEditorLock:往map中添加时防止异步添加两个相同的键值对时有丢失
  • mModified:需要修改的键值对
  • mClear:表示有没有被清空

上面get是String,这里也对应看String:

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

只是添加到mModified中,同时支持链式调用。既然这样,那实际的逻辑一定在commit和apply。

commit

先看commit:

@Override
public boolean commit() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }
    // 返回了MemoryCommitResult对象
    MemoryCommitResult mcr = commitToMemory();
    //
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
    		// 开始的CountDownLatch,要countdown后才能往下走
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
        if (DEBUG) {
            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                    + " committed after " + (System.currentTimeMillis() - startTime)
                    + " ms");
        }
    }
    // notify listeners
    notifyListeners(mcr);
    // 返回结果
    return mcr.writeToDiskResult;
}

为什么返回的是mcr.writeToDiskResult?

       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);
        }

从这可以看出来:

  1. MemoryCommitResult内部的map是在mMap的基础上,过滤了与存在了的相同的键值对得来的。
  2. 只有MemoryCommitResult#setDiskWriteResult中对writeToDiskResult赋值,所有调用处都是在实际的写入方法writeToFile中。
  3. setDiskWriteResult中调用writtenToDiskLatch.countDown(),commit方法可以继续往下执行。

第二条信息回应了上面为什么需要等到解析完成了再创建对象,写入时用到了这些已经存在的键值对。

接着看加入队列,enqueueDiskWrite:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    // 上面的第二个参数Runnable是null,这种情况都是commit来的
    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();
                }
            }
        };

    // 唯一需要写入的,直接run方法不会创建新线程,这时候在主线程写入。
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    // 加入队列,异步写入
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

除开在主线程写入的情况,其他都需要添加到QueuedWork中:

    /**
     * Queue a work-runnable for processing asynchronously.
     *
     * @param work The new runnable to process
     * @param shouldDelay If the message should be delayed
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public static void queue(Runnable work, boolean shouldDelay) {
      	// 返回的对象是一个handler,创建对象的参数Loop是handlerThread创建的子线程的。
        Handler handler = getHandler();

        synchronized (sLock) {
          	// 加到链表
            sWork.add(work);

          	// 发送消息,sCanDelay留坑
            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
             		// commit一定走这里
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

MSG_RUN消息:

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

执行任务:

    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) {
                  	// 虽然是调用run,运行确实在子线程。
                    w.run();
                }

                if (DEBUG) {
                    Log.d(LOG_TAG, "processing " + work.size() + " items took " +
                            +(System.currentTimeMillis() - startTime) + " ms");
                }
            }
        }
    }

运行了,追到:

final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
    @GuardedBy("mWritingToDiskLock")
    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
      	// ...
        boolean fileExists = mFile.exists();

        // ...

        // Rename the current file so it may be used as a backup during the next read
        if (fileExists) {
            boolean needsWrite = false;

            // Only need to write if the disk state is older than this commit
          	// 前提条件是Editor中mModified变化,与MemoryCommitResult创建时不一致。
            if (mDiskStateGeneration < mcr.memoryStateGeneration) {
                if (isFromSyncCommit) {
                    needsWrite = true;
                } else {
                    synchronized (mLock) {
                        // No need to persist intermediate states. Just wait for the latest state to
                        // be persisted.
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                            needsWrite = true;
                        }
                    }
                }
            }

            if (!needsWrite) {
                mcr.setDiskWriteResult(false, true);
                return;
            }

            boolean backupFileExists = mBackupFile.exists();

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

            if (!backupFileExists) {
                if (!mFile.renameTo(mBackupFile)) {
                    Log.e(TAG, "Couldn't rename file " + mFile
                          + " to backup file " + mBackupFile);
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
            } else {
                mFile.delete();
            }
        }

        // Attempt to write the file, delete the backup and return true as atomically as
        // possible.  If any exception occurs, delete the new file; next time we will restore
        // from the backup.
        try {
            FileOutputStream str = createFileOutputStream(mFile);

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

            if (str == null) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

            writeTime = System.currentTimeMillis();

            FileUtils.sync(str);

            fsyncTime = System.currentTimeMillis();

            str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

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

            try {
                final StructStat stat = Os.stat(mFile.getPath());
                synchronized (mLock) {
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                }
            } catch (ErrnoException e) {
                // Do nothing
            }

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

            // Writing was successful, delete the backup file if there is one.
            mBackupFile.delete();

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

            mDiskStateGeneration = mcr.memoryStateGeneration;

            mcr.setDiskWriteResult(true, true);

            if (DEBUG) {
                Log.d(TAG, "write: " + (existsTime - startTime) + "/"
                        + (backupExistsTime - startTime) + "/"
                        + (outputStreamCreateTime - startTime) + "/"
                        + (writeTime - startTime) + "/"
                        + (fsyncTime - startTime) + "/"
                        + (setPermTime - startTime) + "/"
                        + (fstatTime - startTime) + "/"
                        + (deleteTime - startTime));
            }

            long fsyncDuration = fsyncTime - writeTime;
            mSyncTimes.add((int) fsyncDuration);
            mNumSync++;

            if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
                mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
            }

            return;
        } catch (XmlPullParserException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        } catch (IOException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        }

        // Clean up an unsuccessfully written file
        if (mFile.exists()) {
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        mcr.setDiskWriteResult(false, false);
    }

writeToFile主要做了几件事:

  1. 把map转化成xml格式,写入
  2. 更新mStatTimestamp和mStatSize状态
  3. 调用setDiskWriteResult,更新了写入文件结果同时commit中继续执行,能执行下一次提交。

apply

相较commit,enqueueDiskWrite传入了第二个参数postWriteRunnable,其他部分相同。postWriteRunnable对应代码:

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);
                    }
                };

Runnable不为null,isFromSyncCommit为false,所有的任务都要到子线程执行。

QueuedWork#queue中shouldDelay为true,当shouldDelay和sCanDelay同时为true执行

handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);

sCanDelay的初始值就是true,改变它的值都在waitToFinish方法中,发现是找不得调用的。官方在注释中给我们的解释: 1)

    /**
     * Trigger queued work to be processed immediately. The queued work is processed on a separate
     * thread asynchronous. While doing that run and process all finishers on this thread. The
     * finishers can be implemented in a way to check weather the queued work is finished.
     *
     * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
     * after Service command handling, etc. (so async work is never lost)
     */

防止丢失异步任务。

开始遍历执行任务时赋值false,这时apply发送消息的不会延迟,执行结束后赋值true,又会延迟。为什么会反常呢?

waitToFinish是要防止丢失任务,在保存的时候来了一条新的任务,执行了才能存下来这次的数据。

anr原因

剖析 SharedPreference apply 引起的 ANR 问题 - 字节跳动团队

SharedPreferences ANR问题分析 & Android8.0的优化