在Android中, SharePreferences是一个轻量级的存储类,特别适合用于保存软件配置参数。使用SharedPreferences保存数据,其背后是用xml文件存放数据,文件存放在/data/data/ < package name > /shared_prefs目录下:
简单使用
SharedPreferences sharedPreferences = getSharedPreferences("ytr", Context.MODE_PRIVATE);
Editor editor = sharedPreferences.edit();//获取编辑器
editor.putString("name", "MyName");
editor.putInt("age", 21);
editor.commit();//提交修改
会在shared_pref目录下生成如下的文件,文件名为ytr.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="name">MyName</string>
<int name="age" value="21" />
</map>
因为SharedPreferences背后是使用xml文件保存数据,getSharedPreferences(name,mode)
方法的第一个参数用于指定该文件的名称,名称不用带后缀,后缀会由Android**自动加上**。方法的第二个参数指定文件的操作模式,共有四种操作模式,
这四种模式代表的含义为:
Context.MODE_PRIVATE = 0
Context.MODE_APPEND = 32768
Context.MODE_WORLD_READABLE = 1
Context.MODE_WORLD_WRITEABLE = 2
Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问.
Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件,对MODE_PRIVATE来说没有区别,都是追加文件。
Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件。
MODE_WORLD_READABLE:表示当前文件可以被其他应用读取;
MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入。
获取SPref中的值
SharedPreferences sharedPreferences = getSharedPreferences("ytr", Context.MODE_PRIVATE);
// getString()第二个参数为缺省值,如果preference中不存在该key,将返回缺省值
String name = sharedPreferences.getString("name", "");
int age = sharedPreferences.getInt("age", 1);
context.getSharedPreferences() 分析
从Context中获取SharedPref对象
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
// 获取SharedPerf的文件集合
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
// 从SP集合中取出指定的File文件
sp = cache.get(file);
if (sp == null) {
// 缓存中没有时,构造一个SP对象
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
// 多进程的情况下,会进行Check,如果有其他进程修改SP文件,会重新加载
// 文档中有提示,这不是一个好的跨进程通信方式
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文件,如果需要,则重新加载
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
SharedPref对象的构造
// 初始化一些变量
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
// 将SP文件加载到内存中
startLoadFromDisk();
}
startLoadFromDisk()
这里会启动一个新线程进行加载
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
loadFromDisk()
从Disk中获取SP文件
private void loadFromDisk() {
synchronized (SharedPreferencesImpl.this) {
if (mLoaded) {
return;
}
// 如果BackupFile存在,实际读取的是备份文件
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用来获取文件信息
StructStat stat = 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 = XmlUtils.readMapXml(str);
} catch (XmlPullParserException | IOException e) {
Log.w(TAG, "getSharedPreferences", e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
/* ignore */
}
synchronized (SharedPreferencesImpl.this) {
mLoaded = true;
if (map != null) {
// mMap是Spref对象的内存缓存
mMap = map;
// 记录文件信息,从Linux系统获取
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
// 第一次装载时,创建Map
mMap = new HashMap<>();
}
// 这个通知需要持有SharedPreferencesImpl.this执行
// 一般时所有的getXXX()
notifyAll();
}
}
这个时候XML已经被装载到内存中了,内存中的SPref映射正是mMap对象
getXXX()
然后我们分析下getXXX()方法,这里以getString()为例
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
// 此处等待加载完毕,否则一直阻塞
awaitLoadedLocked();
String v = (String)mMap.get(key);
// 实际中的取值还是从内存中取得,如果没有,使用默认的值
return v != null ? v : defValue;
}
}
使用awaitLoadedLocked()等待加载完毕
private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
// 这里使用了个wait(),如果loadFromDisk()方法没有执行完毕
// 会一直阻塞,直到内存中有数据,这里明白loadFromDisk()最后有个notifyAll()的原因了吧
wait();
} catch (InterruptedException unused) {
}
}
}
putXXX()
这里以putString()为例
// 暂存用户设置的数据,待commit,或者apply()时进行写入
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
// 实际上也是先将值写入内存中
mModified.put(key, value);
return this;
}
}
这里有两个特殊的,remove()和clear()
public Editor remove(String key) {
synchronized (this) {
// remove是将对象自己放在HashMap中,下面会介绍如何进行处理
mModified.put(key, this);
return this;
}
}
public Editor clear() {
synchronized (this) {
// 写入一个clear标志,在提交时会进行check
mClear = true;
return this;
}
}
commit和apply分析
apply是异步,commit是同步,在主线程中使用commit可能会影响性能,因为同步IO操作的耗时可能会比较长,两个方法都能保证value被正确的保存到磁盘上。两者都是Editor类的方法,它们的具体实现在EditorImpl类中,我们先大体比较一下这两个函数:
public boolean commit() {
// 首先提交到内存,然后决定是异步还是同步进行IO
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
commitToMemory() 内存同步操作
这个内部类用来处理内存中的数据和协助通知回调
// Return value from EditorImpl#commitToMemory()
private static class MemoryCommitResult {
public boolean changesMade; // any keys different?
public List<String> keysModified; // may be null
public Set<OnSharedPreferenceChangeListener> listeners; // may be null
public Map<?, ?> mapToWriteToDisk;
public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
public volatile boolean writeToDiskResult = false;
public void setDiskWriteResult(boolean result) {
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
// 构造一个MemoryCommitResult
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {
// We optimistically don't make a deep copy until
// a memory commit comes in when we're already
// writing to disk.
// mDiskWritesInFlight这个变量用来表示还有多少待写入Disk的请求
// 我们使用commitToMemory()修改一个值,这个变量会加1
if (mDiskWritesInFlight > 0) {
// We can't modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
// 如果并发的情况下,需要进行拷贝
mMap = new HashMap<String, Object>(mMap);
}
// mcr中实际需要写入文件的Map和mMap是同一份引用
mcr.mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
// 处理监听器
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
mcr.keysModified = new ArrayList<String>();
mcr.listeners =
new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (this) {
// 调用clear()的情况
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();
// "this" is the magic value for a removal mutation. In addition,
// setting a value to "null" for a given key is specified to be
// equivalent to calling remove on that key.
// v==this是remove()一个key的情况
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
// 对putXXX()来的值进行修改
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}
mcr.changesMade = true;
if (hasListeners) {
// 将所有需要回调Listener的字段加入ArrayList
mcr.keysModified.add(k);
}
}
// 清空暂存缓存
mModified.clear();
}
}
return mcr;
}
然后是调用执行操作
/** * Enqueue an already-committed-to-memory result to be written * to disk. * * They will be written to disk one-at-a-time in the order * that they're enqueued. * * @param postWriteRunnable if non-null, we're being called * from apply() and this is the runnable to run after * the write proceeds. if null (from a regular commit()), * then we're allowed to do this disk write on the main * thread (which in addition to reducing allocations and * creating a background thread, this has the advantage that * we catch them in userdebug StrictMode reports to convert * them where possible to apply() ...) */
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
// 构造一个Runnble对象,这里将写入文件的操作封装
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
//每次写入文件成功,会将mDiskWritesInFlight这个变量-1
mDiskWritesInFlight--;
}
// 如果在写入文件完毕后有回调,在这里执行
// commit()时这个变量会为null
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// 判断是否需要同步执行
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
// 需要同步IO的情况
synchronized (SharedPreferencesImpl.this) {
// 是否存在需要写入操作
wasEmpty = mDiskWritesInFlight == 1;
}
// 直接执行
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
// 异步的情况提交线程池进行执行
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
writeToFile() 实际的IO操作
// Note: must hold mWritingToDiskLock
private void writeToFile(MemoryCommitResult mcr) {
// Rename the current file so it may be used as a backup during the next read
if (mFile.exists()) {
// check changesMade 标志,在commitToMem()时如果字段需要修改,将写为true
if (!mcr.changesMade) {
// If the file already exists, but no changes were
// made to the underlying map, it's wasteful to
// re-write the file. Return as if we wrote it
// out.
mcr.setDiskWriteResult(true);
return;
}
// 备份文件不存在,会将这个XML进行备份
if (!mBackupFile.exists()) {
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false);
return;
}
} else {
// 删除文件,便于下次写入
mFile.delete();
}
}
// Attempt to write the file, delete the backup and return true as atomically as
// possible. If any exception occurs, delete the new file; next time we will restore
// from the backup.
try {
// 使用Stream修改文件
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false);
return;
}
// 将Map写成Xml
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
// 这里获取文件信息
final StructStat stat = Os.stat(mFile.getPath());
synchronized (this) {
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
// Writing was successful, delete the backup file if there is one.
// 写入成功时,清除备份文件
mBackupFile.delete();
// 设置标志位为写入成功
mcr.setDiskWriteResult(true);
return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
// Clean up an unsuccessfully written file
// 这里时处理异常情况,清除没有写成功的文件
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false);
}
public void setDiskWriteResult(boolean result) {
writeToDiskResult = result;
// 释放闭锁, 所有调用await()的地方会继续执行
writtenToDiskLatch.countDown();
}
notifyListeners()通知改变字段的Listener回调
private void notifyListeners(final MemoryCommitResult mcr) {
if (mcr.listeners == null || mcr.keysModified == null ||
mcr.keysModified.size() == 0) {
return;
}
// 检测是否是主线程
if (Looper.myLooper() == Looper.getMainLooper()) {
for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
final String key = mcr.keysModified.get(i);
// 通知回调
for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
if (listener != null) {
listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);
}
}
}
} else {
// 使用Handler post到主线程执行
// Run this function on the main thread.
ActivityThread.sMainThreadHandler.post(new Runnable() {
public void run() {
notifyListeners(mcr);
}
});
}
}
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);
}
};
// 准备执行IO,这时postWriteRunnable不为null,所以会提交给线程池执行
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);
}
OnSharedPreferenceChangeListener
/** * Interface definition for a callback to be invoked when a shared * preference is changed. */
public interface OnSharedPreferenceChangeListener {
/** * Called when a shared preference is changed, added, or removed. This * may be called even if a preference is set to its existing value. * * <p>This callback will be run on your main thread. * * @param sharedPreferences The {@link SharedPreferences} that received * the change. * @param key The key of the preference that was changed, added, or * removed. */
void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
}
通过Reg和unReg注册到WeakHashMap中
// WeakReference组成的HashMap,不会造成内存泄漏
private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
synchronized(this) {
mListeners.put(listener, mContent);
}
}
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
synchronized(this) {
mListeners.remove(listener);
}
}
对SharedPref的优化建议
SP的瓶颈分析
IO瓶颈
IO瓶颈造成SP性能差是最大的原因,解决了IO瓶颈,80%的性能问题就解决了。
SP的IO瓶颈包括读取数据到内存与数据写入磁盘两部分。
读取数据到内存有两个场景会触发:
- SP文件没有被加载到内存时,调用getSharedPreferences方法会初始化文件并读入内存。
- 版本低于android_H或使用了MULTI_PROCESS标志时,每次调用getSharedPreferences方法时都会读入。
我们可以优化的就是它了。每次加载数据到内存太过影响效率。
H以下版本留存率已经很低了,基本可以忽略。
对于MULTI_PROCESS,可以采用ContentProvider等其他方式,效率更好,而且可避免SP数据丢失的情况。
数据写入磁盘也有两个场景会触发:
Editor的commit方法,每次执行时同步写入磁盘。
Editor的apply方法,每次执行时在单线程池中加入写入磁盘Task,异步写入。
commit和apply的方法区别在于同步写入和异步写入,以及是否需要返回值。
在不需要返回值的情况下,使用apply方法可以极大的提高性能。
同时,多个写入操作可以合并为一个commit/apply,将多个写入操作合并后也能提高IO性能。
锁瓶颈
SP的get操作,会锁定SharedPreferences对象,互斥其他操作。
SP的put操作,getEditor及commitToMemory会锁定SharedPreferences对象,put操作会锁定Editor对象,写入磁盘更会锁定一个写入锁。
由于锁的缘故,SP操作并发时,耗时会徒增。减少锁耗时,是另一个优化点。
由于读写操作的锁均是针对SP实例对象的,将数据拆分到不同的sp文件中,便是减少锁耗时的直接方案。
降低单文件访问频率,多文件均摊访问,以减少锁耗时。
用开发机进行了简单的性能测试(写入均使用apply,若使用commit则多线程耗时更高):
读写同一文件,10个线程每个读写10次数据:
耗时80-130ms
读写10个文件,每个文件由1个线程读写10次数据:
耗时30-70ms
减少单个SPref文件的大小,将数据均摊到每个文件中,是提高SP访问性能的重要一步。
对SP操作的不当封装
我们采用ContentProvider方案支持跨进程访问,并对所有SP操作均套上了ContentProvider进行访问。
随着项目越来越庞大,通过ContentProvider访问造成的耗时性能也成了问题。
对ContentProvider操作SP测试,耗时是直接操作SP的4倍左右。
所以,最近项目中进行了SP的处理,对于不需要跨进程的SP操作去掉了ContentProvider,尽可能减少无谓耗时。
总结
尽量不要直接调用SharedPreferences进行读写操作。
若直接调用getSharedPreferences(fileName,mode).edit().putString(key,value),则对数据的操作直接耦合了fileName和key,后续想调整file和key会比较困难。
可以考虑封装一下,譬如:
public void saveUserId(){
getSharedPreferences(fileName,mode).edit().putString(“user_id”,value);
}
这样做可以直接对数据访问,而与fileName与key解耦,后续拆分与调整时会很方便。将SP作为耗时操作对待,尽量减少无谓的调用。
譬如以下代码,SP读一次即可:
if(sp.getUserId()>0){
int id=sp.getUserId();
…
}