每日一问:谈谈 SharedPreferences 的 apply() 和 commit()

SharedPreferences 应该是任何一名 Android 初学者都知道的存储类了,它轻量,适合用于保存软件配置等参数。以键值对的 XML 文件形式存储在本地,程序卸载后也会一并清除,不会残留信息。

使用起来也非常简单。

// 读取
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
val string = sharedPreferences.getString("123","")
// 写入
val editor = sharedPreferences.edit()
editor.putString("123","123")
editor.commit()

当我们写下这样的代码的时候,IDE 极易出现一个警告,提示我们用 apply() 来替换 commit()。原因也很简单,因为 commit() 是同步的,而 apply() 采用异步的方式通常来说效率会更高一些。但是,当我们把 editor.commit() 的返回值赋给一个变量的时候,这时候就会发现 IDE 没有了警告。这是因为 IDE 认为我们想要使用 editor.commit() 的返回值了,所以,通常来说,在我们不关心操作结果的时候,我们更倾向于使用 apply() 进行写入的操作。

获取 SharedPreferences 实例

我们可以通过 3 种方式来获取 SharedPreferences 的实例。
首先当然是我们最常见的写法。

getSharedPreferences("123", Context.MODE_PRIVATE)

Context 的任意子类都可以直接通过 getSharedPreferences() 方法获取到 SharedPreferences 的实例,接受两个参数,分别对应 XML 文件的名字和操作模式。其中 MODE_WORLD_READABLEMODE_WORLD_WRITEABLE 这两种模式已在 Android 4.2 版本中被废弃。

  • Context.MODE_PRIVATE: 指定该 SharedPreferences 数据只能被本应用程序读、写;
  • Context.MODE_WORLD_READABLE: 指定该 SharedPreferences 数据能被其他应用程序读,但不能写;
  • Context.MODE_WORLD_WRITEABLE: 指定该 SharedPreferences 数据能被其他应用程序读;
  • Context.MODE_APPEND:该模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件;

另外在 Activity 的实现中,还可以直接通过 getPreferences() 获取,实际上也就把当前 Activity 的类名作为文件名参数。

public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
    return getSharedPreferences(getLocalClassName(), mode);
}   

此外,我们也可以通过 PreferenceManagergetDefaultSharedPreferences() 获取到。

public static SharedPreferences getDefaultSharedPreferences(Context context) {
    return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
            getDefaultSharedPreferencesMode());
}

public static String getDefaultSharedPreferencesName(Context context) {
    return context.getPackageName() + "_preferences";
}

private static int getDefaultSharedPreferencesMode() {
    return Context.MODE_PRIVATE;
}

可以很明显的看到,这个方式就是在直接把当前应用的包名作为前缀来进行命名的。

注意:如果在 Fragment 中使用 SharedPreferences 时,SharedPreferences 的初始化尽量放在 onAttach(Activity activity) 里面进行 ,否则可能会报空指针,即 getActivity() 会可能返回为空。

SharedPreferences 源码(基于 API 28)

有较多 SharedPreferences 使用经验的人,就会发现 SharedPreferences 其实具备挺多的坑,但这些坑主要都是因为不熟悉其中真正的原理所导致的,所以,笔者在这里,带大家一起揭开 SharedPreferences 的神秘面纱。

SharedPreferences 实例获取

前面讲了 SharedPreferences 有三种获取实例的方法,但归根结底都是调用的 ContextgetSharedPreferences() 方法。由于 Android 的 Context 类采用的是装饰者模式,而装饰者对象其实就是 ContextImpl,所以我们来看看源码是怎么实现的。

// 存放的是名称和文件夹的映射,实际上这个名称就是我们外面传进来的 name
private ArrayMap mSharedPrefsPaths;

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 File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}

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

可以很明显的看到,内部是采用 ArrayMap 来做的处理,而这个 mSharedPrefsPaths 主要是用于存放名称和文件夹的映射,实际上这个名称就是我们外面传进来的 name,这时候我们通过 name 拿到我们的 File,如果当前池子中没有的话,则直接新建一个 File,并放入到 mSharedPrefsPaths 中。最后还是调用的重载方法 getSharedPreferences(File,mode)

// 存放包名与ArrayMap键值对,初始化时会默认以包名作为键值对中的 Key,注意这是个 static 变量
private static ArrayMap> sSharedPrefsCache;

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap 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;
}   

private ArrayMap getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();
    ArrayMap packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
}

可以看到,又采用了一个 ArrayMap 来存放文件和 SharedPreferencesImpl 组成的键值对,然后通过通过单例的方式返回一个 SharedPreferences 对象,实际上是 SharedPreferences 的实现类 SharedPreferencesImpl,而且在其中还建立了一个内部缓存机制。

