前言:SharedPreferences是android中用于持久化数据的方式之一,它以key-value的方式组织数据,最后以xml文件的方式保存在/data/data/<package name>/shared_prefs/
目录下。SharedPreferences以key-value方式保存数据,key的类型为String,value支持的类型有:boolean,float,int,long,String,Set<String>。
基本使用:
使用SharedPreferences前,需要先获得一个SharedPreferences对象,有两种方式获得SharedPreferences的对象,一个是在Activity中声明的getPreferences(int mode)
,一个是在Context中声明的getSharedPreferences(String name, int mode)
。getPreferences(int mode)
其实是把当前Activity的类名作为name参数,直接调用getSharedPreferences(String name, int mode)
。这是getPreferences(int mode)
的实现:
public SharedPreferences getPreferences(int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
<span style="color: rgb(85, 85, 85); font-family: 'Trebuchet MS', Verdana, sans-serif; font-size: 1.25em; line-height: 1.4; background-color: rgb(255, 255, 255);">getSharedPreferences(String name, int mode)参数说明:</span>
name 指定保存SharedPreferences的文件名
mode
* MODE_PRIVATE 默认使用这个标志(值为0),SharedPreferences文件只有应用自身可读
* MODE_MULTI_PROCESS Added in API level 11,当多个进程使用同一个SharedPreferences文件时,要指定这个标志(3.0之前默认设置了MODE_MULTI_PROCESS,3.0以后默认是关的)
* MODE_WORLD_READABLE 让其他应用可读SharedPreferences文件,@deprecated in API level 17,不推荐使用
* MODE_WORLD_WRITEABLE 让其他应用可写SharedPreferences文件,@deprecated in API level 17,不推荐使用
一,写SharedPreferences
步骤:1.获得SharedPreferences对象 2.获得Editor对象 3.调用putXXX方法 4.最后apply()提交数据
//代码:
private void writePreferences() {
//如果不存在demo.xml,会新建一个
SharedPreferences sharedPref = getSharedPreferences("demo", Context.MODE_PRIVATE);
//写入数据需要使用Editor
SharedPreferences.Editor editor = sharedPref.edit();
editor.putBoolean("key_demo_boolean", true)
.putInt("key_demo_int", 1)
.putLong("key_demo_long", 2)
.putFloat("key_demo_float,", 3.0f)
.putString("key_demo_string", "Hello, World!");
Set<String> set = new HashSet<String>();
set.add("hello");
set.add("world");
editor.putStringSet("key_demo_set", set);
editor.apply();//最后提交更改
//注意:apply()没有返回值,如果你关注是否提交成功,
//可以使用commit()代替apply()。
//但是apply()效率更高,一般情况下,推荐使用apply()来提交更改,
//两者区别后面会提到。
}
执行后demo.xml的内容:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<float name="key_demo_float," value="3.0" />
<string name="key_demo_string">Hello, World!</string>
<long name="key_demo_long" value="2" />
<int name="key_demo_int" value="1" />
<boolean name="key_demo_boolean" value="true" />
<set name="key_demo_set">
<string>hello</string>
<string>world</string>
</set>
</map>
二, 读SharedPreferences
SharedPreferences sharedPref=getSharedPreferences("demo", Context.MODE_PRIVATE);
int defaultValue=0;
int number=sharedPref.getInt("key_demo_int",defaultValue);
要注意的是,SharedPreferences是接口,而真正的实现在
android.app.SharedPreferencesImpl
中。Context的
getSharedPreferences(String,int)
的实现在
android.app.ContextImpl
中。
三,apply()和commit()的区别
使用SharedPreferences.Editor编辑好数据后,最后都需要调用apply()或commit()来提交更改。这两个方法做的事情都是先将你的更改提交到内存,然后写入硬盘(存储器),它们之间的区别在于:
- apply()方法没有返回值,而commit()的返回值是boolean,你可以从commit()的返回值中了解是否写入成功。
- 在将数据写入硬盘时,commit()方法会在当前线程等待写入完成的结果,而apply()则是开启另外一个线程等待,然后apply()方法本身直接返回。这样就导致调用commit()方法比apply()方法更耗时,因此如果你在UI线程调用commit(),可能会因为这点延迟造成界面短暂的卡顿。
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);
//在enqueueDiskWrite()中被调用
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
//将数据写入到硬盘
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);//回调onSharedPreferenceChanged
}
看commit()的代码,可以看到,commit()会在当前线程等待写入结束
public boolean commit() {
//提交更改到内存
MemoryCommitResult mcr = commitToMemory();
//将数据写入到硬盘
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null );
try {
//在当前线程等待写入结束
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);//回调onSharedPreferenceChanged
return mcr.writeToDiskResult;
}
apply()和commit()都会调用commitToMemory(),它的主要操作是更新保存SharedPreferences的Map,然后返回一个MemoryCommitResult,来看一下MemoryCommitResult的代码:
private static class MemoryCommitResult {
public boolean changesMade; //标识SharedPreferences里的数据是否发生改变
public List<String> keysModified; // may be null
public Set<OnSharedPreferenceChangeListener> listeners; // may be null
public Map<?, ?> mapToWriteToDisk;//存放要写入硬盘的数据
//初始化count为1
public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
//标识写入硬盘是否成功,commit()的返回值来自这个
public volatile boolean writeToDiskResult = false;
public void setDiskWriteResult(boolean result) {
writeToDiskResult = result;
//会使count--,count值变为0,释放调用writtenToDiskLatch.await()的线程
writtenToDiskLatch.countDown();
}
}
提交到内存后,然后这里是将数据写入硬盘:
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
//创建一个将数据写入硬盘的线程
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
//在这里,才将数据写入硬盘
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
//如果postWriteRunnable==null,说明是被commit()而不是apply()调用
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
//每次commitToMemory()会使mDiskWritesInFlight++,写入硬盘完成后,mDiskWritesInFlight--
//如果mDiskWritesInFlight==1,说明当前只有一个commit()操作还没写入到硬盘
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//singleThreadExecutor()会返回一个单线程的线程池,保证任务串行执行(当有多个提交未写入硬盘时)
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
<span style="color: rgb(85, 85, 85); font-family: 'Trebuchet MS', Verdana, sans-serif; font-size: 1.5em; line-height: 1.43; background-color: rgb(255, 255, 255);">Set<String>更新无效问题</span>
<span style="color: rgb(85, 85, 85); font-family: 'Trebuchet MS', Verdana, sans-serif; font-size: 1.5em; line-height: 1.43; background-color: rgb(255, 255, 255);"><span style="color: rgb(51, 51, 51); font-family: 'Helvetica Neue', Helvetica, 'Segoe UI', Arial, freesans, sans-serif; font-size: 16px; line-height: 25.6000003814697px;">在一开始的写SharedPreferences的示例中,我们向demo.xml中写入了一个key为"key_demo_set"的Set<String>,现在我们想向里面追加一个"appendString"。如果你按下面的代码执行,你会发现,等你去读取SharedPreferences时,"appendString"确实被添加了,但是demo.xml中却没有"appendString"。也就是说,现在内存中的SharedPreferences和硬盘中是不一致的。原因在于,不管是apply()还是commit()方法,只有数据发生改变才会进行写入硬盘的操作。虽然set的内容改变了,但是它对应的引用并没有改变。而在SharedPreferences的实现中,判断数据是否发生改变,使用的是Object的equals方法。来看下面的示例代码:</span>
</span>
//这样做不会使数据写入到硬盘
private void updateSetDataFailed() {
SharedPreferences sharedPref = getSharedPreferences("demo", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();//写入数据需要使用Editor
Set<String> set = sharedPref.getStringSet("key_demo_set", new HashSet<String>()/*获取失败才会使用这个,这个示例中,不会用到*/);
set.add("appendString");
editor.putStringSet("key_demo_set", set);
editor.apply();
}
正确的做法是创建一个新的Set对象,再添加更新数据。
//这才是正确的写法
private void updateSetDataSuccess() {
SharedPreferences sharedPref = getSharedPreferences("demo", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();//写入数据需要使用Editor
Set<String> oldSet = sharedPref.getStringSet("key_demo_set", new HashSet<String>()/*获取失败才会使用这个,这个示例中,不会用到*/);
//创建一个新的对象,并复制原来的数据(注意构造函数)
Set<String> newSet=new HashSet<String>(oldSet);
newSet.add("appendString");
editor.putStringSet("key_demo_set", newSet);
editor.apply();
}
从源代码中找找原因。apply()和commit()都会先调用commitToMemory(),看下commitToMemory()的源代码,可以找到答案(看注释部分):
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {
//省略.........
synchronized (this) {
//省略.....
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
if (v == this || v == null) {
//省略......
} else {
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
//使用equals方法判断是否发生改变,如果没有,continue
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}
//标识数据是否发生改变,如果一直continue,changesMade不会被赋值为true
//changesMade在writeToFile()中被使用
mcr.changesMade = true;
//省略....
}
mModified.clear();
}
}
return mcr;
}
mcr.changesMade在writeToFile()方法中被使用,来看看相关的代码:
private void writeToFile(MemoryCommitResult mcr) {
if (mFile.exists()) {
if (!mcr.changesMade) {
mcr.setDiskWriteResult(true);
//可以看到,如果没有发生改变,直接return了,不会执行写数据到硬盘的代码
return;
}
//省略......
}
//省略..........
//省略的代码为将数据写入硬盘的操作,有兴趣的可以看下源代码
}
所以,当你想要对SharedPreferences里的Set数据做出修改时,记得创建先一个新的Set对象(包含原来的数据),然后再进行操作。
Android本地数据存储:Shared Preferences安全风险浅析 http://jaq.alibaba.com/blog.htm?id=56)
Android源码分析之SharedPreferences http://www.cnblogs.com/xiaoweiz/p/3733272.html
SharedPreferences | Android Developers http://developer.android.com/reference/android/content/SharedPreferences.html