SharePreference使用以及原理简析

一、简介

  • SharedPreferences 是 Android 提供的数据持久化的一种手段,适合单进程、小批量的数据存储与访问。
  • 因为SharedPreferences的实现是基于单个xml文件实现的,并且,所有持久化数据都是一次性加载到内存,如果数据过大,是不合适采用 SharedPreferences存放的。
  • 而适用的场景是单进程的原因同样如此,由于Android原生的文件访问并不支持多进程互斥,所以SharePreferences 也不支持,如果多个进程更新同一个xml文件,就可能存在同不互斥问题。
  • SharePreferences 对应的xml文件位置一般都在/data/data/包名/shared_prefs目录下。

二、如何使用


//写入信息
//打开Preferences,名称为setting,如果存在则打开它,否则创建新的Preferences
SharedPreferences setting = getSharedPreferences("hello",MODE_PRIVATE);
//让setting处于编辑状态
SharedPreferences.Editor editor = setting.edit();
//存放数据
editor.putString("name","jacky");
//完成提交
editor.commit();
// editor.apply();

//读取信息
String name = setting.getString("name","0");

三、SPMode值说明

  • MODE_PRIVATE:文件是私有数据,只能被应用本身访问,在该模式下写入的内容会覆盖原文件的内容。
  • MODE_WORLD_READABLE:当前文件可以被其他应用读取,但是不可以进行写入。
  • MODE_WORLD_WRITEABLE:当前文件可以被其他应用写入,ps: 如果需要被其他应用写入和读取可以直接写MODE_WORLD_WRITEABLE + -MODE_WORLD_READABLE。
  • MODE_APPEND:该模式会检查文件是否存在,存在就往文件追加内容,否则就创建新的文件。

四、SharePreference 线程安全吗?

是不是线程安全主要看 SharePreference 的保存信息的方法,也就是commit方法和apply方法。

首先看一下 android.app.SharedPreferencesImpl.EditorImpl#commit方法的源码:
@Override
       public boolean commit() {
           long startTime = 0;

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

           // synchronized 提交到内存
           MemoryCommitResult mcr = commitToMemory();

           // synchronized 磁盘写入
           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;
       }

  • 对于提交到内存的方法commitToMemory 和磁盘写入的方法 enqueueDiskWrite ,都广泛使用了synchronized关键字来保证其线程安全。
  • 而且,commit 函数是在当前线程直接写入文件。
  • 最后还使用了阻塞操作,来等待其余的线程操作完毕。
  • 所以commit操作在多线程下是线程安全的。且注意到使用了try-catch
然后看一下 android.app.SharedPreferencesImpl.EditorImpl#apply 方法的源码:
   @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一样,也是首先调了 commitToMemory 这一步。
  • 但是下一步就有区别了,apply 函数通过子线程完成的写文件的操作。
综上所述
  • commit()是线程安全的,但是性能慢,同步操作,在当前线程完成写文件操作。
  • apply()是线程不安全的,但是性能高,是异步处理IO操作。

五、SharePreference与多进程

其实在sp创建的时候可以指定的加载模式中有个MODE_MULTI_PROCESS,它是Google提供的一个在多线程模式。但是这种模式并不是我们说的支持多进程同步更新等,它的作用只会在getSharedPreferences的时候,才会重新从xml重加载,如果我们在一个进程中更新xml,但是没有通知另一个进程,那么另一个进程的SharePreferences是不会自动更新的。

  /**
     * SharedPreference loading flag: when set, the file on disk will
     * be checked for modification even if the shared preferences
     * instance is already loaded in this process.  This behavior is
     * sometimes desired in cases where the application has multiple
     * processes, all writing to the same SharedPreferences file.
     * Generally there are better forms of communication between
     * processes, though.
     *
     * <p>This was the legacy (but undocumented) behavior in and
     * before Gingerbread (Android 2.3) and this flag is implied when
     * targeting such releases.  For applications targeting SDK
     * versions <em>greater than</em> Android 2.3, this flag must be
     * explicitly set if desired.
     *
     * @see #getSharedPreferences
     *
     * @deprecated MODE_MULTI_PROCESS does not work reliably in
     * some versions of Android, and furthermore does not provide any
     * mechanism for reconciling concurrent modifications across
     * processes.  Applications should not attempt to use it.  Instead,
     * they should use an explicit cross-process data management
     * approach such as {@link android.content.ContentProvider ContentProvider}.
     */
    @Deprecated
    public static final int MODE_MULTI_PROCESS = 0x0004;

官方也建议使用 ContentProvider 来替代 SharePreference 在多线程中的作用。

六、SharePreference原理解析

我们从 android.app.ContextImpl#getSharedPreferences(java.lang.String, int)函数开始看:

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

解读起来也不复杂,先去内存中查询与xml对应的SharePreferences是否已经被创建加载,如果没有那么该创建就创建,该加载就加载。在加载之后,要将所有的key-value保存起来,当然,如果首次访问,可能连xml文件都不存在,那么还需要创建xml文件,与SharePreferences对应的xml文件位置一般都在/data/data/包名/shared_prefs目录下,后缀一定是.xml,数据存储样式如下:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="login_department"></string>
    <int name="user_im" value="1" />
    <string name="mobile">185xxxxxxxx</string>
    <boolean name="HAS_SHOW_BIOMETRIC_DIALOG" value="true" />
    <string name="login_userid"></string>
    <string name="login_ap_username"></string>
    <boolean name="isbackground" value="true" />
    <string name="cbb_app_version">2.19.0</string>
    <string name="appRun">run</string>
    <string name="update_session_key_time">2022-07-15</string>
    <string name="device_info_statistics_time">2022-07-15</string>
    <boolean name="setting_reversal" value="true" />
    <string name="login_face_url"></string>
    <string name="login_role"></string>
    <string name="session_key"></string>
    <string name="app_first_start_time"></string>
    <string name="department_two">行政</string>
    <boolean name="setting_bright" value="true" />
    <string name="login_realname"></string>
    <boolean name="recovery_audio_is_finish" value="true" />
    <string name="updata_im_contant_time">2022-07-15</string>
</map>

那么,创建好对象的 xml文件后,结合之前说的 commit方法和apply方法,流程就连贯起来了。

七、总结

  1. SharePreferences 是Android基于xml实现的一种数据持久化手段。

  2. SharePreferences 不适合存储过大的数据, 因为所有持久化数据都是一次性加载到内存, 数据过大容易造成内存溢出。

  3. SharePreferencescommitapply一个是同步一个是异步(大部分场景下),SharePreferencescommit 方法是直接在当前线程执行文件写入操作, 而 apply 方法是在工作线程执行文件写入, 尽可能使用apply , 因为不会阻塞当前线程。

  4. SharePreferences 并不支持跨进程, 因为它不能保证更新本地数据后被另一个进程所知道,而且跨进程的操作标记已经被弃用。

  5. SharePreferences 批量更改数据时,只需要保留最后一个apply即可,避免添加多余的写文件任务。

  6. 每个SharePreferences 存储的键值对不宜过多, 否则在加载文件数据到内存时会耗时过长, 而阻塞SharePreferences 的相关get或put方法, 造成ui卡顿。

  7. 频繁更改的配置项和不常更改的配置项应该分开为不同的SharePreferences 存放,避免不必要的io操作。

  8. commit有相应的返回值,可以知道操作是否成功,apply没有返回值。

最后附上参考的文章:

关于SharePreference使用以及内部原理简单解析
SharePreference原理
SharedPreferences线程安全吗?commit和apply的区别?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容