最近项目验收遇到了SharedPreferences
(如下简称SP
)有关的问题,因而就去网上搜,知道了大概,这里作一次源码解析,加深理解。众所周知,SP
是轻量级持久化工具,把键值对写成xml文件保存在data/data/packagename/shared_prefs路径下,注意SP
这个类并不支持跨进程使用!下面来看一下如何获取SP
:java
//Context.java
//name是xml的文件名,mode是操做模式
public abstract SharedPreferences getSharedPreferences(String name, int mode);
复制代码
mode有如下几种:android
//SharedPreferences.java
public interface SharedPreferences {
/**
* 用于修改SharedPreferences值的接口,要修改sp里的值必须经过Editor对象
*/
public interface Editor {
/**
* 保存一个键值对
*/
Editor putString(String key, @Nullable String value);
/**
* 根据key移除键值对
*/
Editor remove(String key);
/**
* 清除sp文件里的内容
*/
Editor clear();
/**
* 同步提交修改数据
*/
boolean commit();
/**
* 异步提交修改数据
*/
void apply();
}
/**
* 获取数据
*/
@Nullable
String getString(String key, @Nullable String defValue);
/**
* 获取编辑器
*/
Editor edit();
}
复制代码
上面注释简单的介绍了SP
的方法,固然还有其余类型数据的get
和put
方法没有列举,这里只介绍一种读取和写入的方法。Context
的实现类是android.app.ContextImpl
,看一下getSharedPreferences
的实现:缓存
//ContextImpl.java
@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);
//保存到ArrayMap中
mSharedPrefsPaths.put(name, file);
}
}
//根据文件获取sp对象
return getSharedPreferences(file, mode);
}
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
//sp实现类
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//getSharedPreferencesCacheLocked根据包名获取ArrayMap<File, SharedPreferencesImpl>
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
//检查传入的mode,若是是MODE_WORLD_READABLE或MODE_WORLD_WRITEABLE将抛SecurityException
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,并保存到缓存中
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) {
// 若是是多进程模式须要从新读取文件
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
复制代码
从代码能够看到SP
的实现类是SharedPreferencesImpl
,先看一下构造方法:bash
//SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
mFile = file;
//备份file
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
//开启一个名为SharedPreferencesImpl-load的线程从磁盘读取数据
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
synchronized (mLock) {
//是否读取过
if (mLoaded) {
return;
}
//若是备份文件存在,删除mFile,将备份文件重命名给mFile
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
Map map = null;
StructStat stat = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
//将xml文件转成map
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) {
//表示已经读取过,下次调用getSharedPreferences不会再从磁盘读取
mLoaded = true;
if (map != null) {
//赋值给成员变量mMap
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
//释放锁
mLock.notifyAll();
}
}
复制代码
整个流程的代码看起来都比较简单,这里也就不过多的阐述了,这里我有个疑问为何要备份文件呢?且往下看。app
SharedPreferencesImpl
构造方法中主要是开启一个子线程将xml文件读取到内存中(转成
map
)。
//SharedPreferencesImpl.java
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
//阻塞等待sp将xml读取到内存后再get
awaitLoadedLocked();
String v = (String)mMap.get(key);
//若是value为空返回默认值
return v != null ? v : defValue;
}
}
private void awaitLoadedLocked() {
...
// sp读取完成后会把mLoaded设置为true
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
复制代码
这里主要说一下awaitLoadedLocked()
这个操做会阻塞主线程,因此说SP
是轻量级持久化方式,若是文件过大阻塞时间就会变长,由于将xml文件读取完成后才会释放锁mLock.notifyAll()
;异步
还记得以前说过要想修改值,必须经过Editor
对象,SP
获取Editor
:编辑器
//SharedPreferencesImpl.java
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
//每次都会返回一个新的Editor
return new EditorImpl();
}
复制代码
再看putString
,remove
,clear
:ide
//SharedPreferencesImpl.EditorImpl.java
public Editor putString(String key, @Nullable String value) {
synchronized (mLock) {
//将键值对写入mModified
mModified.put(key, value);
return this;
}
}
public Editor remove(String key) {
synchronized (mLock) {
//移除操做很鸡贼,后面会根据value是不是this来判断是put仍是remove
mModified.put(key, this);
return this;
}
}
public Editor clear() {
synchronized (mLock) {
//后面会根据这个判断是否clear
mClear = true;
return this;
}
}
复制代码
这里能够看到修改值只会将其存到mModified
的map
中去,因此在编辑器中所作的全部更改都会批处理,直到咱们调用commit
或apply
才会设置到mMap
和xml文件中去,这里先说个结论当咱们调用putxxx
和remove
或clear
时,不管谁在前谁在后都会先执行remove
或clear
,接下来就该分析commit
和apply
了。工具
这里先说个结论当咱们调用putxxx
和remove
或clear
时,不管谁在前谁在后都会先执行remove
或clear
,具体说明看后面代码。post
//SharedPreferencesImpl.EditorImpl.java
public boolean commit() {
//提交到内存中,并返回一个MemoryCommitResult对象,具体看3.5
MemoryCommitResult mcr = commitToMemory();
//放入写入队列,具体看3.6
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
try {
//线程等待,直到写入文件操做后
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
//通知监听
notifyListeners(mcr);
//返回提交结果
return mcr.writeToDiskResult;
}
复制代码
//SharedPreferencesImpl.EditorImpl.java
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
// 这个时候说明正在写入文件,没法修改,写入文件以后会重置为0,具体看3.6
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 (mLock) {
//是否发生改变
boolean changesMade = false;
//调用了clear方法的操做,也验证了前面的结论,会先执行clear操做
if (mClear) {
if (!mMap.isEmpty()) {
changesMade = true;
mMap.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// 还记得前面remove方法里面存放this就是为了判断remove的,
// 也验证了前面的结论,其次会执行remove
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
//未发生修改跳过
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
//发生修改或者修改存入map
mMap.put(k, v);
}
// 发生改变
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
//将修改的map置空
mModified.clear();
if (changesMade) {
// 当前提交到内存的次数
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
//将提交内存次数,发生修改的集合,监听,整个map封装成MemoryCommitResult
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
复制代码
//SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
// commit操做为true,apply为false
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
// 写入文件
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
// 写入完成后减1,和3.5节相照应来确保写入状态是否完成
mDiskWritesInFlight--;
}
//apply操做才会执行,具体看3.7节
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// 前面也说了commit操做时才为true
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
// 和3.5节的这行代码mDiskWritesInFlight++相照应
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
// 执行写入文件
writeToDiskRunnable.run();
return;
}
}
//apply操做才会执行,具体看3.8节
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
复制代码
到这里也就看出来了commit
是同步的。writeToFile
方法就不贴出来了,简单说说它的做用:
key
发生改变,则直接返回;不然执行step2;mMap
所有信息写入文件,若是写入成功则删除备份文件,若是写入失败则删除mFile
,同时也就解答了以前为何要备份文件的问题;mcr.setDiskWriteResult
设置写入文件是否成功,并让线程结束等待状态。//SharedPreferencesImpl.EditorImpl.java
public void apply() {
// 3.5节已讲过
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
// 线程等待
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
// 将awaitCommit加入QueuedWork
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 放入写队列
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// 通知监听
notifyListeners(mcr);
}
复制代码
//QueuedWork.java
// apply才会执行这个方法,而且第二个参数为true
public static void queue(Runnable work, boolean shouldDelay) {
// HandlerThread的handler
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
复制代码
这个方法主要做用是延迟执行Runnable
,从这也就看出来了apply
是异步操做。结合3.6,3.7,3.8总结一下apply
,否则看起来有点乱。
SP
文件不宜过大,若是SP
文件须要存储的内容过多,能够根据不一样的功能划分红多个文件;getSharedPreferences
,这样在调用put
和get
操做时,文件已经被读取到内存中了;edit()
, 应该调用一次edit()
,由于每次调用edit()
都会新建一个Editor
;commit()
或apply()
,若是屡次存入值,应该在最后一次调用。