美文网首页程序员
【Android】深入解析 SharedPreferences

【Android】深入解析 SharedPreferences

作者: Tyhoo_Wu | 来源:发表于2020-11-03 21:15 被阅读0次

    SharedPreference是一个 Android 开发自带的适合保存轻量级数据的key-value
    存储库,它使用了xml的方式来存储数据,比如可以使用它保存一些如用户 登录信息等轻量级数据。

    官方文档:
    https://developer.android.google.cn/training/data-storage/shared-preferences

    注:本文以 API 30 的 SharedPreferences 源码进行解析。


    一、获取 SharedPreferences

    在使用SharedPreferences时首先是需要获取到这个SharedPreferences,因此我们首先从SharedPreferences的获取入手,来分析其源码。

    根据名称获取 SharedPreferences

    不论是在Activity中调用getPreferences方法还是调用ContextgetSharedPreferences方法,最终都是调用到了ContextImplgetSharedPreferences(String name, int mode)方法。

    注:ContextImpl 是 Context 的实现类。
    
    源码位置:
    /sdk/sources/android-30/android/app/ContextImpl.java
    

    我们先看看它的代码:

    public SharedPreferences getSharedPreferences(String name, int mode) {
        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) {
                // 调用 getSharedPreferencesPath 方法构建出 name 对应的 File
                file = getSharedPreferencesPath(name);
                // 将 file 放入 map
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }
    

    可以看到,它首先对 Android 4.4 以下的设备做了特殊处理,之后将对 mSharedPrefsPaths的操作加了锁。

    mSharedPrefsPaths的声明如下:

    private ArrayMap<String, File> mSharedPrefsPaths;
    

    可以看到它是一个以namekeyname对应的Filevalue
    HashMap

    获取 SharedPreferences 名称对应的 File 对象

    先看看是如何构建出name对应的File的:

    public File getSharedPreferencesPath(String name) {
        return makeFilename(getPreferencesDir(), name + ".xml");
    }
    

    可以看到,调用makeFilename方法来创建一个名为name.xmlFile

    makeFilename中仅仅是做了一些判断,之后new出了这个File对象并返回。

    private File makeFilename(File base, String name) {
        if (name.indexOf(File.separatorChar) < 0) {
            final File res = new File(base, name);
            BlockGuard.getVmPolicy().onPathAccess(res.getPath());
            return res;
        }
        throw new IllegalArgumentException(
                "File " + name + " contains a path separator");
    }
    

    可以看到,SharedPreference确实是使用xml来保存其中的key-value数据的。

    根据创建的 File 对象获取 SharedPreference

    接着看到获取到File并放入Map后调用的getSharedPreferences(file, mode) 方法:

    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            // 调用 getSharedPreferencesCacheLocked 来获取到一个 ArrayMap
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            // 从这个 Map 中尝试获取到对应的 SharedPreferencesImpl 实现类
            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");
                    }
                }
    
                // 当获取不到对应 SharedPreferencesImpl 时,再创建一个对应的 SharedPreferencesImpl,并将其加入这个 ArrayMap 中
                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) {
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }
    

    很明显是一个缓存机制的实现,以加快之后获取SharedPreference的速度,同时可以发现SharedPreference其实只是一个接口,而 SharedPreferencesImpl才是其具体的实现类。

    缓存机制

    进入getSharedPreferencesCacheLocked方法:

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

    显然,这里有个全局的sSharedPrefsCache,它是一个ArrayMap类型的Map。根据PackageName来保存不同的SharedPreference缓存Map的,通过这样的方式,就保证了不同PackageName中相同nameSharedPreference从缓存中拿到的数据是不同的。

    SharedPreferencesImpl 的创建

    cache中找不到对应的SharedPreferencesImpl时,就会new出一个 SharedPreferencesImpl,看看它的构造函数:

    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        // 调用 makeBackupFile 来进行备份文件的创建
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        // 调用 startLoadFromDisk 来开始从 Disk 载入信息
        startLoadFromDisk();
    }
    

    makeBackupFile方法:

    static File makeBackupFile(File prefsFile) {
        return new File(prefsFile.getPath() + ".bak");
    }
    

    返回一个同目录下的后缀名为 .bak 的同名文件对象。

    从 Disk 加载数据

    startLoadFromDisk方法:

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

    首先在加锁的情况下对mLoaded进行了修改,之后则开了个名为
    SharedPreferencesImpl-load的线程来加载数据。

    loadFromDisk方法:

    private void loadFromDisk() {
        synchronized (mLock) {
            // 如果已经加载过,则不再进行加载
            if (mLoaded) {
                return;
            }
    
            // 判断是否存在备份文件,若存在则将备份文件直接修改为数据文件 name.xml
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }
    
        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);
                    // 通过 XmlUtils 将 xml 中的数据读取为一个 Map
                    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) {
    
        } catch (Throwable t) {
            thrown = t;
        }
    
        // 进行一些收尾处理
        synchronized (mLock) {
            // 将 mLoaded 置为 true
            mLoaded = true;
            mThrowable = thrown;
    
            try {
                if (thrown == null) {
                    // 对 mMap 进行判空处理,以保证在 xml 没有数据的情况下其仍不为 null
                    if (map != null) {
                        mMap = map;
                        mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                        mMap = new HashMap<>();
                    }
                }
            } catch (Throwable t) {
                mThrowable = t;
            } finally {
                // 释放这个读取的锁,表示读取成功
                mLock.notifyAll();
            }
        }
    }
    

    二、编辑 SharedPreferences

    真正对SharedPreferences的操作其实都是在Editor中的,它其实是一个接口, 具体的实现类为EditorImpl

    获取 Editor

    看到SharedPreferencesImpledit方法:

    public Editor edit() {
        synchronized (mLock) {
            awaitLoadedLocked();
        }
    
        return new EditorImpl();
    }
    

    首先先调用了awaitLoadedLocked()方法来等待读取的完成,当读取完成后才会真正创建并返回EditorImpl对象。

    等待读取机制

    由于读取过程是一个异步的过程,很有可能导致读取还没结束,我们就开始编辑,因此这里用到了一个awaitLoadedLocked方法来阻塞线程,直到读取过程完成。

    awaitLoadedLocked方法:

    private void awaitLoadedLocked() {
        if (!mLoaded) {
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }
    

    可以看到,这里会阻塞直到mLoadedtrue,这样就保证了该方法后的方法都会在读取操作进行后执行。

    EditorImpl

    Editor的真正实现类是EditorImpl,它其实是SharedPreferencesImpl的一个内部类。它内部维护了一个mModifiedmModified是一个HashMap类型的Map。通过mModified来存放对SharedPreferences进行的操作,此时还不会提交到SharedPreferencesImpl中的mMap,我们做的操作都是在改变mModified

    下面列出一些EditorImpl对外提供的修改接口,其实都是在对mModified这个Map进行修改:

    public Editor putString(String key, @Nullable String value) {
        ...
    }
    
    public Editor putStringSet(String key, @Nullable Set<String> values) {
        ...
    }
    
    public Editor putInt(String key, int value) {
        ...
    }
    
    public Editor putLong(String key, long value) {
        ...
    }
    
    public Editor putFloat(String key, float value) {
        ...
    }
    
    public Editor putBoolean(String key, boolean value) {
        ...
    }
    
    public Editor remove(String key) {
        ...
    }
    
    public Editor clear() {
        ...
    }
    

    提交 SharedPreferences

    SharedPreferences的提交有两种方式:applycommit

    apply

    public void apply() {
        final long startTime = System.currentTimeMillis();
    
        // 调用 commitToMemory 方法,它内部就是将原先读取进来的 mMap 与刚刚修改过的 mModified 进行合并,并存储于返回的 MemoryCommitResult mcr 中
        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);
                }
            };
    
        // 调用 enqueueDiskWrite 方法,传入了之前构造的 Runnable 对象,这里的目的是进行一个异步的写操作。
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    
        notifyListeners(mcr);
    }
    

    也就是说,apply方法会将数据先提交到内存,再开启一个异步过程来将数据写入硬盘。

    commit

    public boolean commit() {
        long startTime = 0;
    
        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }
    
        // 和 apply 方法一样,调用 commitToMemory 方法进行了合并
        MemoryCommitResult mcr = commitToMemory();
    
        // 和 apply 方法一样,调用 enqueueDiskWrite 方法,不过传入的第二个不再是 Runnable 方法,
        // 通过源码注释 "if null, then we're allowed to do this disk write on the main thread",如果 enqueueDiskWrite 方法传入的第二个参数为 null,则会在当前线程执行写入操作
        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;
    }
    

    也就是说,commit方法会将数据先提交到内存,但之后则是一个同步的过程写入硬盘。

    同步数据至内存

    commitToMemory方法:

    private MemoryCommitResult commitToMemory() {
        long memoryStateGeneration;
        boolean keysCleared = false;
        List<String> keysModified = null;
        Set<OnSharedPreferenceChangeListener> listeners = null;
        Map<String, Object> mapToWriteToDisk;
    
        synchronized (SharedPreferencesImpl.this.mLock) {
            if (mDiskWritesInFlight > 0) {
                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();
    
                    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);
    }
    

    可以看到,将mMap的数据与mModified的数据进行了整合,之后将mModified重新清空。最后将合并的数据放入了MemoryCommitResult中。

    写入数据至硬盘

    enqueueDiskWrite方法:

    private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
        // 若第二个 Runnable 为 null 的话,则会将 isFromSyncCommit 置为 true,也就是写入会是一个同步的过程
        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();
                    }
                }
            };
    
    
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                // 进行同步写入
                writeToDiskRunnable.run();
                return;
            }
        }
    
        // 构造一个 Runnable 来提供给 QueueWork 进行异步写入
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }
    

    QueueWork类内部维护了一个single线程池,这样可以达到我们异步写入的目的。而writeToFile方法中其实就是又调用了之前的XmlUtils来进行xml的写入。


    总结

    SharedPreferences其实就是一个用使用xml进行保存的key-value存储库。在获取SharedPreferences时会进行数据的加载,将name对应的xml文件以Map的形式读入到内存。

    SharedPreferences的编辑操作其实都是在Editor内实现,它内部维护了一个新的Map,所有的编辑操作其实都是在操作这个Map,只有提交时才会与之前读取的数据进行合并。

    SharedPreferences的提交分为两种,applycommit,它们的特性如下:

    • apply
      • 会将数据先提交到内存,再开启一个异步过程来将数据写入硬盘
      • 返回值时可能写入操作还没有结束
      • 写入失败时不会有任何提示
    • commit
      • 会将数据先提交到内存,但之后则是一个同步的过程写入硬盘
      • 写入操作结束后才会返回值
      • 写入失败会有提示

    因此,当我们对写入的结果不那么关心的情况下,可以使用apply进行异步写入,而当我们对写入结果十分关心且提交后有后续操作的话最好使用commit来进行同步写入。


    © 2020 Tyhoo Wu, All rights reserved.

    相关文章

      网友评论

        本文标题:【Android】深入解析 SharedPreferences

        本文链接:https://www.haomeiwen.com/subject/zsksvktx.html