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的区别?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容