所以,从上面的分析中,我们能知道 对于一个相同的 name,我们获取到的都是同一个 SharedPreferencesImpl 对象。

SharedPreferencesImpl

在上面的操作中,我们可以看到在第一次调用 getSharedPreferences 的时候,我们会去构造一个 SharedPreferencesImpl 对象,我们来看看都做了什么。

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

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

private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    // Debugging
    if (mFile.exists() && !mFile.canRead()) {
        Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
    }

    Map 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);
                map = (Map) XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        // An errno exception means the stat failed. Treat as empty/non-existing by
        // ignoring.
    } catch (Throwable t) {
        thrown = t;
    }

    synchronized (mLock) {
        mLoaded = true;
        mThrowable = thrown;

        // It's important that we always signal waiters, even if we'll make
        // them fail with an exception. The try-finally is pretty wide, but
        // better safe than sorry.
        try {
            if (thrown == null) {
                if (map != null) {
                    mMap = map;
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                    mMap = new HashMap<>();
                }
            }
            // In case of a thrown exception, we retain the old map. That allows
            // any open editors to commit and store updates.
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
            mLock.notifyAll();
        }
    }
}

注意看我们的 startLoadFromDisk 方法,我们会去新开一个子线程,然后去通过 XmlUtils.readMapXml() 方法把指定的 SharedPreferences 文件的所有的键值对都读出来,然后存放到一个 map 中。

而众所周知,文件的读写操作都是耗时的,可想而知,在我们第一次去读取一个 SharedPreferences 文件的时候花上了太多的时间会怎样。

SharedPreferences 的读取操作

上面讲了初次获取一个文件的 SharedPreferences 实例的时候,会先去把所有键值对读取到缓存中,这明显是一个耗时操作,而我们正常的去读取数据的时候,都是类似这样的代码。

val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
val string = sharedPreferences.getString("123","")

SharedPreferencesgetXXX() 方法可能会报 ClassCastException 异常,所以我们在同一个 name 的时候,对不一样的类型,必须使用不同的 key。但是 putXXX 是可以用不同的类型值覆盖相同的 key 的。

那势必可能会导致这个操作需要等待一定的时间,我们姑且可以这么猜想,在 getXXX() 方法执行的时候应该是会等待前面的操作完成才能执行的。

因为 SharedPreferences 是一个接口,所以我们主要来看看它的实现类 SharedPreferencesImpl,这里以 getString() 为例。

@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

awaitLoadedLocked() 方法应该就是我们所想的等待执行操作了,我们看看里面做了什么。

