目录
(1)构造函数:实例化成员变量,读出xml文件的key/value
(2)存放数据:通过Editor来将修改的内容暂时存放到mModified中
前言
读源码真是一件开心的事情,可以从最根本的源头发现一些原理。开心
SharedPreferences创建
通过Context的getSharedPreferences获得SharedPreferences对象,跟踪代码最终调用了ContextImpl的getSharedPreferences
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);
//Android O 访问内部存储空间默认是锁定状态,要做权限检查
if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
if (isCredentialProtectedStorage()
&& !getSystemService(StorageManager.class).isUserKeyUnlocked(
UserHandle.myUserId())
&& !isBuggy()) {
throw new IllegalStateException("SharedPreferences in credential encrypted "
+ "storage are not available until after user is unlocked");
}
}
//创建或者从缓存中读取SharedPreferencesImpl实例
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
//。。。。省略代码
return sp;
}
//从缓存中读取SharedPreferencesImpl实例
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
可以先从缓存集合sSharedPrefsCache中查看是否有对应包名下的SharedPreferences,如果没有的话,则新建。注意这里是有一个同步锁,所以SharedPreferences是有同步锁,不支持并发。
重点看下SharedPreferencesImpl里面的实现
SharedPreferencesImpl实现
(1)构造函数:实例化成员变量,读出xml文件的key/value
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
初始化变量,然后通过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;
}
//。。。省略代码
//从xml文件读出所有的内容放到内存中
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);
map = XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
/* ignore */
}
//将读取的内容赋值给mMap
synchronized (mLock) {
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
mLock.notifyAll();
}
}
从这里可以看到,SharedPreferences在初始化的时候,就会将xml中所有的内容都读到内存mMap中,这样如果在存放key和value的时候,如果很大的话,会占用大量内存,同样有同步锁不支持并发。
另外mLoaded这个变量用来标记是否加载完SharedPreferences里面所有的内容。startLoadFromDisk()开始的时候,将该变量置为false,开始读取文件。当读取文件结束之后,会将mLoaded为true。该过程操作为子线程。而在获取Editor的时候,会根据该mLoaded是否变为true来决定是edit()方法所在的主线程是否可以结束wait。
mLock是同步锁,用来保证一个线程可以访问该代码块。我们可以看到这个synchronized (mLock)贯穿到SharedPreferencesImpl的各个方法中。
通过上面几个方法调用之后,我们就初始化了SharedPreferences,同时将SharedPreferences里面的所有的内容读取到内存中,注意mLock是同步锁来保证每次只能有一个线程来访问该方法。
(2)存放数据:通过Editor来将修改的内容暂时存放到mModified中
我们在往SharedPreferences的存放数据的时候,通常先获取一个Editor,然后通过Editor来存储数据
Editor editor = sharedPreferences.edit();
editor.putString(key, value);
主要经过下面的几个步骤
- 1)获得Editor
首先看下SharedPreferencesImpl中获取的Editor的edit()
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
//等待构造方法里面的xml内容完全读取到内存中
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
mLock是同步锁来保证每次只有一个线程可以访问edit()。进入到awaitLoadedLocked()可以看到这里会一直等待mLoaded这个变量变为true(即将所有xml的内容读取完毕)有一个线程等待的过程,也就是等着将所有xml的内容读取完毕完之后才会创建Editor。所以这就意味着我们在读取SharedPreferences的时间如果太长,超过5s的时候就阻塞主线程,引起ANR。这也就提醒我们在使用SharedPreferences的时候,一定不要在里面放很多的数据。我们每次获取的Editor都是一个新的对象。
- 2)添加数据
那么进入到EditorImpl中查看putString()
public Editor putString(String key, @Nullable String value) {
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
mModified就是一个Map集合,用来记录这次修改的key/value。同样mLock是同步锁来保证每次只有一个线程可以访问putString()。
- 3)修改数据
再看下clear()、remove()
public Editor remove(String key) {
synchronized (mLock) {
mModified.put(key, this);
return this;
}
}
public Editor clear() {
synchronized (mLock) {
mClear = true;
return this;
}
}
我们可以看到clear()只是将一个mClear的状态给置为true;而remove也是将该Editor放到对应的key下。
(3)提交修改数据:commit()和apply()
真正的操作处理是在commit()和apply()中。从源码中看下这两个方法有什么区别
public boolean commit() {
//。。。。省略代码
//将Editor修改的内容替换内存mMap的内容
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;
}
public void apply() {
//。。。。省略代码
//将Editor修改的内容替换内存mMap的内容
final MemoryCommitResult mcr = commitToMemory();
//两个方法的差别在于下面的几行代码的先后顺序
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
两个方法都要经过下面几个过程
- 1)commitToMemory()
这个两个方法都是一样的,都是将修改内存mMap中的对应的key/value。从源码中可以看出,根据调用的putXXX() /remove() /clear() 之后修改的mModified的状态,来对mMap里面的key/value进行修改,修改结束之后,清空mModified。
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
// 。。。。省略代码,就是初始化上面的集合
//注意这里就是在修改mMap的值的时候,同时会修改mapToWriteToDisk的值,最终将该值写入到文件中
mapToWriteToDisk = mMap;
// 。。。。省略代码,就是初始化上面的集合
synchronized (mLock) {
boolean changesMade = false;
//如果调用了clear(),就将集合中的清空。这里的mMap里面放着我们初始化的时候,将xml文件读出的所有key/value
if (mClear) {
if (!mMap.isEmpty()) {
changesMade = true;
mMap.clear();
}
mClear = false;
}
//通过调用edit()来存放的key/value
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// 如果执行了remove,则v对应的this,将这些key/value从mMap移除
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
//将添加的的key/value或者修改的value放入的到mMap中
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}
//....省略代码
}
//清空该次修改的记录
mModified.clear();
//....省略代码
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
从代码中可以看到mLock是同步锁来保证每次只有一个线程可以访问该方法。另外注意这里就是在修改mMap的值的时候,同时会修改mapToWriteToDisk的值,最终将mapToWriteToDisk该值写入到文件中。
- 2)SharedPreferencesImpl.this.enqueueDiskWrite
将mapToWriteToDisk的内容写入到xml文件中。commit()和apply()两个方法的区别在于前者传入的postWriteRunnable为null,后者会有对应的postWriteRunnable。
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//如果有传入postWriteRunnable则为异步提交,否则为同步提交,而这里就是和apply()的一个区别
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
//就是将mcr.mapToWriteToDisk写入到xml文件中
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
//而apply()这个地方恰好是传入postWriteRunnable,所以执行这里的代码,而不执行代码的代码
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// commit()没有传入postWriteRunnable,则在当前线程中提交,即UI线程中执行写文件
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//开启子线程执行writeToDiskRunnable,所以apply()传入postWriteRunnable时,则在子线程中进行写文件
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
从代码中可以看出,postWriteRunnable的值为null和不为null的区别在于写文件的时候,如果传入的为null,则在主线程中写文件,而传入postWriteRunnable则会在子线程中写文件。所以commit()和apply()两个方法的区别在于前者是在UI线程中写xml文件,后者在子线程中写xml文件。
QueuedWork就是通过HandlerThread开启的一个子线程,所有提交的任务串行执行
- 3) notifyListeners(mcr)
这个是可以通过registerOnSharedPreferenceChangeListener()来主动注册对xml文件的key的监听。只有当主动注册的时候,此时会去通知所有的listener。
注意如果有注册,一定要及时unregisterOnSharedPreferenceChangeListener
(4)apply()在子线程中写文件
上面(3)中可以看到apply在提交数据的时候发生在子线程,但这样真的没有问题了吗?
我们看下这个QueuedWork
private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
//存放的是每次执行apply()时创建的写文件的runnable
private static final LinkedList<Runnable> sWork = new LinkedList<>();
private static Handler sHandler = null;
我们可以看到sWork、sFinishers 都是用static进行修饰,所以所有的QueuedWork都会进行修改该集合。sWork集合中存放的就是apply()创建时写文件的runnable,这些Runnable等待子线程串行执行。
当关闭Activity的时候,在ActivityThread中可以看到
private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {
//s省略代码。。。。。
// Make sure any pending writes are now committed.
if (!r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
//s省略代码。。。。。
}
在结束的时候都会调用QueuedWork中的waitToFinish()方法,进入到waitToFinish()可以看到会等待该集合里面的runnable是否执行完,直到执行完任务才会关闭Activity。所以这里仍然会发现如果key/value比较大的时候,仍然会出现超时5s的情况。
所以SharedPreferenced是对于存储比较大的key/value的时候,性能是会大打折扣
(5)取数据
取数据就是通过getXXX方法进行取数据。
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
从源码中可以看到这里很简单,就是从mMap中取数据。同样mLock是同步锁来保证每次只有一个线程。 awaitLoadedLocked()来保证读取xml文件完毕之后才可以返回对应的value。
总结
1)从源码中可以在创建SharedPrerenced的时候,会通过子线程将xml文件中的所有key/value一次加载到mMap
2)每次获取Editor的时候,都会创建一个新的EditorImpl对象,并且会等着1)中的子线程执行完之后,才会返回
3)在使用Editor进行putXXX的时候,首先会将数据暂存到mModified中,等到执行apply()或者commit()时,才会将mModified里面的内容去替换mMap的key/value
4)commit()是在主线程去修改xml文件,而apply()是在子线程去修改xml文件。apply()提交的所有任务会以串行的方式依次执行
5)apply()在子线程中写xml文件也不能保证绝对安全,在Activity结束的时候,会去检测排队的任务是否全部执行完
6)getXXX就是简单的从mMap中根据key读取value
6)每个方法的执行都有同步锁,不支持并发
几点注意事项
1)不要存放大的key/value,否则将所有的key/value加载到内存或者在写文件的时候,都有可能引起内存过高,甚至ANR。尽量去存放简单和小的key/value
2)不相关的配置文件单独存放,否则单个文件越大读取速度越慢,避免多次读取无效的key/value到内存中。
3)对数据最好批处理。每次在edit()都会创建一个EditorImpl对象,每次commit()/apply()都会执行一次I/O操作,即使apply()在子线程提交,但是任务比较多的时候,在关闭Activity的时候,也会因为等待所有任务执行完而造成页面ANR
4)最好提前初始化,因为在初始化的时候要从xml读取所有的key/value,并且后面的所有方法都会等待该读取结束之后才会执行相关方法的代码
所以说SharedPreferenced只是一个轻量级的存储,如果比较的内容就需要采用其他方式进行存储了。