SharedPreference是Android中的轻量级的存储方式,将键值对写入xml文件中,并保存在/data/data/package_name/shared_prefs路径下。
1、SharedPreferences.java
它是一个interface,如下:
public interface SharedPreferences {
public interface OnSharedPreferenceChangeListener {
void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
}
public interface Editor {
/**
* 写入键值对
*/
Editor putString(String key, String value);
Editor putStringSet(String key, Set<String> values);
Editor putInt(String key, int value);
Editor putBoolean(String key, boolean value);
/**
* 清除键值为key的preference
*/
Editor remove(String key);
/**
* 清除preference所有内容
*/
Editor clear();
/**
*commit将其preference同步写入持久存储;
*写入成功后返回true
*如果不关心返回值,并在应用主线程中使用,请考虑使用apply()以替换commit()
*/
boolean commit();
/**
*apply启动异步将更改提交到磁盘;没有返回值,如果失败,将不会收到任何通知。
*/
void apply();
}
/**
* Retrieve all values from the preferences.
*/
Map<String, ?> getAll();
/**
* 获取数据
*/
String getString(String key, String defValue);
Set<String> getStringSet(String key, Set<String> defValues);
int getInt(String key, int defValue);
boolean getBoolean(String key, boolean defValue);
void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
}
2、SharedPreference的获取
通过 Context#getSharedPreferences 方式获取。
在Context.java中,getSharedPreferences()是一个抽象方法,
public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);
具体实现在ContextImpl.java中。
这里先说一下这个方法中的两个参数:
name:preference文件名;
mode:有MODE_PRIVATE,MODE_WORLD_READABLE,MODE_WORLD_WRITEABLE,MODE_MULTI_PROCESS。其中MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE在AndroidN以后版本不能使用,若使用会抛SecurityException异常;MODE_MULTI_PROCESS在某些Android版本上支持不好,未来可能会Deprecated,设置了此mode,每次getSharedPreferences时都会重新检查并加载磁盘文件。(如需精确的跨进程数据管理,应使用ContentProvider.)
ContextImpl.getSharedPreferences():
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// 判空处理 "null.xml".
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) {
//创建name.xml文件
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
//检查mode
checkMode(mode);
if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
if (isCredentialProtectedStorage()
&& !getSystemService(UserManager.class)
.isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
//AndroidO及以后版本,sp是ce加密,在用户第一次解锁之前无法使用
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;
}
Sharedpreferences的实现类是SharedPreferencesImpl类,构造方法如下:
SharedPreferencesImpl(File file, int mode) {
mFile = file; //磁盘上的xml文件
mBackupFile = makeBackupFile(file); //xml.bak备份文件,用于写入失败时的恢复
mMode = mode;
mLoaded = false;
mMap = null; //用于内存中缓存数据,getXxx从这里获取数据
mThrowable = null;
startLoadFromDisk();
}
构造方法中涉及到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;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
Map<String, Object> map = null;
StructStat stat = null;
Throwable thrown = 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 = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
} catch (Throwable t) {
thrown = t;
}
synchronized (mLock) {
//设置mLoaded为true,表示已经读取过,下次getSharedPreference时不再从磁盘读取
mLoaded = true;
mThrowable = thrown;
try {
if (thrown == null) {
if (map != null) {
//将map赋给全局变量mMap
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
} catch (Throwable t) {
mThrowable = t;
} finally {
//已经加载完毕,释放mLock锁,通知其他等待线程。
mLock.notifyAll();
}
}
}
3、SharedPreference获取数据方法
以getString(String, String)为例,代码如下:
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
//阻塞等待sp从磁盘读取xml到内存之后再get数据
awaitLoadedLocked();
String v = (String)mMap.get(key);
//v为null,则返回默认值
return v != null ? v : defValue;
}
}
private void awaitLoadedLocked() {
...;
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
awaitLoadedLocked
是等待xml文件加载完毕;如果首次执行getSharedPreferences后立即调用getXxx ,若加载还未完成(mLoaded为false
), getXxx 会卡在awaitLoadedLocked
,一旦加载完毕,则加载线程会通过notifyAll
通知所有在 awaitLoadedLocked
中等待的线程,getXxx 就能够返回了。sp最好不要加载过大的文件,否则阻塞时间过长。
4、SharedPreference写入及移除数据
4.1、写入数据需要Editor对象,先来看一下SharedPreferencesImpl.java中Editor对象的获取:
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
//每次调用edit()都会创建一个EditorImpl对象
return new EditorImpl();
}
EditorImpl类中有两个成员变量mModified和mClear,其中mModified表示每次putXxx后的改变的配置项,mClear表示清空配置项,但是只清了SharedPreferenceImpl的mMap。
现在来看EditorImpl中的put,remove方法:
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
//将键值对存入mModified
mModified.put(key, value);
return this;
}
}
public Editor remove(String key) {
synchronized (mEditorLock) {
//remove中存入value是this
mModified.put(key, this);
return this;
}
}
可见,修改值只会将其暂存到mModified
中,在Editor中所做的所有更改直到调用了commit()
或apply()
才会设置到mMap(内存)
和xml文件(磁盘)中去。
4.2、commit及apply实现
commit()方法:
public boolean commit() {
//完成内存写入,并返回一个MemoryCommitResult对象
MemoryCommitResult mcr = commitToMemory();
//放入磁盘写入队列
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
//等待其他线程执行完毕 writtenToDiskLatch.countdown减为0后从await方法返回
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
}
//通知监听者
notifyListeners(mcr);
//返回提交结果
return mcr.writeToDiskResult;
}
内存写入:
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
//此时正在写入,无法修改
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 (mEditorLock) {
//标记是否发生改变
boolean changesMade = false;
//如果调用了clear方法,会先执行clear操作
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
//remove方法里面存放的value是this,如果是this就去执行remove操作
if (v == this || v == null) {
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
//未发生修改,则跳过
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
//发生了修改,则存入map中
mapToWriteToDisk.put(k, v);
}
//标记发生了修改
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
//将修改的map:mModified置为null
mModified.clear();
if (changesMade) {
//提交到内存的次数
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
//将提交到内存次数,发生修改的集合,监听者及map封装成MemoryCommitResult并返回
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
内存修改完成后,接着加入磁盘写入队列:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//commit为true,apply为false
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
//写入文件
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
//apply操作时此runnable不为null,apply操作时该runnable执行
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
//commit操作时才执行
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//writeToDiskRunnable执行,执行写入文件
writeToDiskRunnable.run();
return;
}
}
//apply操作时才会执行
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
由上述代码段看出commit()是同步的,通过enqueueDiskWrite()
调用 writeToFile()方法
将数据写到磁盘。
apply()方法:
apply方法利用QueuedWork实现异步操作.
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
//线程等待
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
//将awaitCommit加入QueuedWork
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
//放入写队列
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
//通知监听者
notifyListeners(mcr);
}
通过阅读源码,整理总结如下:
1、sharedpreference是一个使用二级缓存(磁盘用xml文件,内存使用hashmap)的key-value存储方式
2、sp写入文件不要太大,否则会导致卡顿等
3、xml解析速度较慢,空间占用大,因此对于高频变化的存储场景不适用
4、不要多次调用edit(),每次调用都会创建一个editor对象
5、不要多次调用apply()或commit(),多次写入的话应该批量修改,最后一次执行调用
6、尽可能早地调用getSharedPreference,这样可以确保在调用getXxx或putXxx时数据已经完成写入内存
7、sharedpreference的跨进程模式只是在获取sharedpreference时重新从磁盘读取文件,使用此模式时每次都要通过getSharedPreferences获取sharedpreference变量,重新load数据,这样会引起其他线程等待耗时,降低内存读写性能。跨进程使用它是不可靠的。