private void awaitLoadedLocked() {
    if (!mLoaded) {
        // Raise an explicit StrictMode onReadFromDisk for this
        // thread, since the real read will be in a different
        // thread and otherwise ignored by StrictMode.
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) {
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}   

可以看到,在 awaitLoadedLocked 方法里面我们使用了 mLock.wait() 来等待初始化的读取操作,而我们前面看到的 loadFromDiskLocked() 方法的最后也可以看到它调用了 mLock.notifyAll() 方法来唤醒后面这个阻塞的 getXXX()那么这里就会明显出现一个问题,我们的 getXXX() 方法是写在 UI 线程的,如果这个方法被阻塞的太久,势必会出现 ANR 的情况。所以我们一定在平时需要根据具体情况考虑是否需要把 SharedPreferences 的读写操作放在子线程中。

SharedPreferences 的内部类 Editor

我们在写入数据之前,总是要先通过类似这样的代码获取 SharedPreferences 的内部类 Editor

val editor = sharedPreferences.edit()

我们当然要看看这个到底是什么东西。

    @Override
    public Editor edit() {
        // TODO: remove the need to call awaitLoadedLocked() when
        // requesting an editor.  will require some work on the
        // Editor, but then we should be able to do:
        //
        //      context.getSharedPreferences(..).edit().putString(..).apply()
        //
        // ... all without blocking.
        synchronized (mLock) {
            awaitLoadedLocked();
        }

        return new EditorImpl();
    }

我们在

可以看到,我们在读取解析完 XML 文件的时候,直接返回了一个 Editor 的实现类 EditorImpl。我们随便查看一个 putXXX 的方法一看。

private final Object mEditorLock = new Object();

@GuardedBy("mEditorLock")
private final Map mModified = new HashMap<>();

@GuardedBy("mEditorLock")
private boolean mClear = false;

@Override
public Editor putString(String key, @Nullable String value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

可以看到,我们在 EditorImpl 里面使用了一个 HashMap 来存放我们的键值对数据,每次 put 的时候都会直接往这个键值对变量 mModified 中进行数据的 put 操作。

commit() 和 apply()

我们总是在更新数据后需要加上 commit() 或者 apply() 来进行输入的写入操作,我们不妨来看看他们的实现到底有什么区别。

先看 commit() 和 apply() 的源码。

@Override
public boolean commit() {
    long startTime = 0;

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

    MemoryCommitResult mcr = commitToMemory();

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

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

可以看到,apply()commit() 的区别是在 commit() 把内容同步提交到了硬盘,而 apply() 是先立即把修改提交给了内存,然后开启了一个异步的线程提交到硬盘。commit() 会接收 MemoryCommitResult 里面的一个 boolean 参数作为结果,而 apply() 没有对结果做任何关心。

我们可以看到,文件写入更新的操作都是交给 commitToMemory() 做的,这个方法返回了一个 MemoryCommitResult 对象,我们来看看到底做了什么。

// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    List keysModified = null;
    Set listeners = null;
    Map mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        // 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(mMap);
        }
        mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            keysModified = new ArrayList();
            listeners = new HashSet(mListeners.keySet());
        }

        synchronized (mEditorLock) {
            boolean changesMade = false;

            if (mClear) {
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.clear();
                }
                mClear = false;
            }

            for (Map.Entry 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 (!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, keysModified, listeners,
            mapToWriteToDisk);
}

可以看到,我们这里的 mMap 即存放当前 SharedPreferences 文件中的键值对,而 mModified 则存放的是当时 edit() 时 put 进去的键值对,这个我们前面有所介绍。这里有个 mDiskWritesInFlight 看起来应该是表示正在等待写的操作数量。

接下来我们首先处理了 edit().clear() 操作的 mClear 标志,当我们在外面调用 clear() 方法的时候,我们会把 mClear 设置为 true,这时候我们会直接通过 mMap.clear() 清空此时文件中的键值对,然后再遍历 mModified 中新 put 进来的键值对数据放到 mMap 中。也就是说:在一次提交中,如果我们又有 put 又有 clear() 操作的话,我们只能 clear() 掉之前的键值对,这次 put() 进去的键值对还是会被写入到 XML 文件中。

// 读取
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
// 写入
val editor = sharedPreferences.edit()
editor.putInt("1", 123)
editor.clear()
editor.apply()
Log.e("nanchen2251", "${sharedPreferences.getInt("1", 0)}")

也就是说,当我们编写下面的代码的时候,得到的打印还是 123。

然后我们接着往下看,又发现了另外一个 commit()apply() 都做了调用的方法是 enqueueDiskWrite()

/**
 * Enqueue an already-committed-to-memory result to be written
 * to disk.
 *
 * They will be written to disk one-at-a-time in the order
 * that they're enqueued.
 *
 * @param postWriteRunnable if non-null, we're being called
 *   from apply() and this is the runnable to run after
 *   the write proceeds.  if null (from a regular commit()),
 *   then we're allowed to do this disk write on the main
 *   thread (which in addition to reducing allocations and
 *   creating a background thread, this has the advantage that
 *   we catch them in userdebug StrictMode reports to convert
 *   them where possible to apply() ...)
 */
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    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();
                }
            }
        };

    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

在这个方法中,首先通过判断 postWriteRunnable 是否为 null 来判断是 apply() 还是 commit()。然后定义了一个 Runnable 任务,在 Runnable 中先调用了 writeToFile() 进行了写入和计数器更新的操作。

然后我们再来看看这个 writeToFile() 方法做了些什么。

