SharedPreferences学习笔记

1,169 阅读4分钟

SharedPreference是用于访问和修改Context.getSharedPreferences返回数据的接口。它是谷歌官方实现的轻量级数据存储的方案,可以用来保存用户信息等等数据。sp提供了很高的一致性保障,但是代价也很高,它很可能会导致anr问题。

SharedPreferences的执行流程

SharedPreferences的获取

我们平常都调用context的getSharedPreferences,这个方法实际是由ContextImpl类实现的。调用它会返回一个SharedPreferencesImpl,对应参数中的文件名。而对于每一个特定的xml文件,所有客户端都对应着同一个sp实例。这种对应关系放在ArrayMap里保存。如果ArrayMap中已经有sp了就直接返回,只有没有的情况下会新建一个sp实例并保存进ArrayMap中。

// getSharedPreferences(String name,int mode)实际上调用 (File file, int mode))
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) {
            // ...
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
        // ...
    }
    return sp;
}
// packagePrefs key:File val:SharedPreferencesImpl 文件和sp一一对应
// sSharedPrefsCache key:packageName val:packagePrefs 包名和上面arraymap一一对应
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;
}

读取数据

SharedPreferencesImpl是SharedPreferences的默认实现类。在被new出来的时候,就调用startLoadFromDisk将k-v对从磁盘上读入内存,为了保证安全的load,使用了mlock做为锁。通过异步的loadFromDisk进程来读取数据,mloaded标志加载是否完成。loadFromDisk的逻辑是如果有备份文件,就用备份文件代替传进来的文件,然后读取kv对存到一个临时的map里去,最终如果没有异常的话再把它赋值给全局的mMap。

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) {
    } catch (Throwable t) {
        thrown = t;
    }
    synchronized (mLock) {
        mLoaded = true;
        mThrowable = thrown;
        try {
            if (thrown == null) {
                if (map != null) {
                    mMap = map;
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                    mMap = new HashMap<>();
                }
            }
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
            mLock.notifyAll();
        }
    }
}

mMap存储k-v键值对,加载完成后无论成功还是失败,都调用mLock.notifyAll()来唤醒所有等待的读进程和editor对象。

// SharedPreferences的getXXX方法都要调用该方法来保证异步load已经完成了
private void awaitLoadedLocked() {
    if (!mLoaded) {
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) {
        try {
            mLock.wait();  // 等待notifyAll唤醒
        } catch (InterruptedException unused) {
        }
    }
    //...
}

public Map<String, ?> getAll() {
    synchronized (mLock) {
        awaitLoadedLocked();
        return new HashMap<String, Object>(mMap);
    }
}
public Editor edit() { 
    synchronized (mLock) { 
        awaitLoadedLocked(); 
    } 
    return new EditorImpl(); 
}

修改数据

为了保持数据保证一致的状态,SharedPreferences内置了一个Editor类来操作一切修改。修改的内容先放入map中存起来,只有commit或apply时才会真正与文件进行操作。

private final Object mEditorLock = new Object(); // 编辑锁
private final Map<String, Object> mModified = new HashMap<>(); 
private boolean mClear = false;
// mModified保存修改的k-v mClear标志是否清空
// 删除键的话 mModified.put(key,this) 做为标志
public Editor putXXX(String key, XXX value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

commit和apply的主要区别在于commit是同步执行的,而apply是异步的可以delay的。可以看到apply和commit的主要流程都是先获得一个MemoryCommitResult对象,然后调用enqueueDiskWrite放入队列中执行,可以看到postWriteRunnable是否为空就是enqueueDiskWrite判断调用方法是commit还是apply的关键。apply的awaitCommit被放入了QueueWork中保证在类似Activity的onstop调用时会去执行,确保数据不会丢失。

public void apply() {
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
        @Override
        public void run() {
            try {
                mcr.writtenToDiskLatch.await(); //等到countdown计数为0之后这个线程才能执行
            } catch (InterruptedException ignored) {
            }
        }
    };
    QueuedWork.addFinisher(awaitCommit);
    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    notifyListeners(mcr);
}

