美文网首页
SharedPreferences 相关总结

SharedPreferences 相关总结

作者: chendroid | 来源:发表于2020-07-21 23:10 被阅读0次

    SP 存储相关内容。

    有关 SharedPreferences 的简单使用:

    以下代码的源码基于 android-29 , 为什么要这么强调一下呢?因为几乎每个版本都会有一些大大小小的修改, 官方也在陆续做优化。

    以下包含以下内容:

    1. SharedPreferences 对象的获取
      • 获取代码
      • context.getSharedPreferences(fileName, mode) 代码分析
      • 新建 SP对象 和 loadFromDisk 的代码分析
      • 小结
    2. 访问 SP 的数据:读数据和写数据操作
      • 读操作
      • 写操作, commit()apply() 的区别
    3. 一些有关 SP 源码的有趣操作
      • mLock 变量的加锁
      • mBackupFile 备份文件的存在
      • apply()commit() 的误解
      • 对多进程的处理,不支持多进程
      • QueuedWork 的影响, 可能会造成阻塞

    1. SharedPreferences 的获取

    1.1 获取代码

    当我们获取一个 Sp 时, 获取代码如下:

    // context getSharedPreferences()
    val preferences = context.getSharedPreferences(prefName,Context.MODE_PRIVATE)
    
    

    通过 ContextImpl 获取到对应文件的 SharedPreferences 对象。
    而我们都知道,如果当前 prefName 的文件不存在,会新建一个文件。

    1.2 context.getSharedPreferences(fileName, mode) 代码分析

    getSharedPreferences(name, mode) 代码如下:

    // 部分代码如下:ContextImpl.getSharedPreferences(xxx) 如下:
    // 1. synchronized 加锁 ,对整个 `ContextImpl` 这个类加锁
    synchronized (ContextImpl.class) {
        // 2. mSharedPrefsPaths 为 arrayMap, 如果为空,则会新建一个,里面存贮这 `key-> 文件名`, 
        // `value -> File`
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();
        }
        file = mSharedPrefsPaths.get(name);
        // 3. 如果没有该文件,则去新建文件
        if (file == null) {
            // 4. getSharedPreferencesPath(name) 中会新建该文件
            file = getSharedPreferencesPath(name);
            // 5. 把该文件加入到 ArrayMap 中,下次直接读取;
            mSharedPrefsPaths.put(name, file);
        }
    }
    
    // 获取真正的 sp 对象
    return getSharedPreferences(file, mode);
    

    上述代码中,重要的位置添加了注释,在这里再说一下:

    1. synchronized 加锁 ,对整个 ContextImpl 这个类加锁
      读这部分源码时,你会发现有很多类似的实现,有时是对 ContextImpl 一个类加锁,有时会对一个对象加锁,其中都是有不同用意的,慢慢体会。
    2. mSharedPrefsPaths 是一个 key-> 文件名 name, value -> File 的一个 ArrayMap,
      所有当前 APP 下的 SP 文件,只要被加载过,都会被存在这个 Map
    3. 如果没有该 SP 文件,则去新建文件 getSharedPreferencesPath(),
      getSharedPreferencesPath 这个方法里面,有指定 SP 文件存放位置的代码。
    4. 把该文件加入到 ArrayMap 中,下次可以直接得到该 File

    注:上述中的文件,其实是 File, 此时读取到的 File 尚未经过 Xml 解析,更未得到 SharedPreferencesImpl 对象。

    1.3 ContextImpl.getSharedPreferences(file, mode) 的代码分析

    简化为下面代码:

    public SharedPreferences getSharedPreferences(File file, int mode) {
        ...
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            ...
            sp = new SharedPreferencesImpl(file, mode);
            ache.put(file, sp);
        }
        ...
        return sp;
    }
    

    getSharedPreferences(file, mode) 方法中,会去检测 sSharedPrefsCache所对应的当前包名的 ArrayMap<File, SharedPreferencesImpl> 中是否有缓存的该文件 Filesp 对象,如果没有该对应的 SP 对象,则会新建一个 new SharedPreferencesImpl(file, mode)

    new SharedPreferencesImpl() 的代码
    主要作用:

    1. 新建 SharedPreferencesImpl 对象;
    2. 创建备份文件;
    3. 异步读取 File 文件里面的内容到 mMap

    new SharedPreferencesImpl(file, mode) 中,会新建一个 BackUp 备份的 File:

    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk();
    }
    
    // 为 prefsFile 新建一个备份的 文件,以 `.bak` 结尾
    static File makeBackupFile(File prefsFile) {
        return new File(prefsFile.getPath() + ".bak");
    }
    

    我们都知道,SP 文件是存储为 xml 格式的文件,那么是如何解析成,我们熟知的 map 形式呢?

    代码在 startLoadFromDisk() 方法里面, 它会开一个线程 threadloadFromDisk
    loadFromDisk 代码如下:

    // 代码如下
    private void loadFromDisk() {
        synchronized (mLock) {
            // 1. 是否解析过该文件, 如果解析过,则不再重复解析,处理多线程访问的问题。
            if (mLoaded) {
                return;
            }
            // 2. 如果备份文件存在,则删除 mFile, 把 mBackupFile 重命名为 mFile
            // 即,最终使用的是备份文件里面的数据
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }
        ...
        Map<String, Object> map = null;
        try {
            str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
            map = (Map<String, Object>) XmlUtils.readMapXml(str);
        } catch() {...}
        ...
        synchronized (mLock) {
            // 3. 设置标志符为 true, 已经读取了该文件到内存 map 中;
            mLoaded = true;
            ...
            // 4. 复制给成员变量 mMap
            mMap = map;
            //在这里其实有个 try catch 给 mMap 赋值, 在 finally 中这么实现
            ...
            finally {
                // 5. 用于唤醒所有等待 mLock 的线程,也就是说在这里从 `xml` 文件中读取数据到内存 mMap 中,
                //是可能会存在线程等待的
                mLock.notifyAll();
            }
        }
        
    }
    

    上面代码中,标注了一些值得注意的地方, 在罗列一下:

    1. 通过 mLoaded判断 是否解析过该文件, 如果解析过,则不再重复解析,处理多线程访问的问题;
    2. 如果备份文件存在,则删除 mFile, 把 mBackupFile 重命名为 mFile , 即,最终使用的是备份文件里面的数据
    3. 设置标志符 mLoadedtrue, 表示已经读取了该文件到内存 map 中;
    4. 复制给成员变量 mMap , 后续所有从该 SP 中访问的数据,都来自于它;
    5. 用于唤醒所有等待 mLock 的线程,也就是说在这里从 xml 文件中读取数据到内存 mMap, 是可能会存在线程等待的。

    得到 sp 返回值后,我们再次回到 getSharedPreferences(File file, int mode)

    //ContextImpl.java android-29
    public SharedPreferences getSharedPreferences(File file, int mode) {
        ...
        
        // synchronized (ContextImpl.class) 加锁, 保证重复从内存中读取文件
        // 1. 得到返回值 sp
        sp = new SharedPreferencesImpl(file, mode);
        // 2. 把返回值 sp 加入到 cache  map 中,下次再次访问该 SP 文件, 不需要再次解析该文件,
        //节省时间和开销
        cache.put(file, sp);
        return sp;
    }
    

    至此,我们已经拿到了 SP 的对象, 经过 getSharedPreferences(File file, int mode) 返回,
    val preferences = SP 这里赋值成功.

    后续,便可以通过它访问存储的数据。

    1.4 小结

    在上述过程中,我们主要分析了如何得到 SP 对象,在这里有以下需要值得注意的点:

    1. ContextImpl 中有两个内存变量 mSharedPrefsPaths sSharedPrefsCache

      sSharedPrefsCacheArrayMap<String, ArrayMap<File, SharedPreferencesImpl>>,
      它的 key 为当前 APP 的包名, value 为 Map 「它的 keysp 对应的 FilevalueSP 对应的对象
      mSharedPrefsPathsArrayMap<String, File>, 是根据 SP 的文件名 name , 获取到对应的 File

    2. 在解析文件时,会优先看是否存在备份文件,如果存在,则用备份文件去替换原有文件;

    3. 解析对应文件 xml FilemMap 中是异步的, 在子线程中完成,并且有加锁处理,防止多个线程进行解析

    2. 访问它的数据 :读操作 + 写操作

    2.1 读操作:getxxx()

    在第一部分,当我们拿到了 SP 对象 preferences 后,我们可以访问到它里面的数据,
    简单示例如下:

    // 通过这种形式可以访问到它内部对应的数据
    preferences.getString("key_name", "default_value")
    

    再看一下,getString(xxx) 内部的实现 :

    // SharedPreferencesImpl.getString() 方式实现代码:
    @Override
    @Nullable
    public String getString(String key, @Nullable String defValue) {
        // 1.使用 synchronized 加锁,防止多线程同时访问
        synchronized (mLock) {
            // 2. 判断是否加载 File 文件到内存 mMap 中, 
            awaitLoadedLocked();
            // 3. 所有取的数据都是在 `mMap` 中取的数据
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
    

    当读取一个数据时,会有以下逻辑, 在上述代码中有标注:

    1. 会通过 synchronized (mLock) 加锁,防止多线程的同时访问;

    2. 在内部会等待 「加载 File 文件到内存mMap」 的完成: 这里本质上是判断mLoaded这个变量的值是否为true, -true:表示文件已经读取好了到内存mMap中; -false: 表示文件尚未读入到内存mMap` 中

      awaitLoadedLocked() 的代码中,有 mLock.wait()
      这也就是为什么需要在 loadFromDisk() 里面最后的 finally 代码块中 mLock.notifyAll()
      唤醒等待的地方,继续执行他们的取数据操作。

    3. 所有取的数据都是在 mMap 中取的数据,
      一旦我们拿到 mMap 时,在同一个进程下的数据获取都是对 mMap 的读数据操作.

    2.2 写操作 putxxx()

    对于 put 操作,我们都知道,其实是需要拿到 preferences.edit() 对象

    preferences.edit() 才是我们操作写数据时需要使用到的。

    @Override
    public Editor edit() {
        ...
        // 
        synchronized (mLock) {
            // 等待加载完成, mMap 有数据
            awaitLoadedLocked();
        }
        return new EditorImpl();
    }
    

    实际的对象为为 EditImpl, 看以下代码:

    // 以 putString(xxx) 代码为例
    @Override
    public Editor putString(String key, @Nullable String value) {
        // 同样有加锁, Object mEditorLock , 防止多线程同时写数据
        synchronized (mEditorLock) {
            // mModified = new HashMap<String, Object>(), 是一个 HashMap 对象
            mModified.put(key, value);
            return this;
        }
    }
    

    从代码中,可以看到:

    1. 同样会有加锁处理, 保证多线程安全
    2. 所有数据的 put 都会暂时放入到 mModified 这个 Map 中;

    暂时存放在 mModified 的数据,会在 EditIpml.commitToMemory() 中写入到 mMap
    EditIpml.commitToMemory() 并不是一个耗时操作,真正把 mMap 写入到 File 中才是耗时操作

    一个完整 put 操作代码调用如下:

    // 首先得到一个 EditorImpl() 对象
    val edit = preferences.edit()
    // 添加数据
    edit.putString("name", "chendroid")
    
    // 提交数据:两种方式,commit()  或者 `apply()`
    //edit.commit()
    edit.apply() 
    

    代码说明如下:

    1. preferences.edit() 每次都会 new EditorImpl(), 因此不要频繁调用 edit() 方法;

    2. 两种提交方式:commit(): commit() 提交后,会在 UI 线程「当前线程」进行数据的写入

    3. apply() 的提交方式: apply() 提交后,会把数据写入的操作放在 IO 线程里面
      因为,如果没有额外的需求,我们一般是使用 apply() 做数据的写入操作

    2.2.1 edit.commit() 后续逻辑

    先分析下源码:

    public boolean commit() {
        ...
        // 1. 首先把 `mModified` 中新增的数据,写入到 `mMap` 内存中,
        MemoryCommitResult mcr = commitToMemory();
        // 2. 真正写入文件 File 中,写入到磁盘中
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
        try {
            // 3. 在这里等待,直到 writtenToDiskLatch 计数器为 0 ,主线程结束等待,开始执行后续逻辑
            // 子线程中,会调用 writtenToDiskLatch.countDown() 使线程计数器减 1
            mcr.writtenToDiskLatch.await();
        } catch ...
        
        } finally {...}
        ...
        notifyListeners(mcr);
        // 4. 返回执行结果,是否提交数据成功。
        return mcr.writeToDiskResult;
    }
    

    在上述代码中, 已经写了部分注解:

    1. 首先,commitToMemory() 实际操作是把 mModified 新增的值给对应的 mMap 新增
    2. 真正写入磁盘的操作是在 enqueueDiskWrite() 里面,这个是耗时操作。它的两个参数是有区别的
      enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable),
      • 第一个参数: mcrcommitToMemory() 的结果
      • 第二个参数: postWriteRunnable 是决定是否会异步提交的关键
    3. commit() 是会等待写入操作完成后才返回的,因为会存在 mcr.writtenToDiskLatch.await() 的操作
    4. commit() 是有返回结果的,返回是否写入到磁盘 disk .

    注:上述代码中,mcr.writtenToDiskLatchCountDownLatch 的实例。
    作用是等待其他的线程都执行完任务,必要时可以对各个任务的执行结果进行汇总,
    然后主线程才继续往下执行。

    下面我们在看一看 edit.apply() 操作。

    enqueueDiskWrite() 的具体逻辑,我们暂不分析

    2.2.2 edit.apply() 后续逻辑

    首先看一下它的源码:

    //
    public void apply() {
        ...
        //1. 和 commit() 相同的逻辑,写入到内存 mMap 中
        final MemoryCommitResult mcr = commitToMemory();
        // 2. 新建了一个 Runnable
        final Runnable awaitCommit = new Runnable() {
            ...
            // 3. 在 Runnable 里面这里的等待操作~
            mcr.writtenToDiskLatch.await();
            ...
        }
        // 4. 把 awaitCommit 添加进去
        QueuedWork.addFinisher(awaitCommit);    
        // 5. 这里新建的 postWriteRunnable 
        Runnable postWriteRunnable = new Runnable() {
            run() {
                // 6. 在第 2 步中新建的 runnable 在这里被调用运行 
                awaitCommit.run();
                // 
                QueuedWork.removeFinisher(awaitCommit);
            }
        }
        // 7. 和 commit()  一样的调用方法, 写入到磁盘中去
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    }
    

    它里面的代码逻辑如下:

    1. commit() 相同的逻辑,写入到内存 mMap

    2. 新建了一个 Runnable, 用于表示等待完成后的操作;

    3. 新建了 postWriteRunnable, 并在 postWriteRunnable 中,调用了 awaitCommit.run(), 表示等待写入磁盘已经完成;

    4. 调用 enqueueDiskWrite, 并传入了 postWriteRunnable


    我们可以发现,上述中 apply()commit() 都调用了 enqueueDiskWrite(), 用于把当前改动写入到磁盘文件中;
    但是有所不同,commit() 中,传入的第二个参数 Runnablenull;
    apply() 中,传入了不为 nullpostWriteRunnable;

    那得看一下 enqueueDiskWrite(mcr, postWriteRunnable) 这个方法的代码了:

    // 写入磁盘操作
    private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
        // 1. isFromSyncCommit 代表着是否同步写入磁盘中,
        //如果为 true , 代表这同步写入,「commit() 传入的 postWriteRunnable == null」 所以为同步写入
        //如果为 false, 代表这异步写入,apply() 传入的 postWriteRunnable != null」, 所以为异步写入
        final boolean isFromSyncCommit = (postWriteRunnable == null);
        //
        final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                    // 2. 真正写入磁盘文件的位置
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    mDiskWritesInFlight--;
                }
                //
                if (postWriteRunnable != null) {
                    // 3. 等写入文件完成后,这里手动调用传入的 `postWriteRunnable.run()`
                    postWriteRunnable.run();
                }
            }
        }
        // 4. 这里为 commit() 走的逻辑,为 true ,同步写入
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            // mDiskWritesInFlight 默认为 0, 分别有 `++` 和 `--` 操作;
            // `mDiskWritesInFlight++` 在 `commitToMemory()` 中;
            // `mDiskWritesInFlight--` 在 `enqueueDiskWrite().writeToDiskRunnable` 中;
            if (wasEmpty) {
                // 5. 这里直接调用 writeToDiskRunnable, 并且 return 数据
                writeToDiskRunnable.run();
                return;
            }
        }
        // 6. apply() 逻辑, 会加入到队列中,等待完成
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }
    

    从上面代码中,我们可以看到:

    1. commit() 传入的 postWriteRunnablenull, isFromSyncCommit == true 表示同步提交;
    2. apply() 传入的 postWriteRunnable != null , isFromSyncCommit == false 表示异步提交;
    3. writeToDiskRunnable 是真正执行写入文件的 runnable, 它的 run() 方法调用的时机决定着它执行在那个线程;
    4. commit() 走的逻辑: 在当前线程中调用 writeToDiskRunnable.run(); 并且返回,是同步提交代码的,可能会存在阻塞主线程的情况;
    5. apply() 走的逻辑: 把 writeToDiskRunnable 加入到 QueuedWork 队列中, 在 IO 线程中执行 writeToDiskRunnable
    6. QueuedWork 内部是自带了 HandlerThread, 在它内部 IO 线程中执行对应的任务
    7. QueuedWork.queue() 会把当前的 work 「Runnable」 加入到 sWork.add(work)「一个 LinkedList 队列」
    8. QueuedWork 在执行 sWork 里面的消息是顺序执行的;
    9. QueuedWork.queue(Runnable work, boolean shouldDelay) 第二个参数是指:是否延迟发送消息;
      在这里,apply() 传入的是 true, 代表会延迟发送消息「hanlder 发送xiaoxi 」,默认延迟 100ms

    2.3 总结

    从上面的读数据和写数据的分析中,我们了解了以下内容:

    1. 读数据时, 数据都是从 mMap 中取得的值;

    2. 写数据时,实际操作的对象为 EditorImpl(),会先把数据存储在 mModified 中,等到 apply() 提交时,才会更新 mMap 中的值, 并且每次提交都会更新磁盘中的文件;

    3. apply() 提交是异步的,在 IO线程完成的;

    4. commit() 提交是同步的, 在当前线程完成的, 可能会出现阻塞主线程的情况;

    5. apply() 的异步执行中,消息是按照顺序依次执行的

    6. 不管是 apply() 还是 commit(), 每次提交后,都会有内存写入磁盘的操作
      即使只改动了一个很小的值,也会把全部写入到磁盘中,多次修改提交,就会有多次的写入磁盘,这也是 SP 性能不足够优秀的原因;

    3 一些 SP 的有趣操作

    3.1 对 mLock 变量的加锁

    为了确保多线程的安全,整个 SharedPreferencesImpl 代码中,总共有 19 处使用了下面的形式:

    synchronized (mLock) {
        // do something
    }
    

    只要涉及到数据的访问,读取,加载,写入,均都对该变量的加锁处理;
    这也保证了数据的安全性以及可见性。

    3.2 mBackupFile 备份文件的存在

    mBackupFile 的存在,保证了在一些特殊的异常情况下,假设在写入磁盘数据时发生了异常,此时仍然存在 mBackupFile 可供使用。

    1. loadFromDisk() 时,会优先检查是否存在 mBackupFile 如果存在,则删除原文件 mFile, 把 mBackupFile 重命名为 mFile;

    2. writeToFile() 时,会先存在检查备份文件是否存在,如果不存在,则将 mFile 重命名为 mBackupFile ;

    3. writeToFile() 继续运行时,如果 File 写入成功,则会再次删除 mBackupFile;

    3.3 对 apply()commit() 的误解

    这里要注意下,因为 commit() 是同步提交到磁盘中的,而 apply() 是异步提交到磁盘中,是在 IO 线程中完成的;
    有些博文的理解是:apply() 会等待一些数据的修改,然后再把这些修改一次性写入磁盘中;
    这是错误的

    ** apply()commit() 每次提交,都会对文件进行一次修改写入磁盘操作**

    它们的不同是在:写入磁盘的线程有所不同

    3.4 对多进程的处理,不支持多进程

    context.getSharedPreferences(prefName,Context.MODE_PRIVATE) 中,
    第二个参数是:mode , 当其符合 Context.MODE_MULTI_PROCESS 时,其实并没有支持多进程的操作。

    在它的注释中:

    // Context.java 中
    /**
    * @deprecated MODE_MULTI_PROCESS does not work reliably 
    */
    @Deprecated
    public static final int MODE_MULTI_PROCESS = 0x0004;
    

    也可以看到官方说是不支持的。

    而当是这个 mode 时,SP 是如何处理的呢?

    // 代码在 Context.getSharedPreferences(File file, int mode) 中
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 || ... {
        // 
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp
    

    而在 sp.startReloadIfChangedUnexpectedly() 这个里面,做了什么操作呢?

    //
    void startReloadIfChangedUnexpectedly() {
        synchronized (mLock) {
            ...
            // 异步开启去磁盘中再读取一遍文件
            startLoadFromDisk();    
        }
    }
    

    而上述代码中,已经把 SP 的 对象 return 出去了,
    在这里并没有多进程做多余的加锁操作,因此 SP并不支持多进程传输数据。

    3.5 QueuedWork 的影响

    apply() 异步提交数据后,最终会被加入到 QueuedWork.enque(postrunnable) 进行处理的。

    那么…… 我们有没有其他地方涉及到这个类?
    QueuedWork 中,有这样一个方法 waitToFinish()

    //Trigger queued work to be processed immediately. 触发队列中的任务立马执行
    // Is called from the Activity base class's onPause(), 
    //它会在 `Activity.onPause()` 方法中被调用 
    // so async work is never lost: 以防异步工作丢失,数据未完成写入。
    public static void waitToFinish() {
        //
        ...
    }
    

    它被调用的位置在 ActivityThread.java 中,源码如下:

    // ActivityThread.handlePauseActivity() 方法中
    public void handlePauseActivity(...) {
        ...
        // Make sure any pending writes are now committed.
        if (r.isPreHoneycomb()) {
            // 调用位置。 使得所有异步的操作都完成
            QueuedWork.waitToFinish();
        }
        ...
    }
    

    而在 QueuedWork.waitToFinish() 的源码中,如果会完成的话,会一直执行,知道任务完成,才会继续走 onPause()) 的其余操作。

    所以, 当我们异步 apply() 提交很多次任务时,这时突然杀死 APP, 这时候如果 QueuedWork 中未完全执行,是会发生阻塞的;

    而一旦阻塞的时间过长,可能就会发生 ANR.

    这里我们要注意一下。

    4 整体总结

    有关 SP 的分析,在上面,尽可能的分析到了。

    可以根据上面的代码分析,给出一些建议:

    1. 尽可能分散的使用 SP 文件,建立多个 SP 文件,同时保证每个 SP 文件不会过大,因为每次都会写入磁盘的操作,即使你只修改了这份文件的小个小部分;
    2. 分成多个文件还有一个原因是:当你对其中的数据进行操作时,不能同时操作,可能会存在等待的情况,如果是多个文件,分别去不同的文件中取,会高效;
    3. 可以提前初始化 SP ,得到它的对象, 这样再使用的时候就可以不用等待可以直接存储数据;
      例如在 Application 中初始化
    4. 不要每次都 sp.edit(), 因为 edit() 方法总会新建一个 EditorImpl 对象;
    5. 无特殊需求,不要使用 commit() 提交修改,而是使用 apply()
    6. SP 不支持多进程通信, 不要依赖它做一些多进程的操作
      支持多进程的方式:

    参考链接

    1. Android 之不要滥用 SharedPreferences(上): https://www.jianshu.com/p/5fcef7f68341
      注:这里的作者的一系列文章都很精彩,可以阅读下。
    2. https://github.com/JeremyLiao/FastSharedPreferences, 源码的一些实现
    3. Android-29 源码

    这次就先这样吧~

    2020.7.21 by chendroid

    相关文章

      网友评论

          本文标题:SharedPreferences 相关总结

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