@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    long startTime = 0;
    long existsTime = 0;
    long backupExistsTime = 0;
    long outputStreamCreateTime = 0;
    long writeTime = 0;
    long fsyncTime = 0;
    long setPermTime = 0;
    long fstatTime = 0;
    long deleteTime = 0;

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

    boolean fileExists = mFile.exists();

    if (DEBUG) {
        existsTime = System.currentTimeMillis();

        // Might not be set, hence init them to a default value
        backupExistsTime = existsTime;
    }

    // Rename the current file so it may be used as a backup during the next read
    if (fileExists) {
        boolean needsWrite = false;

        // Only need to write if the disk state is older than this commit
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {
                needsWrite = true;
            } else {
                synchronized (mLock) {
                    // No need to persist intermediate states. Just wait for the latest state to
                    // be persisted.
                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                        needsWrite = true;
                    }
                }
            }
        }

        if (!needsWrite) {
            mcr.setDiskWriteResult(false, true);
            return;
        }

        boolean backupFileExists = mBackupFile.exists();

        if (DEBUG) {
            backupExistsTime = System.currentTimeMillis();
        }
        // 此处需要注意一下
        if (!backupFileExists) {
            if (!mFile.renameTo(mBackupFile)) {
                Log.e(TAG, "Couldn't rename file " + mFile
                      + " to backup file " + mBackupFile);
                mcr.setDiskWriteResult(false, false);
                return;
            }
        } else {
            mFile.delete();
        }
    }

    // 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 (DEBUG) {
            outputStreamCreateTime = System.currentTimeMillis();
        }

        if (str == null) {
            mcr.setDiskWriteResult(false, false);
            return;
        }
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

        writeTime = System.currentTimeMillis();

        FileUtils.sync(str);

        fsyncTime = System.currentTimeMillis();

        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

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

        try {
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized (mLock) {
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            }
        } catch (ErrnoException e) {
            // Do nothing
        }

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

        // Writing was successful, delete the backup file if there is one.
        mBackupFile.delete();

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

        mDiskStateGeneration = mcr.memoryStateGeneration;

        mcr.setDiskWriteResult(true, true);

        if (DEBUG) {
            Log.d(TAG, "write: " + (existsTime - startTime) + "/"
                    + (backupExistsTime - startTime) + "/"
                    + (outputStreamCreateTime - startTime) + "/"
                    + (writeTime - startTime) + "/"
                    + (fsyncTime - startTime) + "/"
                    + (setPermTime - startTime) + "/"
                    + (fstatTime - startTime) + "/"
                    + (deleteTime - startTime));
        }

        long fsyncDuration = fsyncTime - writeTime;
        mSyncTimes.add((int) fsyncDuration);
        mNumSync++;

        if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
            mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
        }

        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, false);
}

代码比较长,做了一些时间的记录和 XML 的相关处理,但最值得我们关注的还是其中打了标注的对于 mBackupFile 的处理。我们可以明显地看到,在我们写入文件的时候,我们会把此前的 XML 文件改名为一个备份文件,然后再将要写入的数据写入到一个新的文件中。如果这个过程执行成功的话,就会把备份文件删除。由此可见:即使我们每次只是添加一个键值对,也会重新写入整个文件的数据,这也说明了 SharedPreferences 只适合保存少量数据,文件太大会有性能问题。

看完了这个 writeToFile() ,我们再来看看下面做了啥。

// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
    boolean wasEmpty = false;
    synchronized (mLock) {
        wasEmpty = mDiskWritesInFlight == 1;
    }
    if (wasEmpty) {
        writeToDiskRunnable.run();
        return;
    }
}

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);       

可以看到,当且仅当是 commit() 并且只有一个待写入操作的时候才能直接执行到 writeToDiskRunnable.run(),否则都会执行到 QueuedWorkqueue() 方法,这个 QueuedWork 又是什么东西?

/** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */
@GuardedBy("sLock")
private static final LinkedList sFinishers = new LinkedList<>();

/** Work queued via {@link #queue} */
@GuardedBy("sLock")
private static final LinkedList sWork = new LinkedList<>();
/**
 * Internal utility class to keep track of process-global work that's outstanding and hasn't been
 * finished yet.
 *
 * New work will be {@link #queue queued}.
 *
 * It is possible to add 'finisher'-runnables that are {@link #waitToFinish guaranteed to be run}.
 * This is used to make sure the work has been finished.
 *
 * This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism
 * to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for
 * other things in the future.
 *
 * The queued asynchronous work is performed on a separate, dedicated thread.
 *
 * @hide
 */
public class QueuedWork {
     /**
     * Add a finisher-runnable to wait for {@link #queue asynchronously processed work}.
     *
     * Used by SharedPreferences$Editor#startCommit().
     *
     * Note that this doesn't actually start it running.  This is just a scratch set for callers
     * doing async work to keep updated with what's in-flight. In the common case, caller code
     * (e.g. SharedPreferences) will pretty quickly call remove() after an add(). The only time
     * these Runnables are run is from {@link #waitToFinish}.
     *
     * @param finisher The runnable to add as finisher
     */
    public static void addFinisher(Runnable finisher) {
        synchronized (sLock) {
            sFinishers.add(finisher);
        }
    }

    /**
     * Remove a previously {@link #addFinisher added} finisher-runnable.
     *
     * @param finisher The runnable to remove.
     */
    public static void removeFinisher(Runnable finisher) {
        synchronized (sLock) {
            sFinishers.remove(finisher);
        }
    }

