[Framework] 深入理解 Android SharedPreferences

[Framework] 深入理解 Android SharedPreferences

SharedPreferencesAndroid 中用来本地化存储简单 key-value 的工具,在开发过程中经常用来存放一些用户的一些简单设置。 Android 给我们写了一个非常优秀的样板代码来存储本地数据,还是非常有价值来学习下。

源码基于 Android 31

读取数据

我们使用 SP 都是 Context#getSharedPreferences() 来获取 SP 实例,最终都会调用到 ContextImpl#getSharedPreperences(),然后我们也以这个函数为我们分析的入口函数。

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

通过 name 来获取 SP 对应的文件,如果没有获取到就通过 getSharedPreferencesPath() 方法来创建一个文件,最后调用 getSharedPreferences() 方法来获取 SP 对象。

来看看 getSharedPreferencesPath() 创建文件的实现:

Java 复制代码
@Override
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}

@UnsupportedAppUsage
private File getPreferencesDir() {
    synchronized (mSync) {
        if (mPreferencesDir == null) {
            mPreferencesDir = new File(getDataDir(), "shared_prefs");
        }
        return ensurePrivateDirExists(mPreferencesDir);
    }
}

private File makeFilename(File base, String name) {
    if (name.indexOf(File.separatorChar) < 0) {
        final File res = new File(base, name);
        // We report as filesystem access here to give us the best shot at
        // detecting apps that will pass the path down to native code.
        BlockGuard.getVmPolicy().onPathAccess(res.getPath());
        return res;
    }
    throw new IllegalArgumentException(
            "File " + name + " contains a path separator");
}

上面的方法很简单,就是在 /data/data/<pakcage_name>/shared_prefs 目录下面创建一个 SP 的本地文件,文件名称是 name + .xml

getSharedPreferences() 方法获取 SP 对象:

Java 复制代码
@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);
            // ...
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
        // ...
        return sp;
}

以文件为 key 去查询有没有 SharedPreferencesImpl 对象,如果没有就创建一个。

SharedPreferencesImpl 构造函数:

Java 复制代码
@UnsupportedAppUsage
SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    startLoadFromDisk();
}

构造函数非常简单,简单说一下其中重要的成员变量,mFile 就是对应的 SP 本地文件;mBackupFile 备份文件,在写之前都会把之前的文件存储为备份,如果写入失败,后续就继续读这个备份文件,写入成功就删除备份文件,他的名字是源文件加上 .bak 后缀; mLoaded 表示是否已经把本地文件加载到内存中了;mMap 内存中存放的数据;最后执行 startLoadFromDisk() 方法从文件中加载数据。

继续看看关键的 startLoadFromDisk() 方法。

Java 复制代码
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

直接新建一个线程调用 loadFromDisk() 方法。

Java 复制代码
private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    // Debugging
    if (mFile.exists() && !mFile.canRead()) {
        Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
    }

    Map<String, Object> map = null;
    StructStat stat = null;
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                map = (Map<String, Object>) XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        // An errno exception means the stat failed. Treat as empty/non-existing by
        // ignoring.
    } catch (Throwable t) {
        thrown = t;
    }

    synchronized (mLock) {
        mLoaded = true;
        mThrowable = thrown;

        // It's important that we always signal waiters, even if we'll make
        // them fail with an exception. The try-finally is pretty wide, but
        // better safe than sorry.
        try {
            if (thrown == null) {
                if (map != null) {
                    mMap = map;
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                    mMap = new HashMap<>();
                }
            }
            // In case of a thrown exception, we retain the old map. That allows
            // any open editors to commit and store updates.
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
            mLock.notifyAll();
        }
    }
}

刚开始会判断备份文件是否存在,如果存在就表示上次写入的时候失败了,就直接使用备份文件的数据当成新的文件,然后加载。

通过 XmlUtils#readMapXml() 方法读取文件流中的 xml,以 Map 的格式返回,最后存储在 mMap 成员变量中。

最后会通过 mLock.notifyAll() 通知其他还在等待加载完成的方法,比如读取的时候就需要等待加载完成,后续会讲到。

再看看读取数据的方法,我以 getString() 方法作为例子,其他的方法都是大同小异:

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

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

获取前会通过自旋的方式去检查加载的状态,加载成功后直接,从内存中的数据去读取。

写入数据

写入数据的时候需要通过 SharedPreferences#edit() 方法返回一个 Editor 对象,然后通过 Editor 对象进行写,写完了以后调用 commit() 或者 apply() 方法提交,最后写入到本地文件中。

先看看 SharedPreferencesImpl#edit() 方法:

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 实例返回,EditorImplSharedPreferencesImpl 中的内部类。

我们以 EditorImpl#putString()EditorImpl#remove()EditorImpl#clear() 方法为例子看看写入,删除和清空,其他写入的方法也都是大同小异。

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

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

@Override
public Editor clear() {
    synchronized (mEditorLock) {
        mClear = true;
        return this;
    }
}

插入或者修改数据会直接把值放在 mModified 中,删除的时候直接把对应 keyvalue 设置成 EditorImpl 自己,清空数据只是加一个 mClear 变量标识。

