SharePreferences 分析及正确使用姿势

1.Android 常见数据存储方式

在 Android 中,常用数据存储方式通常有以下几类:

文件存储:将数据存储在文件中。文件存储根据位置不同,可以存储在应用包下,成为内部存储;也可以存储在 storage文件夹(当然也有可能是mnt文件夹,不同的手机厂商可能不一样)上,称之为外部存储

在这里插入图片描述

  • SharedPreferences 存储:SharedPreferences 是 Android 提供的用来存储一些简单配置信息的一种机制,核心原理是:保存基于 XML 文件存储的 key-value 键值对数据。通常使用该种方式用来存储一些简单信息,例如:应用版本信息,应用主题类型,开关配置,用户信息等等。其采用 Map 数据结构来存储数据,以键值对方式存储,使得读取与写入很方便,属于一种轻量级的存储机制。
  • SQLite数据库存储:Android 系统中轻量级关系型数据,允许用户进行创建表结构,存储应用数据等操作
  • 使用 ContentProvider 存储数据:在应用程序之间,共享或者传递相关信息时,往往可以使用 Content Provider 和 ContentResolver 实现
  • 网络获取:存储在服务器上,通过接口数据从服务器后台获取,需要网络访问

2.SharedPreferences

SharedPreferences 本身是一个接口,无法直接创建 SharedPreferences 实例。可以通过 Context 提供的 getSharedPreferences(String name, int mode)方法来获取 SharedPreferences 实例,第一个参数表示要操作的xml文件名,第二个参数表示操作模式:MODE_PRIVATE、MODE_WORLD_READABLE、MODE_WORLD_WRITEABLE,推荐使用 MODE_PRIVATE。

Editor:SharedPreferences 只能获取数据,不能存储和修改。存储修改是通过 SharedPreferences.edit() 获取的内部接口 Editor 对象实现。

SharedPreferences 简单使用:

val sp = getSharedPreferences("sp_test", Context.MODE_PRIVATE)
val edit = sp.edit()
edit.putString("key", "123")
edit.putInt("number", 10)
edit.apply()
println("sp_string " + sp.getString("key", ""))
println("sp_string " + sp.getInt("number", 0))

SharedPreferences 对应的 xml 文件位置:/data/data/package name/shared_prefs/

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <int name="number" value="10" />
    <string name="key">123</string>
</map>

3. SharedPreferences 源码分析

在分析 SharedPreferences 源码之前,我们带着几个问题来看:

  • 为什么 SharedPreferences 适用于少量数据存储?
  • SharedPreferences 的 commit 和 apply 有什么区别,分别在什么场景下使用?
  • 为什么 SharedPreferences 会导致卡顿、ANR 等问题?
  • 如何正确使用 SharedPreferences,以及我们可以做哪些优化?
  • 多进程下使用 SharedPreferences 是否安全?
3.1 SharedPreferences 实例获取

SharedPreferences 实例获取是通过 Context 来获取,具体实现在 ContextImpl 中。在 ContextImpl 中有这几个变量:

// 记录所有的 SharedPreferences 文件,key 为文件名,value 为文件
private ArrayMap<String, File> mSharedPrefsPaths;

// 以包名为key, 二级key是以SP文件, 以 SharedPreferencesImpl 为value 的嵌套 map 结构. 注意:sSharedPrefsCache 是静态类成员变量, 每个进程只保存唯一一份, 且由 ContextImpl.class 锁保护.
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

// 记录 SharedPreferences 所在文件路径,/data/data/包名/shared_prefs/
private File mPreferencesDir;

下面来看看如果获取 SharedPreferences 实例:

首先调用 getSharedPreferences 方法,该方法先找到名字为 name 的 xml 文件,不存在的话则创建一个新文件。可以看到 mSharedPrefsPaths 在这个方法中创建,mSharedPrefsPaths 不是 static 变量,相当于每个 Context 都有一个 mSharedPrefsPaths,但相当于 static 变量,因为创建时是以 ContextImpl.class 加锁保护的,也就是说同一个文件名的 xml 文件,有且仅有一个。

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    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);
}

获取指定路径下的 Sharepreferences 文件

// 返回的是获取的 Sharepreferences 文件 File,文本全路径为 /data/data/包名/shared_prefs/name.xml
@Override
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}

