码迷,mamicode.com
首页 > 移动开发 > 详细

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

时间:2019-07-08 19:48:13      阅读:173      评论:0      收藏:0      [点我收藏+]

标签:message   mst   解析   ignore   场景   char   轻量   XML   real   

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_READABLE 和 MODE_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);
}   

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

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 有三种获取实例的方法,但归根结底都是调用的 Context 的 getSharedPreferences() 方法。由于 Android 的 Context 类采用的是装饰者模式,而装饰者对象其实就是 ContextImpl,所以我们来看看源码是怎么实现的。

// 存放的是名称和文件夹的映射,实际上这个名称就是我们外面传进来的 name
private ArrayMap<String, File> 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<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

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

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

    final String packageName = getPackageName();
    ArrayMap<File, SharedPreferencesImpl> 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<String, Object> 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<String, Object>) 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","")

SharedPreferences 的 getXXX() 方法可能会报 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<String, Object> 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<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String, Object> 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<String, Object>(mMap);
        }
        mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

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

        synchronized (mEditorLock) {
            boolean changesMade = false;

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

            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 (!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(),否则都会执行到 QueuedWork 的 queue() 方法,这个 QueuedWork 又是什么东西?

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

/** Work queued via {@link #queue} */
@GuardedBy("sLock")
private static final LinkedList<Runnable> 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。我们根据注释也知道了这个方法会在 Activity 的 onPause() 和 BroadcastReceiver 的 onReceive() 方法后调用。假设我们频繁的调用了 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 里面的对象,互斥其他操作,而当 put、commit() 和 apply() 操作的时候都会锁住 Editor 的对象,在这样的情况下,效率会降低。
  7. 由于每次都会把整个文件加载到内存中,因此,如果 SharedPreferences 文件过大,或者在其中的键值对是大对象的 JSON 数据则会占用大量内存,读取较慢是一方面,同时也会引发程序频繁 GC,导致的界面卡顿。

基于以上缺点:

  1. 建议不要存储较大数据到 SharedPreferences,也不要把较多数据存储到同一个 name 对应的 SharedPreferences 中,最好根据规则拆分为多个 SharedPreferences 文件。
  2. 频繁修改的数据修改后统一提交,而不是修改过后马上提交。
  3. 在跨进程通讯中不去使用 SharedPreferences。
  4. 获取 SharedPreferences 对象的时候会读取 SharedPreferences 文件,如果文件没有读取完,就执行了 get 和 put 操作,可能会出现需要等待的情况,因此最好提前获取 SharedPreferences 对象。
  5. 每次调用 edit() 方法都会创建一个新的 EditorImpl 对象,不要频繁调用 edit() 方法。
    参考链接:https://juejin.im/post/5adc444df265da0b886d00bc#heading-10

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

标签:message   mst   解析   ignore   场景   char   轻量   XML   real   

原文地址:https://www.cnblogs.com/liushilin/p/11153041.html

踩
(0)
赞
(0)
   
举报
评论 一句话评论(0)
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!