简单使用
SharedPreferences sharedPreferences = getSharedPreferences("ouwen", Context.MODE_PRIVATE);
SharedPreferences.Editor edit = sharedPreferences.edit();
edit.putString("ouwen","123456");
保存数据:edit.apply(); 或者 edit.commit()
读取数据:sharedPreferences.getString("ouwen","");
通常查看源码都是带着问题去分析, 避免在源码里面迷失了,
那么关于SharedPreferences的几点问题:
- 怎么保存数据的?
- 怎么读取数据的?
- 它是线程安全的吗?
- apply和commit都可以提交数据,那么有什么区别?
- 有什么需要注意的吗?要怎么优化?
怎么保存数据的?
先说结论再看源码分析
本质是把Map中的数据转成xml文件, 以键值对的方式存储在本地
- 第一步: 获取SharedPreferences实例
- 第二步: 创建一个Map把我们当前修改的数据存储在内存中
- 第三步: 调用commit()或者apply() 把内存中的数据存储到本地
getSharedPreferences()源码
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
mBase: 是ContextImpl
传入一个name和mode, 首先会尝试从mSharedPrefsPaths中获取name在本地的File, 如果为null, 就调用getSharedPreferencesPath(name) 创建一个name +".xml"的文件并且加入mSharedPrefsPaths缓存中, 继续调用getSharedPreferences(file,mode)
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
checkMode(mode);
...
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
...
return sp;
}
为了简洁容易看, 删除了部分代码
- 先调用getSharedPreferencesCacheLocked()获取File对应的缓存cache, 这个ArrayMap是ConextImpl里的一个静态成员属性
- 第一次缓存是null, 所以会调用new SharedPreferencesImpl(file, mode) 创建一个实例加入缓存中并且返回
然后调用edit()方法获取Editor的实例EditorImpl
@Override
public Editor edit() {
...
synchronized (mLock) {
awaitLoadedLocked(); 这里会阻塞等待,直到读取文件完成
}
return new EditorImpl();
}
public final class EditorImpl implements Editor {
...
private final Map mModified = new HashMap<>();
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
@Override
public Editor putInt(String key, int value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
...
}
拿到SharedPreferences.Editor后就可以往里面putString(), putInt 等等...
哦~~~原来我们put的数据都是保存在mModified
这个Map集合里面了,
这个时候数据还是在内存里面, 只有调用commit()或者apply()才会保存到本地文件, 那就继续看源码吧
先对比一下commit() 和 apply() 源码上有什么区别?
public boolean commit() {
...
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
...
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
public void apply() {
...
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await(); 等待提交完成
} catch (InterruptedException ignored) {
}
}
};
//把这个等待提交的Runnable, 加入到QueuedWork中
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run(); 执行上面的Runnable
//从QueuedWork中移出等待提交的Runnable
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
相同点:
- 都会调用commitToMemory(), 然后调用enqueueDiskWrite()把Map的数据写入本地文件中
不同点:
- commit() 有返回值, 可以知道成功还是失败
- apply() 没有返回值, 并且多了2个Runnable
commitToMemory()方法会遍历之前put数据的mModified
这个Map, 把我们修改的数据同步到另外一个Map中, 并且清空mModified自己
enqueueDiskWrite()
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
commit()方法调用enqueueDiskWrite的时候, 这个postWriteRunnable为空
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
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;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
创建一个writeToDiskRunnable里面会执行当writeToFile()把数据写入文件
调用commit的时候, isFromSyncCommit = true, 所以会当前线程执行
调用apply的时候会把写入文件的Runnable加入QueuedWork.queue()中执行, 其实是把Runnable发送到HandlerThread的子线程中执行
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
private static Handler getHandler() {
synchronized (sLock) {
if (sHandler == null) {
HandlerThread handlerThread = new HandlerThread("queued-work-looper",
Process.THREAD_PRIORITY_FOREGROUND);
handlerThread.start();
sHandler = new QueuedWorkHandler(handlerThread.getLooper());
}
return sHandler;
}
}
怎么读取数据的?
我们的程序启动后,当在某个地方想从SharedPreferences 里面获取数据的时候
会调用下面代码:
SharedPreferences sharedPreferences = getSharedPreferences("name", Context.MODE_PRIVATE);
sharedPreferences.getString(key,"");
- 获取SharedPreferences实例
- 调用getString(key,"") 方法获取数据
第一次调用的时候, 会调用new SharedPreferencesImpl()来创建SharedPreferences 的实例, 也就是在SharedPreferencesImpl的构造方法里, 会从本地的xml文件加载数据到内存
SharedPreferencesImpl(File file, int mode) {
...
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
...
}
try {
...
BufferedInputStream str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map) XmlUtils.readMapXml(str);
} catch (Throwable t) {
thrown = t;
}
synchronized (mLock) {
mLoaded = true;
try {
if (thrown == null) {
if (map != null) {
mMap = map;
} else {
mMap = new HashMap<>();
}
}
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll(); 加载完成, 通知正在等待的代码继续执行
}
}
}
startLoadFromDisk()方法会启动一个线程, 然后异步调用loadFromDisk()把xml文件的内容加载到内存Map中
思考: new Thread? 为啥不用线程池呢?
其实不是所有需要创建线程都用线程池来实现, 当我们确认了调用的次数很少的时候,直接new Thread也是可以的
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked(); 阻塞等待加载文件完成
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
思考: 读取本地文件是异步的, 那么如果保证数据正确?
在getString()方法中可以看到, 先会调用awaitLoadedLocked(), 再从mMap中根据key获取value. 这个awaitLoadedLocked()会判断当前文件是否下载完成, 如果没有完成就阻塞等待; 上面异步加载文件的loadFromDisk()方法, 在加载完成后会调用mLock.notifyAll()通知这里继续执行
private void awaitLoadedLocked() {
...
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
有什么需要注意的吗? 要怎么优化?
SP表示SharedPreferences
1.apply()也有可能导致卡顿甚至ANR
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await(); 等待提交完成
} catch (InterruptedException ignored) {
}
}
};
//把这个等待提交的Runnable, 加入到QueuedWork中
QueuedWork.addFinisher(awaitCommit);
apply()虽然在子线程写入文件, 但是在提交的时候会把awaitCommit加入到QueuedWork中, 并且在ActivityThread.handlePauseActivity(), ActivityThread.handleStopActivity(), ActivityThread.handleStopService(), ActivityThread.handleSleeping() 等等多处地方都会调用QueuedWork.waitToFinish()去阻塞等待SP保存数据完成, 也就是说如果apply()执行写入文件, Activity会等待它执行完成才能关闭页面...
更多关于 apply() 的问题, 可以看一下字节跳动技术团队的这篇文章
2.不要保存比较大的内容, 会导致卡顿
因为SP加载的key和value以及put的内容, 会一直保存在内存当中,占有内存
第一次从SP中获取数据的时候, 如果文件过大可能会导致卡顿,虽然读取文件是在子线程中执行, 但是看getString的源码知道, 会调用awaitLoadedLocked阻塞在当前线程等读取文件完成
优化: 可以提前在子线程先初始化SP, 过一段时间再去getString()获取数据
3.不相关的配置, 不要保存在同一个文件中
因为文件越大加载越慢, 从而导致保存在内存的数据越多, 如果有的配置用的比较频繁,有的又很少用到,那分开文件存储可以提高一点性能(文件比较小可以忽略)
4.避免频繁提交, 尽量批量修改一起提交
因为写入本地文件的时候会加锁, 并且可能阻塞线程, 所以要避免频繁提交,提交使用apply()
5.最好不要用来跨进程使用
SP无法保证跨进程使用时数据正常
如果项目对性能要求比较高, 或者想要跨进程使用
可以看下微信的MMKV, 使用也很简单, 听说性能高,稳定性强(暂时没用过= =)
微信MMKV项目地址
小结:
怎么保存数据的?
修改的数据会存储内存Map中,当调用commit或者apply,就把数据写入本地文件怎么读取数据的?
第一次会创建子线程从本地文件读取到内存Map中, 所以第一次稍微慢一点, 后面的都在内存操作所以比较快它是线程安全的吗?
是线程安全的, 因为所有的读和写操作都加了synchronized,保证线程安全apply和commit都可以提交数据,有什么区别?
commit有返回值,可以马上知道提交结果, 但是commit是在当前线程执行写入文件操作的
apply没有返回值, 是在HandlerThread的子线程中写入到文件