文章目录
获取SP实例
- 在Activity中,可通过 getPreferences(mode) 获得sp实例(以当前类名为sp文件名)。
- 任意ContextImpl的子类中,都可通过getSharedPreferences(name,mode)获得sp实例。
Mode选择:
- Context.MODE_PRIVATE: 指定该 SharedPreferences 数据只能被本应用程序读、写;
- Context.MODE_WORLD_READABLE: 已废弃
- Context.MODE_WORLD_WRITEABLE: 已废弃
- Context.MODE_APPEND:该模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件;
//Activity.java
public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
ContextImpl
- sSharedPrefsCache是静态缓存对象,存储着sp文件和SharedPreferenceImpl实现类的对应关系。
- 每一个文件对应了一个SharedPreferenceImpl对象
//ContextImpl.java
//存储了sp文件名和sp文件的对应关系
private ArrayMap<String, File> mSharedPrefsPaths;
//存储了包名和一个arrayMap packagePrefs的对应关系,packagePrefs存储了sp文件和sp实现类的对应关系
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
File file;
//从mSharedPrefsPaths 缓存中,根据name取file
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name); //根据name得到file
if (file == null) { // file不存在则新建并放入缓存中
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//根据包名得到缓存cache(包名以前支持跨进程访问,mode,后来废弃掉了)
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
//根据file得到sp的映射关系
sp = cache.get(file);
if (sp == null) { //sp为null,则新建spImpl,并放入缓存cache中
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
return sp;
}
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
//从sSharedPrefsCache 缓存中,根据包名取file和spImpl的对应关系
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) { //缓存中没有,则新建arrayMap并放入缓存中
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
SP初始化
//ContextImpl.java
private Map<String, Object> mMap;
private boolean mLoaded = false; //标志位,用于记录xml文件是否已经加载完成
private final Object mLock = new Object(); //spImpl的锁对象
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file); //备份文件,用于异常处理
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk(); //实例化时开启子线程,加载xml文件中内容
}
static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
//从磁盘加载xml文件流,并解析为map对象
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) { //如果加载完成,则直接返回
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
Map<String, Object> map = null; //解析xml文件,得到临时map
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; //不管成功还是失败,都是load完成
mThrowable = thrown;
try { //如果没有发生异常,map一定不为空
if (thrown == null) {
if (map != null) {
mMap = map;
} else {
mMap = new HashMap<>();
}
}
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll(); //通知所有等待线程
}
}
}
获取键值对
以getString为例
在 awaitLoadedLocked 方法里面我们使用了 mLock.wait() 来等待初始化的读取操作完成,而我们前面看到的 loadFromDiskLocked() 方法的最后也可以看到它调用了 mLock.notifyAll() 方法来唤醒后面这个阻塞的 getXXX()。
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked(); //等待直到xml文件解析完成,如果解析失败则抛异常,否则继续往下走
String v = (String)mMap.get(key); //经过xml文件解析,map已经转换为内存存储
return v != null ? v : defValue;
}
}
private void awaitLoadedLocked() {
while (!mLoaded) {
try {
mLock.wait(); //xml文件没解析完成,则一致等待
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) { //如果发生异常,则抛异常
throw new IllegalStateException(mThrowable);
}
}
存入键值对
commit & apply
- mMap 即存放当前 SharedPreferences 文件中的键值对
- mModified 则存放的是当时 edit() 时 put/remove 进去的键值对
- mDiskWritesInFlight 看起来应该是表示正在等待写的操作数量。
- 每次调用 edit() 方法都会创建一个新的 EditorImpl 对象,不要频繁调用 edit() 方法
- mcr.writtenToDiskLatch是一个countDownLatch对象
countDownLatch
- 这个类使一个线程等待其他线程各自执行完毕后再执行。
- 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
@Override
public Editor remove(String key) {
synchronized (mEditorLock) {
mModified.put(key, this);
return this;
}
}
@Override
public Editor clear() {
synchronized (mEditorLock) {
mClear = true;
return this;
}
}
@Override
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked(); //等待直到xml文件解析完成,如果解析失败则抛异常,否则继续往下走
}
return new EditorImpl(); //返回一个EditorImpl实例
}
public final class EditorImpl implements Editor {
private final Object mEditorLock = new Object();
private final Map<String, Object> mModified = new HashMap<>();
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value); //存入内存mModified中
return this;
}
}
//提交,无返回结果
@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) {
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
//提交,有返回值
@Override
public boolean commit() {
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
}
return mcr.writeToDiskResult;
}
}
commitToMemory提交到内存中
注意:
editor.clear()只是更改了标志位,mClear 设置为 true。在commitToMemory()时,会直接通过 mMap.clear() 清空此时文件中的键值对,然后再遍历 mModified 中新 put 进来的键值对数据放到 mMap 中。
也就是说:在一次提交中,如果我们又有 put 又有 clear() 操作的话(不管执行先后顺序),我们只能 clear() 掉commit/apply之前的键值对,这次 put() 进去的键值对还是会被写入到 XML 文件中。
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
mMap = new HashMap<String, Object>(mMap); //对mMap克隆一份
}
mapToWriteToDisk = mMap; //内存中存储的键值对集合
mDiskWritesInFlight++; //
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();
if (v == this || v == null) {
//如果内存中不存在对应的key,则不做处理,否则通过remove(k)移除掉
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
//如果内存中存在对应的key且value相同,则不做处理,否则put(k,v)更新/插入
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
changesMade = true;
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
enqueueDiskWrite
commit
- SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null)
- isFromSyncCommit 为true,来自同步commit,则在当前线程执行writeToDiskRunnable
- 执行writeToFile(mcr, true)
apply
- SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable)
- QueuedWork.queue(writeToDiskRunnable, true) 发消息给子线程的handler对象进行处理
- 执行writeToFile(mcr, true)
- 执行postWriteRunnable
在我们写入文件的时候,我们会把此前的 XML 文件改名为一个备份文件,然后再将要写入的数据写入到一个新的文件中。如果这个过程执行成功的话,就会把备份文件删除。
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--
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
if (isFromSyncCommit) { //如果是来自同步commit(),则在当前线程执行writeToDiskRunnable
boolean wasEmpty = false;
//执行commitToMemory,mDiskWritesInFlight++
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
if (!needsWrite) { //不需要写磁盘,直接返回
mcr.setDiskWriteResult(false, true);
return;
}
// Attempt to write the file, delete the backup and return true as atomically as
// possible. If any exception occurs, delete the new file; next time we will restore
// from the backup.
try {
FileOutputStream str = createFileOutputStream(mFile);
//将mcr.map写入磁盘
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
}
// Writing was successful, delete the backup file if there is one.
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);
}
// 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);
}
为什么SharedPreference不适合存储大容量数据
- 一般我们的 getXXX() 方法是写在 UI 线程的,如果这个方法被阻塞的太久,就会发生卡顿现象。
- 读取出来的键值对一直存在内存中,直到应用被销毁才释放。
- 每次添加键值对的时候,都会重新写入整个文件的数据
总结
-
针对同一个name,我们获取到的都是同一个 SharedPreferencesImpl 对象。
-
SharedPreferencesImpl实例化时,会开启一个子线程,通过XmlUtils.readMapXml(),读取xml文件中的所有键值对,存放在内存map中。
-
针对sp,不管是读取还是写入,都是直接操作map内存,且需等待之前的读磁盘完成。
-
commit写入磁盘,是当前线程中同步阻塞式的,有返回值(写入是否成功);
-
apply写入磁盘,在子线程中处理,非阻塞式,没有返回值
-
不管commit、apply都不会影响立即读取(从内存中读),推荐使用apply。