Android SharedPrefrence源码详解
1,SharedPrefrences是什么
SharedPrefrences是android.content
包下的一个用于访问和修改getSharedPreferences
返回的Prefrences(预设置/首选项)数据的接口,是Android自有的一种数据存储方式。对于任何一组特定的Prefrences,都有一个所有客户端共享的类实例。
对Prefrences的修改必须通过SharedPrefrences.Editor对象,以确保Prefrences值在提交到存储时保持一致的状态和控制。从各种不同的get
方法返回的对象必须被应用程序视为不可变的。
SharedPrefrences类提供了很强的一致性保证。所以对它的频繁操作,可能会降低应用程序的速度。经常变化的属性或者可以容忍损失的属性不妨选用其他的存储方式。另外,这个类不支持跨多个进程使用。
SharedPrefrences提供了一系列对Prefrences数据读写的操作,SharedPrefrences中直接提供了读取Prefrences的方法:getAll、getString、getInt、getBoolean
等。而对于Prefrences数据的写入的相关操作:putString、remove、commit、apply
在SharedPrefrences的内部接口Editor中。
2,SharedPreferences的创建
在Context
中是通过getSharedPreferences
方法获取实例的。ContextImpl
是Content
的通用API实现类,为Activity
和其他应用组件提供基础的上下文,同时也实现了getSharedPreferences
方法:
2.1 ContextImpl#getSharedPreferences
public SharedPreferences getSharedPreferences(String name, int mode) {
// 至少有一个应用实际上传递了null名称,这是被允许的,
// null将会被字符串化并生成一个名为“null.xml”的文件。
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);
}
其中mSharedPrefsPaths
是一个 ArrayMap<String, File>
对象,存储从首选项名称映射到生成的路径,相当于一个临时缓存,对象也只在这个方法里被使用过。getSharedPreferencesPath(name)
返回DataDir
文件夹下"shared_prefs"
路径中名为name.xml
文件
即:/data/data/包名/shared_prefs/指定的文件名.xml
再看getSharedPreferences(File file, int mode)
:
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
// sSharedPrefsCache是一个ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>>对象
// 存储从包名映射到首选项名,再映射到缓存的首选项,是一个临时缓存。
// getSharedPreferencesCacheLocked就是从sSharedPrefsCache中get数据。
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
// 省略部分安全性校验代码
...
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) {
// 如果多进程模式下,其他进程在后台更改了prefs文件,系统会重新加载。
// 这是3.0之前的一个操作,该模式已被标记为过时,不建议使用这个模式了,
// 并且谷歌没有对多进程操作做任何支持,官方的建议是去使用ContentProvider。
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
2.2 SharedPreferencesImpl
从上面代码可以看出,Preference对象是优先从sSharedPrefsCache
中取的,取不到才会去创建。sSharedPrefsCache
是一个静态变量,以此来保证所有客户端共享类的实例;所有Prefrence相关的类成员变量都是线程同步的。SharedPreferencesImpl是SharedPrefrences的实现类。
SharedPreferencesImpl(File file, int mode) {
mFile = file;
// 在file文件同目录下生辰有一个“.bak”的备份文件。
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
// 从磁盘上读取数据到mMap中,
startLoadFromDisk();
}
文件读取(I/O)是一个耗时操作,startLoadFromDisk
开启一个新的线程, 并在新线程中调用loadFromDisk
获取文件中所有数据的映射对象(mMap
)。mLoaded
是线程唤醒标识,用来防止SharedPrefrences还未从文件中读取成功时就调用了get方法。文件开始读取时,mLoaded==false
在loadFromDisk
方法中;当文件读取完毕,mLoaded
会被置为true
。在mLoaded==false
时所有的get
操作在线程中都将处于挂起等待状态,直到mLoaded==true
即文件读取完毕,读取操作才会被唤醒(mLock.notifyAll
)。
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
loadFromDisk:
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
// 文件已经加载,无需再次加载
return;
}
// 若备份文件存在,用备份文件替换mFile
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
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) {
// ErrnoException表示映射失败,thrown 是null
} catch (Throwable t) {
thrown = t;
}
synchronized (mLock) {
// 文件读取完毕,将mLoaded 置为true
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<>();
}
}
// thrown 是空值, mMap维持原有值不变, 同时还允许任何修改操作。
} catch (Throwable t) {
mThrowable = t;
} finally {
// 唤醒线程
mLock.notifyAll();
}
}
}
3,SharedPreferences的读取
在SharedPreferences的创建过程中,已经完成了由文件到Map的转变,此时数据已经被全部加载到内存中,并保存在mMap
对象中,读取操作无非就是从mMap
中取值,以String的读取为例:
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
awaitLoadedLocked 是做线程同步的,以防止文件尚未加载完毕就调用了get方法获取数据。
private void awaitLoadedLocked() {
if (!mLoaded) {
// BlockGuard是一个StrictMode对象,以严格模式控制保证
// 从磁盘上读取文件的操作必须在非UI线程上执行
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
StrictMode 在做性能优化时是一个很好用的工具类,需要了解StrictMode可以点击 这里 或 这里。
4,SharedPreferences的写入
SharedPreferences
接口直接提供了数据读取的功能,而相对应的写入操作则在其内部接口Editor
中定义。EditorImpl是Editor的实现类,通过SharedPreferences的实例对象的edit()
获取EditorImpl的实例。
4.1 EditorImpl
以putString为例:
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
// mModified 是一个 Map<String, Object> 类型的EditorImpl类全局变量
// 用以存储修改后的Preference键值对。
mModified.put(key, value);
return this;
}
}
remove()、clear()
操作不同于put
。
remove(key)
方法做了一个操作:mModified.put(key, this)
clear()
方法会将全局变量mClear
置为 true:mClear = true
所有的put和remove操作只是将修改后的数据临时存储于一个map(mModified
)中,而让修改生效则必须调用 apply()
或commit()
。
apply():
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);
notifyListeners(mcr);
}
commit():
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 {
... ...
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
apply()
和commit()
方法都做了两个操作:
- 将需要提交到文件系统的数据通过
commitToMemory()
方法封装成 MemoryCommitResult 对象。 - 调用
enqueueDiskWrite()
方法将提交动作加入到写操作的队列中,以顺序执行。
不同之处在于apply单独创建线程用于操作;而commit直接阻塞当前线程进行操作,直到执行完毕。
commitToMemory():
mapToWriteToDisk = mMap;
// 这是一个记录调用提交次数的计数器,每提交一个+1,每写入一次-1。
mDiskWritesInFlight++;
... ...
if (mClear) {
// 在调用clear()方法时,会将mClear置为true,mapToWriteToDisk 直接清空
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
// mModified是put和remove操作修改后的数据的一个临时存储区
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// 前面说过,remove(key)方法实际上也就是mModified.put(key, this)
// 所以,如果mModified中包含value==this的元素就移除mapToWriteToDisk中的该键值对
if (v == this || v == null) {
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
// 如果mapToWriteToDisk中存在与mModified中当前元素相同的元素,直接跳过,进行下一循环
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
enqueueDiskWrite(MemoryCommitResult, Runnable):
这个方法的作用是 将提交动作加入到写操作的队列中,然后按照他们加入队列中的顺序逐一执行。
当apply()
调用时,由于它是异步的,所以会传入一个Runnable,而commit()
传入的则是null.
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
// Runnable 为空,则认为是同步的提交操作
final boolean isFromSyncCommit = (postWriteRunnable == null);
// 写入文件的工作任务
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
// 这是一个记录调用提交次数的计数器,每提交一个+1,每写入一次-1。
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
// 如果是同步提交,且当前只有一次尚未执行的提交操作,直接在当前线程执行提交任务
writeToDiskRunnable.run();
return;
}
}
// 如果是异步提交,或当前有大于一次尚未执行的提交操作,将当前操作加入队列
// 并根据是否是同步提交来决定是否延迟执行writeToDiskRunnable。
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
4.2 MemoryCommitResult
前面提到apply()
和commit()
方法都引入了一个类 :MemoryCommitResult,这个类的封装很简单:
// Return value from EditorImpl#commitToMemory()
private static class MemoryCommitResult {
final long memoryStateGeneration;
// 修改过的key列表
@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, @Nullable List<String> keysModified,
@Nullable Set<OnSharedPreferenceChangeListener> listeners,
Map<String, Object> mapToWriteToDisk) {
this.memoryStateGeneration = memoryStateGeneration;
this.keysModified = keysModified;
this.listeners = listeners;
this.mapToWriteToDisk = mapToWriteToDisk;
}
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}
其中, CountDownLatch 是通过一个计数器来实现的线程等待工具类。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
4.3 写入文件 — writeToFile
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 (fileExists) {
// 在创建SharedPrefrenceImpl对象时
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 (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();
}
}
// 尝试以尽可能原子的方式写入文件、删除备份并返回true。
// 如果出现异常,删除新文件;下次将从备份中恢复。
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();
}
// 写入成功,如果有备份文件就删除
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);
}