// mPreferencesDir 为 Sharepreferences 的文件路径,即 /data/data/包名/shared_prefs/
@UnsupportedAppUsage
private File getPreferencesDir() {
    synchronized (mSync) {
        if (mPreferencesDir == null) {
            mPreferencesDir = new File(getDataDir(), "shared_prefs");
        }
        return ensurePrivateDirExists(mPreferencesDir);
    }
}

有了 name.xml 文件,开始创建 SharedPreferencesImpl 对象,注意只是首次创建,后面不再创建,因为内存中有缓存该对象。

@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");
                }
            }
            // 创建 SharedPreferencesImpl 对象并缓存起来
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
    // MODE_MULTI_PROCESS 已经废弃,不再建议使用,多进程下不能保证数据安全
    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;
}

// 创建缓存集合 sSharedPrefsCache,key 是包名,value 是以 file 为key,value 为 SharedPreferencesImpl 对象的嵌套 map 集合,也就是获取该当前包名下 ArrayMap<File, SharedPreferencesImpl>,file 和 SharedPreferencesImpl 一一对应。
@GuardedBy("ContextImpl.class")
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;
}

android N 以后不再支持 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE。

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

下面来看下 MODE_MULTI_PROCESS 这种扩进程方式是不安全的,在 MODE_MULTI_PROCESS 模式下,如果文件被修改过或重新加载,使得数据是最新的。

@UnsupportedAppUsage
void startReloadIfChangedUnexpectedly() {
    synchronized (mLock) {
        // TODO: wait for any pending writes to disk?
        if (!hasFileChangedUnexpectedly()) {
            return;
        }
        startLoadFromDisk();
    }
}

// 获取文件 mFile 的 StructStat 信息,根据文件的时间戳和文件大小判定是不是修改过,注意虽然加了 mLock,包括重新加载方法 startLoadFromDisk 也是加锁的,但是在多进程下,是有多个虚拟机的,加载对于多进程是无效的,所以对于多进程下使用 MODE_MULTI_PROCESS 是不安全的。
private boolean hasFileChangedUnexpectedly() {
    synchronized (mLock) {
        if (mDiskWritesInFlight > 0) {
            // If we know we caused it, it's not unexpected.
            if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
            return false;
        }
    }
    final StructStat stat;
    try {
        BlockGuard.getThreadPolicy().onReadFromDisk();
        stat = Os.stat(mFile.getPath());
    } catch (ErrnoException e) {
        return true;
    }
    // 
    synchronized (mLock) {
        return !stat.st_mtim.equals(mStatTimestamp) || mStatSize != stat.st_size;
    }
}
3.2 SharedPreferences 分析

在这里插入图片描述

SharedPreferences 与 Editor 是两个接口,SharedPreferencesImpl 和 EditorImpl 分别实现了对应接口. SharedPreferencesImpl 提供查询各个类型数据的方法,getXX(String key, float defValue),而如果是存储数据和删除等操作,需要通过 edit() 方法获取 EditorImpl 实例来操作。

  • 先来看下 SharedPreferencesImpl 构造
SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    startLoadFromDisk();
}

这里主要做两件事,一个是先备份 name.xml 文件,备份文件名字是 name.bak。另一件是从文件中加载数据到内存中。下面主要看下加载过程。

// 通过 mLock 锁来确保 mLoaded 状态同步,然后开启一个线程来加载文件
@UnsupportedAppUsage
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

// 开始加载文件到内存中
private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        删除原有文件,并将备份文件重命名为 mFile
        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");
    }

    // 这段主要操作是通过流操作来读取 XML 文件,将里面的 key-value 读取到 Map 中
    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;
    }

    // 读取完成后的操作
    // 1.记录文件的操作时间和文件大小
    // 2.读取完成后则通知其他等待的位置唤醒,继续执行操作,因为等待的地方是在等待文件加载完成,也就是说任何操作都是需要在文件加载完成的基础上执行的
    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();
        }
    }
}

文件未加载前任何操作都需要等待,getXX 方法和 edit 方法

@GuardedBy("mLock")
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);
    }
}
  • 查询方法

查询方法比较简单,文件加载完成后则直接从内存中读取数据,即从 Map 中查询数据,以 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;
    }
}

注意,读取操作是同步操作,因为 mMap 中数据是有可能改变的,这里猜想一下,为什么不用 ConcurrentHashMap 呢?是不就可以不用加锁处理了?个人认为是可以的,但是有一点 ConcurrentHashMap 是在 jdk 5.0 才出现的,意味着之前的版本还是得用加锁处理,索性干脆直接加锁处理。这里除了加锁处理还有一个 awaitLoadedLocked() 方法,即上面提到的需要在文件加载完成才可以查询,否则会阻塞在这里。

  • edit 方法
