美文网首页
SharedPreference 源码分析

SharedPreference 源码分析

作者: 未见哥哥 | 来源:发表于2019-01-31 01:23 被阅读37次

    一、 本节目标

    SharedPreference 是一个轻量级的 key-value 存储框架。开发者很容易地可以使用它的 api ,但是如果不恰当的使用可能会导致一些问题,所以针对如何使用和处理这些问题,列出了以下几个小点。

    • 1、sp 实例的获取。

    • 2、sp 是如何进行读写操作和缓存处理的?

    • 3、commit 和 apply 的区别?

    • 4、不恰当使用 sp 的一些坑。

    • 5、sp 中几种模式的选择。

    我们这篇博客主要就是来学习一下上面所提到的几个点,并从源码的角度来分析各个问题。

    二、sp 实例的获取

    2.1、 初始化

    SharedPreference 是一个接口,它的实现类是 SharedPreferenceImpl。有三种方式可以来获取:

    • 方式一 Activity#getPreferences
    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        return mBase.getSharedPreferences(name, mode);
    }
    
    • 方式二 PreferenceManager#getDefaultSharedPreferences
    public static SharedPreferences getDefaultSharedPreferences(Context context) {
        return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
                getDefaultSharedPreferencesMode());
    }
    
    • 方式三 ContextImpl#getSharedPreferences
    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
      ...
    }
    

    通过源码可以知道,最终都是用调用第三种方式来实例化 SharedPreferenceImpl 这个实例的。

    2.2、 getSharedPreferences源码分析

    这里使用了一个集合来缓存实例化后的 SharedPreferenceImpl 实例,这样下次调用该方法时,就直接从内存中获取这个对象。

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        ...
        SharedPreferencesImpl sp;
        //加锁,线程安全。
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
    
            //1. 从缓存存取 SharedPreferencesImpl 实例
            sp = cache.get(file);
            if (sp == null) {
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
        return sp;
    }
    

    2.3、 SharedPreferencesImpl 的构造

    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        startLoadFromDisk();
    }
    

    从构造方法中可以看出

    • mFile 就是当前 SharedPreferencesImpl 所映射的文件,所有的 key-value 的数据都会保存到这个文件中。

    • mBackupFile 可以理解为是一个备份文件,当保存数据失败等操作,可以从这个文件进行恢复数据。

    • mMode 表示文件的模式。

    • mMap 用于存储 key-value 键值对信息。

    • mLoaded 理解这个变量之前,首先要知道,sp 使用来存储 key-value 的,这些键值对数据在内存中是保存到一个 mMap 集合中,对应写入的磁盘文件就是 mFile 。这个变量就是标记 mFile 文件的数据是否已经加载到 mMap 集合中。具体的使用,下面会描述。

    • startLoadFromDisk() 内部开启线程去从文件中加载数据到 mMap 集合中。具体的使用,下面会描述。

    2.4、 startLoadFromDisk 加载数据

    在 2.3 中可以知道, mMap 是用于存储键值对的集合,而 mFile 是最终保存的本地文件。startLoadFromDisk 就是从 mFile 本地文件中去加载数据到 mMap 集合中,下面我们来看看源码是如何实现的。

    private void startLoadFromDisk() {
    
        // mLoaded 标记为 false
        synchronized (this) {
            mLoaded = false;
        }
    
        //开启线程,读取磁盘数据
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
    

    loadFromDisk 在子线程读取磁盘数据,源码如下:

    private void loadFromDisk() {
        synchronized (SharedPreferencesImpl.this) {
    
            //mLoaded 避免重复读取
            if (mLoaded) {
                return;
            }
    
    
            //如果备份文件存在,使用备份文件,回滚数据。
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }
    
        Map map = null;
        StructStat stat = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    //读取数据,保存到 map 对象中
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16*1024);
                    map = XmlUtils.readMapXml(str);
                } catch (XmlPullParserException | IOException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }
        synchronized (SharedPreferencesImpl.this) {
            //标记读取成功
            mLoaded = true;
            if (map != null) {
                //给成员变量赋值
                mMap = map;
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            } else {
                //首次可能文件为空,所以在这里 new 一个对象。
                mMap = new HashMap<>();
            }
            //读取完毕,激活其他线程。
            notifyAll();
        }
    }
    

    以上两个方法概括起来主要做了以下几件事:

    • 1、 mLoaded标记的设置,开始读取数据时,将其设置为 false,表示还未加载完成,读取成功之后标记为 true,表示加载完成。

    • 2、 mBackupFile首先判断备份文件是否存在,存在则优先从备份文件中恢复数据。

    • 3、 按照指定的格式读取文件数据,保存到 mMap 集合中。

    • 4、 激活其它阻塞的线程。具体如何使用,下面会介绍。

    三、 sp 对集合存取的操作

    3.1、 从集合中取出数据

    public int getInt(String key, int defValue) {
        synchronized (this) {
            //判断 mLoaded 数据是否为 true,不是则 wait 等待数据加载线程执行完毕。
            awaitLoadedLocked();
            //从 mMap 集合中获取数据。
            Integer v = (Integer)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
    

    get 操作都是加了 synchronized 的,因此它是线程安全的。在获取数据之前首先 awaitLoadedLocked() 方法,判断数据是否加载成功。 我们知道如果调用 getSharedPreference() 方法之后很快又去调用 getXxx() 方法,那么可能此时线程还在加载加载文件中的数据到内存中,这时的 mLoadedfalse,那么 awaitLoadedLocked(); 这个方法就会使当前线程阻塞直到数据加载成功,这时会 notifyAll()这样就可以正常的读取数据了。

    3.2、 存入数据到集合中

    存取一个整型数据:

    context.getSharedPreference
              .edit()
              .putInt("key",value)
              .commit();
    

    这里会涉及到一个 Editor的对象,它也是一个接口,它的实现类是 EditorImpl
    而 putInt,putBoolean,putLong 等操作都是通过 Editor 实现的。我们可以通过 getSharedPreference.edit()就可以获取 Editor 的实例对象。

    而对 EdiatorImpl 的一系列的 putXxx() 和 getXxx() 就是在操作 mModified 集合,而最后通过调用 commit()或者apply()就会将数据写入 mMap 和对应的 mFile 中。

    public final class EditorImpl implements Editor {
        private final Map<String, Object> mModified = Maps.newHashMap();
     
        ...
        public Editor putInt(String key, int value) { ... }
        public boolean commit() { ... }
        public boolean apply() { ... }
        ...
    }
    

    四、 保存数据 commit与 apply 的区别

    4.1、 异步式 apply 保存数据

    public void apply() {
        //1.保存数据到mMap内存中,并返回一个MemoryCommitResult 对象
        final MemoryCommitResult mcr = commitToMemory();
        final Runnable awaitCommit = new Runnable() {
                public void run() {
                    try {
                        //3.阻塞调用者
                        mcr.writtenToDiskLatch.await();
                    } catch (InterruptedException ignored) {
                    }
                }
            };
        //2.往 QueuedWork 添加一个任务。
        QueuedWork.add(awaitCommit);
        Runnable postWriteRunnable = new Runnable() {
                public void run() {
                    //从 QueuedWork 中移除
                    awaitCommit.run();
                    QueuedWork.remove(awaitCommit);
                }
            };
        //4.执行写入的任务。
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
        
        notifyListeners(mcr);
    }
    

    通过上面的apply()源码可以看出大致流程是

    • 1、 将数据写入到内存 mMap 中,并返回一个 MemoryCommitResult 对象,它封装要写入的数据到文件中的。

    • 2、 QueuedWork.add(awaitCommit);往 QueuedWork 添加一个任务。

    • 3、 当 awaitCommit被调用时,那么当前线程会被阻塞,直到 postWriteRunnable这个任务执行,才将 awaitCommit从QueuedWork中移除。

    • 4、 执行写入的任务。

    4.2、 commitToMemory()

    简单描述就是使用 MemoryCommitResult 来封装需要写入到文件的数据 mMap 和使用 changesMade 标记当前这次 apply 或者 commit 操作是否有新的数据要提交。

    // Returns true if any changes were made
    private MemoryCommitResult commitToMemory() {
        MemoryCommitResult mcr = new MemoryCommitResult();
        synchronized (SharedPreferencesImpl.this) {
            // 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);
            }
            //存储要写入到文件的数据
            mcr.mapToWriteToDisk = mMap;
            mDiskWritesInFlight++;
            boolean hasListeners = mListeners.size() > 0;
            if (hasListeners) {
                mcr.keysModified = new ArrayList<String>();
                mcr.listeners =
                        new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
            }
            synchronized (this) {
                if (mClear) {
                    if (!mMap.isEmpty()) {
                        mcr.changesMade = true;
                        mMap.clear();
                    }
                    mClear = false;
                }
                //遍历 mModified 将数据保存到 mMap 集合中。
                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 (!mMap.containsKey(k)) {
                            continue;
                        }
                        mMap.remove(k);
                    } else {
                        if (mMap.containsKey(k)) {
                            Object existingValue = mMap.get(k);
                            if (existingValue != null && existingValue.equals(v)) {
                                continue;
                            }
                        }
                        mMap.put(k, v);
                    }
                    //标记是有读写操作。
                    mcr.changesMade = true;
                    if (hasListeners) {
                        mcr.keysModified.add(k);
                    }
                }
                mModified.clear();
            }
        }
        return mcr;
    }
    

    4.3、 enqueueDiskWrite(...)

    这个方法主要是创建一个任务 writeToDiskRunnable并且交给线程池去执行。在这个任务中是负责将 mMap 中的数据写入到文件中,并且在写入完成后,调用 postWriteRunnable.run();来执行写入任务完毕后的后续操作,例如从 QueueWork移除任务等。

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
    
                        //写入文件的任务
                        writeToFile(mcr);
                    }
                    synchronized (SharedPreferencesImpl.this) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        //写入任务执行完毕后的后续操作,例如从 QueueWork移除任务等。
                        postWriteRunnable.run();
                    }
                }
            };
          ...
        //在线程池中执行写入任务。
        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
    }
    

    4.4、writeToFile(mcr)写入数据到文件中

    在这个方法中真正去执行写入操作,我大致关心的是整体流程,因此具体的写操作就不必要深究了。我们这里处理写的操作外,还有一个方法需要注意的是,那就是mcr.setDiskWriteResult(...),这个方法内部会调用 writtenToDiskLatch.countDown();递减等待线程的数量。

    private void writeToFile(MemoryCommitResult mcr) {
        // Rename the current file so it may be used as a backup during the next read
        if (mFile.exists()) {
            //当前没有要写入的操作
            if (!mcr.changesMade) {
                //标记写入完成
                mcr.setDiskWriteResult(true);
                return;
            }
            ...   
        }
        // Attempt to write the file, delete the backup and return true as atomically as
        // possible.  If any exception occurs, delete the new file; next time we will restore
        // from the backup.
        try {
            FileOutputStream str = createFileOutputStream(mFile);
            if (str == null) {
                mcr.setDiskWriteResult(false);
                return;
            }
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
            FileUtils.sync(str);
            str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
            try {
                final StructStat stat = Os.stat(mFile.getPath());
                synchronized (this) {
                    mStatTimestamp = stat.st_mtime;
                    mStatSize = stat.st_size;
                }
            } catch (ErrnoException e) {
                // Do nothing
            }
            // Writing was successful, delete the backup file if there is one.
            mBackupFile.delete();
            //标记写入成功
            mcr.setDiskWriteResult(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);
    }
    
    
    public void setDiskWriteResult(boolean result) {
        writeToDiskResult = result;
        //递减等待线程
        writtenToDiskLatch.countDown();
    }
    

    4.4、setDiskWriteResult的内部实现

    我们知道在 4.1 中有如下代码,当时并没有解释 mcr.writtenToDiskLatch.await();这段代码的作用。我们知道 QueueWork 只要执行 awaitCommit 这个任务,那么当前的线程会被阻塞,直到写入任务完成。

    //1.保存数据到mMap内存中,并返回一个MemoryCommitResult 对象
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            public void run() {
                    try {
                        //3.阻塞调用者
                        mcr.writtenToDiskLatch.await();
                    } catch (InterruptedException ignored) {
                }
        }
     };
     Runnable postWriteRunnable = new Runnable() {
                public void run() {
                    //从 QueuedWork 中移除
                    awaitCommit.run();
                    QueuedWork.remove(awaitCommit);
                }
            };
    

    setDiskWriteResult这个方法是在writeToFile中调用的,它内部会调用 writtenToDiskLatch.countDown();递减等待线程的数量,此时的等待线程就是写入数据的线程。当等待线程的数量递减到数量为 0 时也就是writeToFile方法执行完毕,那么这 postWriteRunnable 任务被调用时,这时就可以将 awaitCommit从 QueueWork 内部的队列中移除。

    public void setDiskWriteResult(boolean result) {
        writeToDiskResult = result;
        //递减等待线程
        writtenToDiskLatch.countDown();
    }
    

    4.5、阻塞式commit() 提交数据

    看完 apply 那么现在来看 commit 应该就很容明白了, commit 是阻塞式的,执行完毕之后会返回一个 boolean 变量值表示数据是否写入成功。

    public boolean commit() {
        //保存到内存中
        MemoryCommitResult mcr = commitToMemory();
        //写入数据
        SharedPreferencesImpl.this.enqueueDiskWrite(
            mcr, null /* sync write on this thread okay */);
        try {
            //阻塞等待写入线程完毕
            mcr.writtenToDiskLatch.await();
        } catch (InterruptedException e) {
            return false;
        }
        notifyListeners(mcr);
        return mcr.writeToDiskResult;
    }
    

    五、 不恰当使用 sp 的一些坑

    5.1、 示例1

    在这里先看一个 ANR 的日志信息。

    "main" prio=5 tid=1 WAIT
      | group="main" sCount=1 dsCount=0 obj=0x4155cc90 self=0x41496408
      | sysTid=13523 nice=0 sched=0/0 cgrp=apps handle=1074110804
      | state=S schedstat=( 2098661082 1582204811 6433 ) utm=165 stm=44 core=0
      at java.lang.Object.wait(Native Method)
      - waiting on <0x4155cd60> (a java.lang.VMThread) held by tid=1 (main)
      at java.lang.Thread.parkFor(Thread.java:1205)
      at sun.misc.Unsafe.park(Unsafe.java:325)
      at java.util.concurrent.locks.LockSupport.park(LockSupport.java:157)
      at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:813)
      at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:973)
      at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1281)
      at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:202)
      at android.app.SharedPreferencesImpl$EditorImpl$1.run(SharedPreferencesImpl.java:364)
      at android.app.QueuedWork.waitToFinish(QueuedWork.java:88)
      at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:2689)
      at android.app.ActivityThread.access$2000(ActivityThread.java:135)
      at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1494)
      at android.os.Handler.dispatchMessage(Handler.java:102)
      at android.os.Looper.loop(Looper.java:137)
      at android.app.ActivityThread.main(ActivityThread.java:4998)
      at java.lang.reflect.Method.invokeNative(Native Method)
      at java.lang.reflect.Method.invoke(Method.java:515)
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:777)
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:593)
      at dalvik.system.NativeStart.main(Native Method)
    

    在上面的日志中,我们很明显可以看到一个关键字await,我们猜应该是线程锁的问题。代码定位到 QueuedWork.waitToFinish中。

    从 waitToFinish 方法中可以知道它会在Activity暂停时,BroadcastReceiver的onReceive方法调用后或者service的命令处理后被调用,并且调用这个方法的目的是为了确保异步任务被及时完成。

    /**
     * Finishes or waits for async operations to complete.
     * (e.g. SharedPreferences$Editor#startCommit writes)
     *
     * Is called from the Activity base class's onPause(), after
     * BroadcastReceiver's onReceive, after Service command handling,
     * etc.  (so async work is never lost)
     */
    public static void waitToFinish() {
        Runnable toFinish;
        while ((toFinish = sPendingWorkFinishers.poll()) != null) {
            toFinish.run();
        }
    }
    

    waitToFinish()这个方法内部是去遍历 QueueWork 的队列 sPendingWorkFinishers并且执行对应的任务,而回到4.1 节中,我们创建了一个 awaitCommit 并且添加到 QueueWork队列中,如果此时 waitToFinish() 被执行时,而这时 mcr. writtenToDiskLatch中的等待线程数量没有递减到 0,也就是此时 commit 和 apply 的写入文件操作还在进行,那么waitToFinish调用线程就会被阻塞,从而导致 ANR的问题。

        //1.保存数据到mMap内存中,并返回一个MemoryCommitResult 对象
        final MemoryCommitResult mcr = commitToMemory();
        final Runnable awaitCommit = new Runnable() {
                public void run() {
                    try {
                        //3.阻塞调用者
                        mcr.writtenToDiskLatch.await();
                    } catch (InterruptedException ignored) {
                    }
                }
            };
        //2.往 QueuedWork 添加一个任务。
        QueuedWork.add(awaitCommit);
    

    5.2、 示例2

    ANR 的第二个场景是,在调用 getSharedPreference() 之后,马山又调用 getXxx()方法,就有可能出现 ANR 的情况。
    因为加载文件数据到内存中是在一个子线程中去执行的,因此在为了保证数据的同步性,在调用 getXxx()等方法时,会先调用 awaitLoadedLocked()判断数据是否已经加载到内存中,如果 mLoaded = false 就表示没有加载完成,这时是出于一个 wait 状态,这时如果本地的 mFile 文件比较大的话,那么如果在主线程调用 getXxx(),那么就有可能出现 ANR 现象,因为它会等待直到数据加载完成,这时 mLoaded = true,才会去释放锁。

    public int getInt(String key, int defValue) {
        synchronized (this) {
            awaitLoadedLocked();
            Integer v = (Integer)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
    

    5.3、 示例3

    如果通过 apply 来保存数据,但是马上就调用 getXxx() 方法的话,这时保存数据和获取数据是异步的,因此 getXxx() 得到的数据可能为空或者是旧的数据。

    5.4、 示例4

    在4.4节中,调用writeToFile(mcr)将 mMap 数据写入到文件中,这个操作是全量数据mMap写入到文件中,而不是增量写入,因此不管是调用 commit还是 apply时,最好全部通过 Editor进行修改数据之后,再进行写入操作,而不是一次修改就一次写入。

    XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
    

    六、 sp 中几种文件创建模式

    • MODE_PRIVATE

    默认的模式,当前创建的文件只能被当前 Application 使用。

    • MODE_WORLD_READABLE

    读模式,允许其他应用程序读取该文件,在 Android N 之后会有一个 SecurityException 异常。@Deprecated

    • MODE_WORLD_WRITEABLE

    写模式,允许其他应用程序写入该文件,在 Android N 之后会有一个 SecurityException 异常。@Deprecated

    • MODE_MULTI_PROCESS

    多进程模式,这种模式是不安全的,官方不建议使用,可以使用 ContentProvider 来代替。当设置MODE_MULTI_PROCESS模式, 则每次getSharedPreferences过程, 会检查SP文件上次修改时间和文件大小, 一旦所有修改则会重新从磁盘加载文件。@Deprecated

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        //1. 检查 mode ,不符合会抛出异常。
        checkMode(mode);
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
        //2,多进程模式,重新从本地加载数据
        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;
    }
    
    private void checkMode(int mode) {
        if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
            if ((mode & MODE_WORLD_READABLE) != 0) {
                throw new SecurityException("MODE_WORLD_READABLE no longer supported");
            }
            if ((mode & MODE_WORLD_WRITEABLE) != 0) {
                throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
            }
        }
    }
    
    • 1、 checkMode 检查传入的 mode 是否合适。

    从以下 checkMode 方法源码可以知道, Google 在 Android N 之后,就不再支持 MODE_WORLD_READABLEMODE_WORLD_WRITEABLE模式了。这样对用户的隐私权限进一步的收紧,如果有这方面的需求还是建议使用 FileProvider 内容提供者来实现。

    • 2、 如果是 MODE_MULTI_PROCESS 模式,那么建议不要在保存 SharedPreference 的实例,每次调用时都应该调用 getSharedPreference() 来确保数据是从本地加载到的。

    记录于 2019年1月31日

    相关文章

      网友评论

          本文标题:SharedPreference 源码分析

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