public boolean commit() {
    MemoryCommitResult mcr = commitToMemory();
    SharedPreferencesImpl.this.enqueueDiskWrite(
 mcr, null);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

其实commitToMemory主要是用于生成MemoryCommitResult,包括几个重要变量mapToWriteToDisk,memoryStateGeneration ,keysCleared ,keysModified 用于存入磁盘时的处理。mDiskWritesInFlight标志了当前有几个线程在执行提交操作,因此commitToMemory时它会++,等到writeToFile完毕时,它又会--.memoryStateGeneration是用于标志内存sp的版本,通过比对它和磁盘sp版本,全局的memory sp版本就能确定是否要commit或者apply。

writtenToDiskLatch 是一个倒数计时器,在apply的awaitCommit中它的await()方法就是等待setDiskWriteResult方法执行将计数器减一,使writtenToDiskLatch所在的主线程可以执行。

private static class MemoryCommitResult {
    final long memoryStateGeneration;
    final boolean keysCleared;
    @Nullable final List<String> keysModified;
    @Nullable final Set<OnSharedPreferenceChangeListener> listeners;
    final Map<String, Object> mapToWriteToDisk;
    final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

    @GuardedBy("mWritingToDiskLock")
    volatile boolean writeToDiskResult = false;
    boolean wasWritten = false;

    private MemoryCommitResult(long memoryStateGeneration, boolean keysCleared,
            @Nullable List<String> keysModified,
            @Nullable Set<OnSharedPreferenceChangeListener> listeners,
            Map<String, Object> mapToWriteToDisk) {
            // ...
    }
    // writeToFile 会执行
    void setDiskWriteResult(boolean wasWritten, boolean result) {
        this.wasWritten = wasWritten;
        writeToDiskResult = result;
        writtenToDiskLatch.countDown();
    }
}

private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    boolean keysCleared = false;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String, Object> mapToWriteToDisk;
    synchronized (SharedPreferencesImpl.this.mLock) {
        if (mDiskWritesInFlight > 0) {
            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) {
            // mapToWriteToDisk 的设置逻辑
            if (changesMade) {
                mCurrentMemoryStateGeneration++; // 全局的 
            }
            // 多个进程操作 在writeToFile执行时,memoryStateGeneration和
            // mCurrentMemoryStateGeneration并不一定相同
            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
            listeners, mapToWriteToDisk);
}

在enqueueDiskWrite,根据mDiskWritesInFlight也就是进行提交的线程个数来判断该怎么执行,如果时commit并且mDiskWritesInFlight=1,那么本线程直接可以执行,否则的话放入队列执行。真正执行的函数就是writeToFile,然后如果是apply的话postWriteRunnable阻塞线程等待结果。

 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--;  // 提交完 diskwrite的进程就减1
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();  // awaitCommit等待结果返回
                }
            }
        };

    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1; // 只剩一个并且是commit可以直接本进程执行
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    // 否则放入队列执行 
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

writeToFile的逻辑主要可以分成下面几个步骤:

  • 首先设置needsWrite的布尔值, 首先只有磁盘sp版本比较低才需要write,然后看看是不是commit,或者apply时mcr的sp版本和全局最新的版本一致才更新,否则不需要太频繁的操作。
  • 在needsWrite为true的情况下,看看有没有备份文件,备份文件主要是在修改失败时恢复到上一个数据一致性状态用的,所以如果没有备份文件,就用mFile创建一个,有的话mFile就可以删了,备份文件的设置出错的话,函数就直接返回了。
  • 修改文件去,修改成功的话删除备份文件,并且更新各种值比如磁盘sp版本等等。调用setDiskWriteResult返回结果,这个函数内执行了倒数计时器的countdown操作,使postWriteRunnable不会再阻塞。
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();

    if (fileExists) {
        boolean needsWrite = false;
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {
                needsWrite = true;
            } else {
                synchronized (mLock) {
                    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();
        }
    }
    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);
        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) {
        }
        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);
    }
    if (mFile.exists()) {
        if (!mFile.delete()) {
            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }
    mcr.setDiskWriteResult(false, false);
}

SharedPreferences的问题

在实际使用的情况下,确实发现用了sharedpreferences之后会感觉有点卡顿,我的模拟机比较明显。在阅读sp的源码和别人的博客之后发现,sp在加载和writeToFile时候都是一次性读取整个文件或重写整个文件的,如果xml比较大的话,就会影响性能。

同时由于apply方法会调用QueuedWork.addFinisher(awaitCommit),awaitCommit就是阻塞线程等待writeToFile返回,而在activity.onstop、broadcastReceiver.onReceive、service.handleCommend的时候,会调用QueuedWork.waitToFinish()去执行queuework里面所有的work和finisher。这样的话在activity.onstop的时候就会花很长时间等待,会影响到activtiy生命周期的改变,造成anr问题。

不过为啥一定要调用waitToFinsh还没有很理解,而且在其他人的博客里也看到通过直接清空queuework的等待队列减少anr的产生。

改进的想法

  • 可以自定义SharedPreferences的实现类
  • 调用waitToFinsh方法之前,清空等待队列
  • 学习使用mmkv

感想

学习了sharedpreferences的源码,确实感觉自己对整个流程和使用都有了更加清晰的认识,也大概了解了如果以后想优化该从什么方面着手。在整个阅读的过程中,感觉自己对线程的同步、异步、锁的知识等等了解了还不够多不够深刻,需要以后加强。

参考文档

Android 重学系列 SharedPreferences源码解析

剖析 SharedPreference apply 引起的 ANR 问题

SharedPreferences anr 处理方案_花不掉泪的博客-CSDN博客