@Override
public Editor edit() {
    synchronized (mLock) {
        awaitLoadedLocked();
    }

    return new EditorImpl();
}

同样需要等待文件加载完成,否则也会等待

  • EditorImpl

EditorImpl 实现 Editor 接口,完成 SharePreferences 的添加、删除、清除、commit 和 apply 操作。EditorImpl 有两个变量 mModified 和 mClear。putXX,remove 操作都是基于 mModified 进行的,mClear 用于 clear 方法。

下面看下 putString 和 remove 方法,方法操作都加了所操作,保证数据的同步性,remove 方法有点特殊,待删除的值用 EditorImpl 自己来占位

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

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

调用 EditorImpl 的 putXX 等方法仅仅将要操作的元素放在 mModified,还没有和 SharePreferences 中的元素同步以及写入文件中,调用 commit 或者 apply 方法才会进行提交到内存并写入文件中。

  • 数据提交

commit 方法:

@Override
public boolean commit() {
    long startTime = 0;
    // 提交到内存中,将 mModified 中数据和 SharePreferences 中 Map 同步
    MemoryCommitResult mcr = commitToMemory();

    // 开启写入文件任务,可以看到注释中提示 commit 方法是同步操作,即在当前线程直接指定写文件操作,这一点需要注意
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
        // 这里虽然调用了 await,并不会阻塞,因为 commit 方法是同步操作执行完成时 writtenToDiskLatch 中当前的 count 是 0,,会直接向下执行。
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

commit 是同步操作方法,也是耗时方法,毕竟需要进行写文件操作。执行分为两步:

  • 将修改的数据提交到内存中 commitToMemory
  • enqueueDiskWrite,执行写文件操作
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    boolean keysCleared = false;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    // 最终需要写入文件的数据
    Map<String, Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {

        // 如果正在写文件,则拷贝一份 mMap
        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 在这里时代表的旧的数据
        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;

           // 调用 EditorImpl 的 clear 方法会走这里
            if (mClear) {
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.clear();
                }
                keysCleared = true;
                mClear = false;
            }
            // 这里遍历 EditorImpl 中的 mModified,并和 mapToWriteToDisk 中的数据对比,此时 mapToWriteToDisk 中是修改前的数据,遍历完成后则修改为最终新的数据
            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.
                // 注意这里的 this,上面提到过 this 在删除时用于占位,被删除的 key,则会从 mapToWriteToDisk 中删除
                if (v == this || v == null) {
                    if (!mapToWriteToDisk.containsKey(k)) {
                        continue;
                    }
                    mapToWriteToDisk.remove(k);
                } else {
                    // mapToWriteToDisk 中已经有了 mModified 中的元素,则不进行操作
                    if (mapToWriteToDisk.containsKey(k)) {
                        Object existingValue = mapToWriteToDisk.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    // 否则不存在或者元素修改过,则添加 mapToWriteToDisk 中或者覆盖旧的元素
                    mapToWriteToDisk.put(k, v);
                }

                changesMade = true;
                if (hasListeners) {
                    keysModified.add(k);
                }
            }

            mModified.clear();

            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }

            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
            listeners, mapToWriteToDisk);
}

commitToMemory() 方法中代码虽然有点多,但是逻辑还是比较清晰的,主要的操作就是先将 SharePreferences 中 mMap 赋给 mapToWriteToDisk,此时 mapToWriteToDisk 中是修改前的元素,再和 EditorImpl 中的 mModified 中元素对比,得到最终的元素,这样就将修改结果同步到内存中了。

enqueueDiskWrite 是执行写文件的入口,commit 方法调用时传入的 postWriteRunnable 为 null,所以 isFromSyncCommit 为 true,代表同步操作,不需要提交到 QueuedWork 的队列中。

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.
    // 在 commitToMemory 中 mDiskWritesInFlight++,所以 对于 commit 方法,会直接调用 writeToDiskRunnable.run(),直接进行写文件操作
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