我们一同看看提交修改的 commit()apply() 方法:

Java 复制代码
@Override
public boolean commit() {
    long startTime = 0;

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

    MemoryCommitResult mcr = commitToMemory();

    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
        if (DEBUG) {
            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                    + " committed after " + (System.currentTimeMillis() - startTime)
                    + " ms");
        }
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}


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

commit()apply() 方法都是大同小异,不同的是 commit() 是同步写入,apply() 是异步写入。他们都会调用 commitToMemory() 方法把 EditorImpl 中修改的数据提交到内存中,然后调用 SharedPreferencesImpl#enqueueDiskWrite() 方法同步到本地文件。只是 commit() 没有传递 Runnable 对象,而 apply() 有传递,在这个 Runnable 对象中会等待修改完成,没有完成的可以在 QueuedWork 中查询对应的 Runnable

看看如何把修改同步到内存中去:

Kotlin 复制代码
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);
}

这里有一个非常重要的参数 mDiskWritesInFlight 变量,当它大于 0 的时候就表示有其他的写入还没有完成,后续会再讲到这个参数。

首先判断是否要清空数据,如果要清空数据就把以前的数据清空。

然后遍历 EditorImpl 中修改的数据,如果 value 值为空或者为 EditorImpl 对象时就表示要删除这条数据,反之就是更新数据或者插入数据。最后就得到了更新后的数据 mapToWriteToDisk,也就是 mMap 内存中的数据,最后会把更新后等待写入到本地文件中的数据添加到 MemoryCommitResult 中供后续写入到文件中使用。

然后再来看看 SharedPreferences#enqueueDiskWrite() 是如何写入修改后的文件到磁盘的:

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

最终执行写文件的任务是 writeToDiskRunnable,如果是异步调用或者如果有其他提交还没有修改完(异步通过 postWriteRunnable 判断,是否有其他的修改没有完成通过 mDiskWritesInFlight 判断),就会用其他线程异步执行。 反之就在当前线程直接执行。执行写文件的方法是 writeToFile()

Java 复制代码
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    // ...
    
    try {
        FileOutputStream str = createFileOutputStream(mFile);
        // ...
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        
        // ...

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

        // ...

        mcr.setDiskWriteResult(true, true);
        
        // ...

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

我省略了部分代码,留下关键代码,通过 XolUtils.writeMapXml() 方法把更新后的数据写入到文件中,更新成功后会把结果写入到结果中,然后删除备份文件,如果更新失败会删除更新失败的文件。

总结

每个不同 SPname 都对应一个不同的本地文件,每次使用时都会去检查是否有加载到内存中,如果没有加载就会从本地去加载然后保存到内存中(没有回收机制),以后再使用就不用再加载。 如果要执行读写操作的话,也必须阻塞等待加载完成。读取操作性能比较好,是直接读取内存中的数据。写操作需要先更新内存中的数据,然后把更新的数据全部重新再写人本地文件。

根据 SP 的各种特点我们在开发中需要注意以下事项:

  • 同一个 SP 文件存放的数据不宜过大,过大会导致初次加载慢,写入的数据也慢,占用的内存也多,一个 SP 文件存放的数据过大可以考虑根据业务逻辑拆分成多个 SP 文件。如果是存放的单条 key-value 过大,可以考虑自己写一个本地文件缓存,如果是有非常多条的 key-value 也可以考虑使用数据库。

  • 在获取 SP 对象时是不会阻塞线程的,但是在使用它时会有可能会阻塞,因为需要等待本地文件加载完成,所以使用时最好能够在后台线程操作。

  • 写入有通过 commitapply 两种方式提交数据,commit 是同步等待整个 SP 文件写入完成,apply 是异步,他们都有可能造成卡顿,甚至 ANR,虽然 apply 是异步的,但是在某些组件的生命周期中也会检查没有完成的 SPapply 任务,长时间没有完成也会 ANRcommit 方法强烈建议在后台线程中执行,这样就不会导致 ANR

相关推荐
烬奇小云3 小时前
认识一下Unicorn
android·python·安全·系统安全
顾北川_野15 小时前
Android 进入浏览器下载应用,下载的是bin文件无法安装,应为apk文件
android
CYRUS STUDIO15 小时前
Android 下内联汇编,Android Studio 汇编开发
android·汇编·arm开发·android studio·arm
右手吉他15 小时前
Android ANR分析总结
android
PenguinLetsGo17 小时前
关于 Android15 GKI2407R40 导致梆梆加固软件崩溃
android·linux
杨武博19 小时前
音频格式转换
android·音视频
音视频牛哥21 小时前
Android音视频直播低延迟探究之:WLAN低延迟模式
android·音视频·实时音视频·大牛直播sdk·rtsp播放器·rtmp播放器·android rtmp
ChangYan.21 小时前
CondaError: Run ‘conda init‘ before ‘conda activate‘解决办法
android·conda
二流小码农21 小时前
鸿蒙开发:ForEach中为什么键值生成函数很重要
android·ios·harmonyos
夏非夏1 天前
Android 生成并加载PDF文件
android