    /**
     * Trigger queued work to be processed immediately. The queued work is processed on a separate
     * thread asynchronous. While doing that run and process all finishers on this thread. The
     * finishers can be implemented in a way to check weather the queued work is finished.
     *
     * 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() {
        long startTime = System.currentTimeMillis();
        boolean hadMessages = false;

        Handler handler = getHandler();

        synchronized (sLock) {
            if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
                // Delayed work will be processed at processPendingWork() below
                handler.removeMessages(QueuedWorkHandler.MSG_RUN);

                if (DEBUG) {
                    hadMessages = true;
                    Log.d(LOG_TAG, "waiting");
                }
            }

            // We should not delay any work as this might delay the finishers
            sCanDelay = false;
        }

        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
        try {
            processPendingWork();
        } finally {
            StrictMode.setThreadPolicy(oldPolicy);
        }

        try {
            while (true) {
                Runnable finisher;

                synchronized (sLock) {
                    finisher = sFinishers.poll();
                }

                if (finisher == null) {
                    break;
                }

                finisher.run();
            }
        } finally {
            sCanDelay = true;
        }

        synchronized (sLock) {
            long waitTime = System.currentTimeMillis() - startTime;

            if (waitTime > 0 || hadMessages) {
                mWaitTimes.add(Long.valueOf(waitTime).intValue());
                mNumWaits++;

                if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
                    mWaitTimes.log(LOG_TAG, "waited: ");
                }
            }
        }
    }
    /**
     * Queue a work-runnable for processing asynchronously.
     *
     * @param work The new runnable to process
     * @param shouldDelay If the message should be delayed
     */
    public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }
}

简单地说,这个 QueuedWork 类里面有一个专门存放 Runnable 的两个 LinkedList 对象,他们分别对应未完成的操作 sFinishers 和正在工作的 sWork
我们在 waitToFinish() 方法中,会不断地去遍历执行未完成的 Runnable。我们根据注释也知道了这个方法会在 ActivityonPause()BroadcastReceiveronReceive() 方法后调用。假设我们频繁的调用了 apply()方法,并紧接着调用了 onPause() ,那么就可能会发生 onPause() 一直等待 QueuedWork.waitToFinish 执行完成而产生 ANR。也就是说,即使是调用了 apply() 方法去异步提交,也不是完全安全的。如果 apply() 方法使用不当,也是可能出现 ANR 的。

总结

说了这么多,我们当然还是需要做一个总结。

  1. apply() 没有返回值而 commit() 返回 boolean 表明修改是否提交成功 ;
  2. commit() 是把内容同步提交到硬盘的,而 apply() 先立即把修改提交到内存,然后开启一个异步的线程提交到硬盘,并且如果提交失败,你不会收到任何通知。
  3. 所有 commit() 提交是同步过程,效率会比 apply() 异步提交的速度慢,在不关心提交结果是否成功的情况下,优先考虑 apply() 方法。
  4. apply() 是使用异步线程写入磁盘,commit() 是同步写入磁盘。所以我们在主线程使用的 commit() 的时候,需要考虑是否会出现 ANR 问题。
  5. 我们每次添加键值对的时候,都会重新写入整个文件的数据,所以它不适合大量数据存储。
  6. 多线程场景下效率比较低,因为 get 操作的时候,会锁定 SharedPreferencesImpl 里面的对象,互斥其他操作,而当 putcommit()apply() 操作的时候都会锁住 Editor 的对象,在这样的情况下,效率会降低。
  7. 由于每次都会把整个文件加载到内存中,因此,如果 SharedPreferences 文件过大,或者在其中的键值对是大对象的 JSON 数据则会占用大量内存,读取较慢是一方面,同时也会引发程序频繁 GC,导致的界面卡顿。
  8. get 操作都是线程安全的, 并且 get 仅仅是从内存中 (mMap) 获取数据, 所以无性能问题。
    基于以上缺点:
  9. 建议不要存储较大数据到 SharedPreferences,也不要把较多数据存储到同一个 name 对应的 SharedPreferences 中,最好根据规则拆分为多个 SharedPreferences 文件。
  10. 频繁修改的数据修改后统一提交,而不是修改过后马上提交。
  11. 在跨进程通讯中不去使用 SharedPreferences
  12. 获取 SharedPreferences 对象的时候会读取 SharedPreferences 文件,如果文件没有读取完,就执行了 get 和 put 操作,可能会出现需要等待的情况,因此最好提前获取 SharedPreferences 对象。
  13. 每次调用 edit() 方法都会创建一个新的 EditorImpl 对象,不要频繁调用 edit() 方法。
    参考链接:https://juejin.im/post/5adc444df265da0b886d00bc#heading-10

你可能感兴趣的:(每日一问:谈谈 SharedPreferences 的 apply() 和 commit())