我们平时在项目里保存数据的时候经常使用SharedPreferences来保存数据,比如说什么第一次启动的数据,还有一些比较轻量级的数据保存都保存在这个里面。我们平时不管是在子线程还是主线程中都可以使用这个类来获取数据和更新数据而且也没有烦人的多线程问题。
使用方法
- 存数据的两种方法:commit 和 apply
SharedPreferences sp = getSharedPreferences("test_demo", Context.MODE_PRIVATE);
sp.edit().putString("name", "demo").putInt("time", 110000).commit();
SharedPreferences sp = getSharedPreferences("test_demo", Context.MODE_PRIVATE);
sp.edit().putString("name", "demo").putInt("time", 110000).apply();
- 取数据的方式
SharedPreferences sp = getSharedPreferences("fileName", Context.MODE_PRIVATE);
String name = sp.getString("name", null);
int time = sp.getInt("time", 0);
实现原理
getSharedPreferences的具体实现是在frameworks/base/core/java/android/app/ContextImpl.java
public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
}
final String packageName = getPackageName();
//根据包名来判断系统中是否存在指定的Map对象了
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
sSharedPrefs.put(packageName, packagePrefs);
}
.......
sp = packagePrefs.get(name);
//如果指定的sp不存在内存中的话,那么就创建一个文件,并且将对象保存到packagePrefs
if (sp == null) {
File prefsFile = getSharedPrefsFile(name);
sp = new SharedPreferencesImpl(prefsFile, mode);
packagePrefs.put(name, sp);
return sp;
}
}
//主要是用来控制多进程下使用的,但是该代码并不能保证多进程下出问题,所以google官方文档里面才会说后续再支持多进程的问题
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;
}
首先程序会判断 sSharedPrefs 是否为空,该集合是一个整个系统的sharep的集合,key是一个包名,然后value又是一个Map集合,该ArrayMap集合的key则是我们传进的来 文件名字,value的话则是 SharedPreferencesImpl,该对象主要是用来操纵本地的 sharep信息用的。如果 sp 不存在内存中的话,那么就会创建一个 .xml 文件对象并且传给实现类。
public File getSharedPrefsFile(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
//在这里我们可以看到如果我们传进来的name如果有 "/" 文件路径分隔符的话,则会报错的
private File makeFilename(File base, String name) {
if (name.indexOf(File.separatorChar) < 0) {
return new File(base, name);
}
throw new IllegalArgumentException("File " + name + " contains a path separator");
}
SharedPreferences 的实现类 frameworks/base/core/java/android/app/SharedPreferencesImpl.java
在创建SharedPreferencesImpl对象的时候首先会创建一个临时文件对象跟我们传进来的文件的名字是一样的,只是后缀名是.bak的。并且会将本地存在的sharep的值通过xmlpull解析器解析到内存的map集合中。其实我们第一次使用sharep的话,是不会创建那个文件的,这个也是很多人会迷惑的,为啥这里人家不创建一个sharep.xml的文件,这个问题我们后续我们会说到。
private void loadFromDiskLocked() {
if (mLoaded) {
return;
}
//如果本地的备份文件存在的话,那么将备份文件重命名成.xml的文件
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 = Libcore.os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
//使用xmlpull将内容解析到内存中
map = XmlUtils.readMapXml(str);
} catch (XmlPullParserException e) {
Log.w(TAG, "getSharedPreferences", e);
} catch (FileNotFoundException e) {
Log.w(TAG, "getSharedPreferences", e);
} catch (IOException e) {
Log.w(TAG, "getSharedPreferences", e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
}
mLoaded = true;
if (map != null) {
mMap = map;
//将本地sharep.xml文件的时间和大小保存到内存中
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<String, Object>();
}
notifyAll();
}
以上就是我们通过Context来获取Sharep对象的大概流程,其实我们获取sharep对象的时候,首先会检测内存的map中是否存在,然后再去创建Sharep的实现类对象,然后保存到map中方便下次用的时候直接在内存中获取了。这里我们先分析将数据保存到Sharep文件的过程,因为数据都还没有保存到文件中,就先分析获取数据的话,这个时候我们可能会有点懵的。
public Editor edit() {
// TODO: remove the need to call awaitLoadedLocked() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
// context.getSharedPreferences(..).edit().putString(..).apply()
// ... all without blocking.
synchronized (this) {
awaitLoadedLocked();
}
return new EditorImpl();
}
我们在保存数据的时候首先都会使用sharep的 edit()方法,其实这个方法方法的背后就是创建Editor接口的实现类,我们保存的数据的实现都是在EditorImpl里面,比如说我们保存字符串,其他类型的数据保存的话基本上就差不对的:
public final class EditorImpl implements Editor {
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;
.....
public Editor putString(String key, String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
.....
}
通过代码我们可以知道EditorImpl内部维护这一个Map集合,该集合用于保存我们put进去的数据,并且返回当前的对象,也就是说我们保存的数据并没有立刻就保存到文件中因为这样子可以减少IO的频繁的操作,然后我们在一次edit()方法之后依次不断的put我们想要的数据,最后我们在统一comit()或者是apply()数据的,下面我们依次的分析apply和commit()的实现:
apply 保存数据的方式
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
//这里我们将保存在内存的数据最后一次性的保存到文件中
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
//这里如果我们注册了sharep的监听的话,那么当我们将数据保存之后就会通知Shared发生改变
notifyListeners(mcr);
}
Ps:在保存数据的时候,这里会创建一个等待任务,并且将这个等待任务保存到QueuedWork,然后再创建一个写的任务,用户将 await的任务从QueuedWork中移除出去的,QueuedWork中的任务统一都是在waitToFinish()方法中执行,从方法的注释中我们可以知道当activity处于onPause,或者是收到广播,或者是服务处理的时候都会调用该方法的,也就是会开始执行任务的。
/**
* Finishes or waits for async operations to complete.
* (e.g. SharedPreferences$Editor#startCommit writes)
*
* Is called from the Activity base class's onPause(), after
* BroadcastReceiver's onReceive, after Service command handling,
* etc. (so async work is never lost)
*/
public static void waitToFinish() {
Runnable toFinish;
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
toFinish.run();
}
}
首先会将临时保存在mModified的数据通过过滤,跟以前的数据进行变化对比最后保存在全局的mMap集合中,然后再将value变化对应的key保存在一个list中,方便以后通过listener将修改的值的key回调出去。
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {
.....
mcr.mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
//这里会判断注册的listener的个数,然后赋值给MemoryCommitResult对象。
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
mcr.keysModified = new ArrayList<String>();
mcr.listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
//同步代码块,这里判断是否有别的地方调用clear来清理数据
synchronized (this) {
if (mClear) {
if (!mMap.isEmpty()) {
mcr.changesMade = true;
mMap.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
if (v == this) { // magic value for a removal mutation
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
boolean isSame = false;
//这里判断全局的mMap中是否存在key了;如果存在然后判断当前修改的数据
//与以前的保存的数据是否一致,如果相同的话则不更新内存数据。
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}
//这里将会有变动的key保存到一个list集合中,然后在通过listener将key回调出去
mcr.changesMade = true;
if (hasListeners) {
mcr.keysModified.add(k);
}
}
mModified.clear();
}
}
return mcr;
}
在保存数据的时候,首先会判断shared.xml文件是否存在,如果存在的话则先将该文件重命名为.bak的文件,如果是第一次shared.xml不存在话,则将内存中保存的数据直接通过xml pull解析器直接保存到文件中最后设置好文件的权限模式等等,同时删除备份文件(.bak),最后当我们所有的操作完成的时候,记得要释放刚才的锁操作的。不然 await 任务将会一直处于等待的状态
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
......
}
};
//同步的操作主要是用于commit操作,异步的操作apply则缺少postWriteRunnable,
//也就是CountDownLatch的地方不同,等下会介绍下
final boolean isFromSyncCommit = (postWriteRunnable == null);
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
将数据写入到文件的的过程就不分析了,无非就是各种文件是否存在的判断,以及将数据通过xmlpull保存到本地文件中,这里最关键的就是看懂写入文件的线程的await操作,确实要注意的是CountDownLatch的使用,从apply保存数据来看,我们知道保存数据的时候是通过异步的方式来保存(也就是开了线程去保存)这样子导致ANR异常的概率就更小了,但是网上有人提出了这样子还出ANR的情况以及分析,本人这里并没有看明白为啥那样子也还会出现问题,因为CountDownLatch的 await 也是在一个线程里面,写入文件的时候也是在另外单独的线程里面。
commit 提交的方式
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;
}
其实commit跟apply一些基本上操作基本上都是一样的,不同的就是 writtenToDiskLatch await的线程不同,从代码中我们可以知道 CountDownLatch 等待的将是提交数据的线程,如果我们单独的开线程去保存的数据,那么主线程将会处于等待的状态直到数据保存之后才会释放 await,这个时候如果保存到本地的时间过长的话将会导致ANR异常的问题。因为这个时候挂起的是UI线程,如果对于数据量比较大的话,个人感觉还是放在线程里面去保存,这样子可以保证界面操作的流畅性的。通过上面我们已经分析了保存数据的两种的方式,下面我们来看看获取数据的具体事情,shared获取数据的话相对来说就显得更加简单的,只是简单的从保存在内存中的map直接取出来就行了。
public String getString(String key, String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
总结
通过对shared的分析我们发现获取数据是非常简单的,就是从内存的map中根据key来获取value的。再保存数据的时候则涉及到线程同步的问题,最关键的是使用CountDownLatch 来使任务当前处于等待,直接保存的任务执行完成之后,才将当前await的任务释放并且往下执行,在待命中我们看到很多的同步代码块用防止多线程保存数据和获取数据的时候出现问题,这个也是我们非常值得学习的地方。至于里面的一些缺点的话由于功力未到所以暂时不能提供更好的意见,也还不知道如何优化,只能知道其实实现的大概原理。
现在我们知道apply保存数据比commit数据效率更高的,因为apply是直接开了一个线程去保存数据并且不会阻塞UI线程的,而commit保存数据的时候如果不开线程的话,那么CountDownLatch则会将主线程直接挂起来的直到保存数据结束才会释放,如果保存数据的过程时间稍微长的话,这样子容易导致界面操作不流畅,所以我们更加建议在项目大的时候直接开一个线程去commit 数据。但是在shared中有着大量的同步代码块,如果在大量线程中去操作shared数据的话那么效率不高,特别是在高并发的时候,但是对于移动应用内我感觉并没有很大的问题。
下面分享两个比较好的对shared的文章的链接:
- http://blog.chinaunix.net/uid-29506893-id-5761774.html 该文章主要是指出shared在保存数据的时候导致了程序的ANR问题,但是我在代码的时候没有怎么发现出现在哪里的。
- http://www.cnblogs.com/puff/p/5530825.html 该文章是一些对shared的优缺点的分析和总结