一.描述
最近项目验收遇到了SharedPreferences
(以下简称SP
)有关的问题,于是就去网上搜,知道了大概,这里做一次源码解析,加深理解。众所周知,SP
是轻量级持久化工具,把键值对写成xml文件保存在data/data/packagename/shared_prefs路径下,注意SP
这个类并不支持跨进程使用!下面来看一下如何获取SP
:
//Context.java
//name是xml的文件名,mode是操作模式
public abstract SharedPreferences getSharedPreferences(String name, int mode);
mode有以下几种:
- MODE_PRIVATE 默认模式,创建的文件只能在应用内访问(或者共享相同userID的所有应用)
- MODE_WORLD_READABLE(过时)允许其他应用访问本应用的文件,使用此模式会抛出异常
- MODE_WORLD_WRITEABLE(过时)允许其他应用写本应用的文件,使用此模式会抛出异常
- MODE_MULTI_PROCESS(过时)官方提示这种模式在某些版本无法可靠运行,并且未来也不会支持多进程
二.结构
//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();
}
三.源码解析
3.1 getSharedPreferences方法
上面注释简单的介绍了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
,先看一下构造方法:
//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();
}
}
整个流程的代码看起来都比较简单,这里也就不过多的阐述了,这里我有个疑问为什么要备份文件呢?且往下看。
- 总结:
SharedPreferencesImpl
构造方法中主要是开启一个子线程将xml文件读取到内存中(转成map
)。
3.2 getString
//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()
;
3.3 Editor
还记得之前说过要想修改值,必须通过Editor
对象,SP
获取Editor
:
//SharedPreferencesImpl.java
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
//每次都会返回一个新的Editor
return new EditorImpl();
}
再看putString
,remove
,clear
:
//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
了。
3.4 commit
这里先说个结论当我们调用putxxx
和remove
或clear
时,无论谁在前谁在后都会先执行remove
或clear
,具体说明看后面代码。
//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;
}
3.5 commitToMemory
//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);
}
3.6 enqueueDiskWrite
//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
设置写入文件是否成功,并让线程结束等待状态。
3.7 apply
//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);
}
3.8 queue
//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()
,如果多次存入值,应该在最后一次调用。