写文件操作

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

    boolean fileExists = mFile.exists();

    // 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) {
        // 如果是同步写,即调用 commit 则需要写入
            if (isFromSyncCommit) {
                needsWrite = true;
            } else {
            // 调用 apply 方法时,需要保持 mcr 中数据和  SharePreferences 中的数据保持一致才进行写入,比如对于同一个sp文件,连续调用 n 次 apply ,就会有 n 次写入磁盘任务执行,实际上只需要最后执行最后那次就可以了,最后那次提交对应内存的 map 是持有最新的数据,所以就可以省掉前面 n-1 次的执行,这个就是 android 8.0 中做的优化
                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 (!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 (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);

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

        // 写入成功,删除备份文件
        mBackupFile.delete();
        mDiskStateGeneration = mcr.memoryStateGeneration;

        // 结果通知,写入成功,唤醒阻塞的位置
        mcr.setDiskWriteResult(true, true);

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

        return;
    } catch (XmlPullParserException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    } catch (IOException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    }

    // 写入失败请款下则删除 mFile,下次从备份文件恢复
    if (mFile.exists()) {
        if (!mFile.delete()) {
            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }
    mcr.setDiskWriteResult(false, false);
}

写入文件主要是将 commitToMemory() 方法中的 mapToWriteToDisk 集合数据写入文件中,可以看出每次写入都是全量写入文件,如果写入成功则删除备份文件,如果写入失败则删除mFile.
可见, 每次commit是把全部数据更新到文件, 所以每个文件的数据量必须保证足够精简。每次修改数据进行一次性调用 commit,而且尽量不要在主线程中调用,因为 commit 方法是同步操作,写文件耗时操作会在当前线程中执行,在主线程调用可能会导致卡顿、ANR 等问题。

再来看看 apply 方法

@Override
public void apply() {
    final long startTime = System.currentTimeMillis();

    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }

                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    notifyListeners(mcr);
}

apply 方法中多了一个 awaitCommit,这是一个等待操作,android 系统会在 Activity 的 onStop ,onPause 等生命周期中,调用 QueuedWork.waitToFinish,等待落盘的任务队列执行完成,但是如果任务队列中的任务很多,或者待写入的数据量很大时(sp 文件是全量读写的),在一些 io 性能差的中低端机型上就会很容易出现 anr。

在 Android 8.0 下进行了优化,8.0 之前只会等待 awaitCommit,在低端机上多次调用 apply 时很容易出现 ANR,8.0 之后 会主动触发 processPendingWork 取出写任务列表中依次执行,而不是只在在等待。同时 8.0 之后对多次调用 apply 方法也进行了优化,比如对于同一个sp文件,连续调用 n 次 apply ,就会有 n 次写入磁盘任务执行,实际上只需要最后执行最后那次就可以了,最后那次提交对应内存的 map 是持有最新的数据,所以就可以省掉前面 n-1 次的执行,这个就是 android 8.0 中做的优化。

4. SharePreferences 优化

4.1 apply 和 commit 主要区别
  • 在于 apply 的写入文件操作是在单线程的线程池来完成。apply方法开始的时候, 会把 awaitCommit 放入 QueuedWork;文件写入操作完成, 则会把相应的 awaitCommit 从 QueuedWork 中移除。QueuedWork 在这里存在的价值主要是用于在 Stop Service, finish BroadcastReceiver 过程用于判定是否处理完所有的异步 SP 操作.
  • apply没有返回值, commit 有返回值能知道修改是否提交成功
  • apply是将修改提交到内存,再异步提交到磁盘文件; commit 是同步的提交到磁盘文件;
  • 多并发的提交 commit 时,需等待正在处理的 commit 数据更新到磁盘文件后才会继续往下执行,从而降低效率; 而 apply 只是原子更新到内存,后调用 apply 函数会直接覆盖前面内存数据,从一定程度上提高很多效率。
4.2 SharedPreferences 使用建议
  • 在工作线程中写入 sp 时,直接调用 commit 就可以,不必调用 apply,这种情况下,commit 开销更小
  • 在主线程中写入 sp 时,不要调用 commit,要调用 apply
  • sp 对应的文件尽量不要太大,按照模块创建不同的 sp 文件,而不是一个整个应用都读写一个 sp 文件。适当地拆分文件, 可以减少同步锁竞争,并提高写入效率。
  • sp 适合读写轻量的、小的配置信息,不适合保存大数据量的信息,比如大的 json 字符串,有助于减少卡顿/anr
  • 使用 SharedPreferences 是最好不要一上来就执行 getSharedPreferences().edit(),将 getSharedPreferences() 和 edit 过程拆分,从源码分析中可以看到,edit 会等待文件加载完成,此时会造成阻塞,容易导致卡顿等问题出现
  • 当有连续的调用 putXX 方法操作时(特别是循环中),当确认不需要立即读取时,最后一次调用 commit 或 apply 即可
  • 不要使用MODE_MULTI_PROCESS
