SharedPrefrences是开发中常用的类,作用是持久化本地的一些基础数据,使用简单易封装的特性,相比数据库、内容提供者更为实用。
SharedPreferences的本质是一个文件操作的接口类。先看下源码的注释部分:
大致的传达的意思是:
Editor
是修改的操作类对象接着上面的说,竟然是一个文件操作的类,必然会涉及到本地的文件操作,牵扯到IO流和文件。那具体操作的文件是啥了?
SharedPreferences
是一个接口,而他的具体实现类是SharedPreferencesImpl
,在writeToFile
方法中就用了XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str)
去实现将数据写入xml文件中。一般保存在/data/data//shared_prefs下,需要root权限。
所以,SharedPreferences实际就是一个xml文件和IO操作的集合。
如果重复对文件进行操作,会是耗时操作。所以SharedPreferences是有缓存机制的,将磁盘内容读取到内存中,然后直接对内存进行操作,实现缓存。怎样实现缓存机制的了?
@Override
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保证线程安全
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
//拿到第一个缓存 string,file的map
file = mSharedPrefsPaths.get(name);
if (file == null) {
//如果当前name的为空,新建一个命名为/data/data//shared_prefs下的.xml文件存放入mSharedPrefsPaths中
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
//根据file拿第二个缓存
return getSharedPreferences(file, mode);
}
我们每次调用context.getSharedPreferences(String name,int mode)
会通过name创建一个对应的file,然后会相应的有IO流的操作,为了避免重复多次进行getSharedPreferences创建,导致文件流操作过多,这里创建了个map的cache[name,file]缓存,使用了synchronized保证了线程安全,并且保证只存在一个这样的缓存。上面拿到了对应的file后调用getSharedPreferences(File file, int mode)
拿到最终的sp.
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
//同上面一样,保证线程安全
synchronized (ContextImpl.class) {
//getSharedPreferencesCacheLocked就是拿到对应包名的map缓存,看下面源代码,会发现跟上面的大同小异步骤
final ArrayMap cache = getSharedPreferencesCacheLocked();
//拿到file对应的sp
sp = cache.get(file);
if (sp == null) {
//从android N 开始,系统不在支持APP访问另一个APP的sp,也就是不在支持MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE
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 getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
//获取包名对应的缓存ArrayMap>
//所以sSharedPrefsCache跟包名对应 每一应用默认会分配一个进程,每个进程维护唯一份sSharedPrefsCache,并且由ContextImpl.class的锁保护
ArrayMap packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
第二个缓存cache是将file映射到sharedPreferences的关键,其中重要部分就是getSharedPreferencesCacheLocked
拿到的ArrayMap
。具体的流程图如下:
上面看到缓存机制中,当sp==null时,会去通过SharedPreferencesImpl的构造函数去创建sp
SharedPreferencesImpl(File file, int mode) {
//name映射的file,即sp对应的文件对象,所有的K-V值都存放与次
mFile = file;
//容灾文件的file 后缀为.bak
mBackupFile = makeBackupFile(file);
//创建模式
mMode = mode;
//文件的读取完成进度boolean值
mLoaded = false;
//sp中所有K-V数据,从xml文件获取到的
mMap = null;
//开启线程异步加载文件的内容
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
//开启线程去加载
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
//mLoaded是读取完成的标志
synchronized (mLock) {
//如果加载完成,直接返回
if (mLoaded) {
return;
}
//如果灾备文件存在,则直接使用灾被文件,将mFile删除,并修改灾备为新的name映射的file。
//可以理解成回滚
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 map = null;
StructStat stat = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
//创建io流
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
//读取xml内容,并将转换成map的k-v数据
//深入了解readThisValueXml()方法,你可以了解到sp支持的数据类型和解析方法
map = XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
/* ignore */
}
synchronized (mLock) {
//保证线程安全,加载完成mLoaded置为true
mLoaded = true;
if (map != null) {
//这个map就是sp的k-v值
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
//释放锁
mLock.notifyAll();
}
}
前面得到的mMap对象了,取值直接调用mMap.get(key)
就行了。需要注意的是每次的读取操作需要根据上面提到的mLoaded字段去判断文件的内容是否加载到内存里面了。源码如下
public Map getAll() {
//保证线程安全
synchronized (mLock) {
//判断是否加载到内存
awaitLoadedLocked();
//noinspection unchecked
return new HashMap(mMap);
}
}
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
上面就是熟悉的get**
操作,最主要的要看下awaitLoadedLocked();
方法怎么判断的
private void awaitLoadedLocked() {
if (!mLoaded) {
//去监控磁盘的读取操作
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
//锁等待
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
由代码可知:如果文件加载还未完成,则mLoaded为false,则方法get**
就会锁等待;如果加载完成会调用mLock.notifyAll()
并且mLoaded会被置为true,锁解决掉,直接从缓存获取到值。由于直接从缓存中取值,除去第一次的新建操作外,大部分取值情况不用等待。
值的修改是需要Editor
对象去操作的,里面包含了所以的put**
接口方法,跟SharedPreferences
一样是一个抽象接口类,最终的实现是由EditorImpl
去完成的。先说明一点的是:Editor的每次操作都是先修改内存中的数据,最终写入磁盘的操作要通过commit()
或 apply()
同步或者异步去写入与磁盘的。
public Editor putString(String key, @Nullable String value) {
//保证线程安全
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
这里的mModified不是mMap,设置值的操作也不是直接对SharedPrefrences的mMap进行处理。而是操作mModified,在提交磁盘时会与mMap进行合并,生成新的mMap,然后再写入磁盘的。相应的remove(String key)
和 clear()
操作也并非真正的执行了移除和清空操作。
public Editor remove(String key) {
synchronized (mLock) {
//记住这个this,后面提交时会判断这个this,其实设置为null就行了
mModified.put(key, this);
return this;
}
}
public Editor clear() {
synchronized (mLock) {
//设置标志位,并没有实际清空
mClear = true;
return this;
}
}
所以,一般情况下,只有第一次创建SharedPrefrences是一定的开销的,后面的读取值操作开销很小,是在内存上操作的。但是写入或者修改值操作,在没有提交情况下也是基本没有开销的(也没有起到修改新增效果),所以推荐尽量批量设置值后一次性提交commit()或者apply(),来减少开销,毕竟一次次修改提交会有大量的io流操作。
前面提到的sp的数据的新增或者修改需要提交操作,但是提交的方法有commit()
和apply()
两种提交方式。主要的区别就是同步和异步的区别
commit是直接写入磁盘的,同步进行的操作,会返回一个写入成功的结果值。相比apply优势就是,能得到准确的返回结果,对一些重要的值操作,得知写入结果后去做一些其他处理或者对失败情况做一些补救工作。源码如下
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
//commitToMemory就是merg操作,将mModified和mMap进行比较
MemoryCommitResult mcr = commitToMemory();
//执行写操作 传入mcr对象,再写入操作完成后,释放mcr的锁
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;
}
无论commit()还是apply()都会先去merge下,`commitToMemory()`都要去新旧比较更新出最终的值。源码如下:
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List keysModified = null;
Set listeners = null;
Map 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.
//mDiskWritesInFlight当前的操作磁盘的线程数
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(mMap);
}
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList();
listeners = new HashSet(mListeners.keySet());
}
synchronized (mLock) {
//合并是否发生改变的标志位
boolean changesMade = false;
//清空map的标志位
if (mClear) {
if (!mMap.isEmpty()) {
changesMade = true;
mMap.clear();
}
mClear = false;
}
//mModified的遍历
for (Map.Entry 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.
//remove的标志位
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
//如果包含该K值,替换新的Value值
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
//不包含,新增
mMap.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
//清空暂存的mModified
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
//创建MemoryCommitResult
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
ps-源码并不是所有的都要看
里面定义了一个计数器闭锁,计数为1,它会等待一个线程结束后解锁,而这个线程干的事情就是写入磁盘的操作。
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
private static class MemoryCommitResult {
final long memoryStateGeneration;
@Nullable final List keysModified;
@Nullable final Set listeners;
final Map mapToWriteToDisk;
//创建的计数器闭锁
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
@GuardedBy("mWritingToDiskLock")
//volatile关键字修饰,保证writeToDiskResult的可见性、有序性 保证commit返回的结果的准确性
volatile boolean writeToDiskResult = false;
boolean wasWritten = false;
private MemoryCommitResult(long memoryStateGeneration, @Nullable List keysModified,
@Nullable Set listeners,
Map mapToWriteToDisk) {
this.memoryStateGeneration = memoryStateGeneration;
this.keysModified = keysModified;
this.listeners = listeners;
this.mapToWriteToDisk = mapToWriteToDisk;
}
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
//写入磁盘成功是否的结果
writeToDiskResult = result;
//计数减1 ,这里计数数量为1,即为解锁。计数结束,所有线程并行
writtenToDiskLatch.countDown();
}
}
commit操作实际不一定都是在主线程里面执行的,但是利用了闭锁能保证返回正确的结果,即使不在同一线程,commit的执行也能其他线程执行时不影响写入顺序。(假设操作是在主线程,如果有大量的commit操作,也会导致主线程卡顿,闭锁)
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//isFromSyncCommit 是否 是同步提交 apply的参数中会传递一个Runnable对象,commit为null
final boolean isFromSyncCommit = (postWriteRunnable == null);
//创建一个线程 执行 写盘操作 这里还没执行 只是创建
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
//写入操作
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
//执行完毕后,减少一个线程个数
mDiskWritesInFlight--;
}
//commit不会走这里
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
//commit走这里
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
//当前只有一个线程操作磁盘,空闲状态
if (wasEmpty) {
writeToDiskRunnable.run();
//return 不在执行 QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
return;
}
}
//非空闲状态,执行writeToDiskRunnable线程 无延迟
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
//放到LinkedList中再执行run,也是LinkedList遍历顺序去执行Runnable.run
}
MemoryCommitResult中mcr闭锁等待,如果写入耗时,闭锁可能造成anr
返回MemoryCommitResult的writeToDiskResult的结果,为commit返回的写入成功是否值。
与commit()
相比,直接开启一个线程去执行写操作,不用关系操作是否成功是否;对于一些非关键变量或者不需要写入失败补救的值,最好使用apply()执行值修改。
public void apply() {
final long startTime = System.currentTimeMillis();
//同commit一样
final MemoryCommitResult mcr = commitToMemory();
//创建提交的等待线程
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
//等待解锁后才能执行下一个runnable,writeToFile会解锁
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
//添加到waitToFinish去执行 , finishers 通过这种方式实现检查排队任务是否完成。
//waitToFinish触发排队任务马上执行
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
//awaitCommit去闭锁,阻塞线程。 所以大文件的sp存储也会造成主线程的卡顿
awaitCommit.run();
//移除掉
QueuedWork.removeFinisher(awaitCommit);
}
};
//开始执行apply的enqueueDiskWrite
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
同commit一样去merge生成新的mMap
apply和commit用的是同一个方法,多的是postWriteRunnable不为null
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
// isFromSyncCommit 为false
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
//writeToFile完成后会开锁
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
//写完线程数减一
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
//下面不会执行
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//执行异步任务writeToDiskRunnable,也会执行 postWriteRunnable.run();
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
writeToFile的关键是写入操作,还有setDiskWriteResult
计数闭锁的关闭。怎么写入不需要太去了接。setDiskWriteResult(boolean wasWritten, boolean result)
是开锁操作。删除计时源码如下:
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
boolean fileExists = mFile.exists();
//重命名当前文件,以便在下次读取时将其用作备份
if (fileExists) {
boolean needsWrite = false;
// 只需要在磁盘状态比此提交更早时写入
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
//commit 是即时写入
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) {
//不需要写入,此处不是异常,可能是合并状态未更新。分析commitToMemory()里面的参数changesMade的值
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);
FileUtils.sync(str);
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);
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);
}
无论是apply还是commit方法去提交修改值操作,如果文件过大,涉及到io操作,也会出现anr情况,如果日志中出现countDownLatch.await
waitToFinish()
关键字段大概率就是存在耗时操作并且没有执行完。
4.4之前版本中,如果name为null,系统会以字符串“null”为名称,但是这不符合命名规范,为了避免这些和整理使用,建议存放同一类中集中管理,保证命名不重名。
如需要使用将对象序列化存入sp,如果原序列化对象发生改变了,那么serialVersionUID可能发生改变了,就不是同一个类了。虽然可以手动指定serialVersionUID去保证,但是还是建议不要序列化大的的数据类型到sp里面。