4.3 SharedPreferences 其他优化

在 8.0 以下没优化之前,如果防止等待任务导致的卡顿和 ANR 问题呢?要想解决的话,可以尝试清理锁队列,风险就是可能导致数据存储失败。

具体操作:

Activity 的 onStop,以及 Service 的 onStop 和 onStartCommand 都是通过 ActivityThread 触发的,ActivityThread 中有一个 Handler 变量,我们通过 Hook 拿到此变量,给此 Handler 设置一个 callback,Handler 的 dispatchMessage 中会先处理 callback。

try {
    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Method currentAtyThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
    Object activityThread = currentAtyThreadMethod.invoke(null);

    Field mHField = activityThreadClass.getDeclaredField("mH");
    mHField.setAccessible(true);
    Handler handler = (Handler) mHField.get(activityThread);

    Field mCallbackField = Handler.class.getDeclaredField("mCallback");
    mCallbackField.setAccessible(true);
    mCallbackField.set(handler,new SpCompatCallback());
    Log.d(TAG,"hook success");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
} catch (NoSuchFieldException e) {
    e.printStackTrace();
} catch (NoSuchMethodException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
} catch (Throwable e){
    e.printStackTrace();
}

自定义callbak:SpCompatCallback,在这个方法中做清理等待锁列表的操作:

public class SpCompatCallback implements Handler.Callback {


    public SpCompatCallback(){
    }

    //handleServiceArgs
    private static final int SERVICE_ARGS = 115;
    //handleStopService
    private static final int STOP_SERVICE = 116;
    //handleSleeping
    private static final int SLEEPING = 137;
    //handleStopActivity
    private static final int STOP_ACTIVITY_SHOW = 103;
    //handleStopActivity
    private static final int STOP_ACTIVITY_HIDE = 104;
    //handlePauseActivity
    private static final int PAUSE_ACTIVITY = 101;
    //handlePauseActivity
    private static final int PAUSE_ACTIVITY_FINISHING = 102;

    @Override
    public boolean handleMessage(Message msg) {
        switch (msg.what){
            case SERVICE_ARGS:
                SpHelper.beforeSpBlock("SERVICE_ARGS");
                break;
            case STOP_SERVICE:
                SpHelper.beforeSpBlock("STOP_SERVICE");
                break;
            case SLEEPING:
                SpHelper.beforeSpBlock("SLEEPING");
                break;
            case STOP_ACTIVITY_SHOW:
                SpHelper.beforeSpBlock("STOP_ACTIVITY_SHOW");
                break;
            case STOP_ACTIVITY_HIDE:
                SpHelper.beforeSpBlock("STOP_ACTIVITY_HIDE");
                break;
            case PAUSE_ACTIVITY:
                SpHelper.beforeSpBlock("PAUSE_ACTIVITY");
                break;
            case PAUSE_ACTIVITY_FINISHING:
                SpHelper.beforeSpBlock("PAUSE_ACTIVITY_FINISHING");
                break;
            default:
                break;
        }
        return false;
    }
}

清理操作实际上是反射调用 sPendingWorkFinishers.clear();

public class SpHelper {
    private static final String TAG = "SpHelper";
    private static boolean init = false;
    private static String CLASS_QUEUED_WORK = "android.app.QueuedWork";
    private static String FIELD_PENDING_FINISHERS = "sPendingWorkFinishers";
    private static ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = null;

    public static void beforeSpBlock(String tag){
        if(!init){
            getPendingWorkFinishers();
            init = true;
        }
        Log.d(TAG,"beforeSpBlock "+tag);
        if(sPendingWorkFinishers != null){
            sPendingWorkFinishers.clear();
        }
    }

    private static void getPendingWorkFinishers() {
        Log.d(TAG,"getPendingWorkFinishers");
        try {
            Class clazz = Class.forName(CLASS_QUEUED_WORK);
            Field field = clazz.getDeclaredField(FIELD_PENDING_FINISHERS);
            field.setAccessible(true);
            sPendingWorkFinishers = (ConcurrentLinkedQueue<Runnable>) field.get(null);
            Log.d(TAG,"getPendingWorkFinishers success");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (Throwable e){
            e.printStackTrace();
        }

    }
}

5 参考

全面剖析SharedPreferences

剖析 SharedPreference apply 引起的 ANR 问题

SharedPreferences ANR问题分析和解决 & Android 8.0的优化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值