SP的特点以及基本使用方式
SharedPreferences因非常适合存储较小键值集合数据且使用非常简单的特点,而受到广大程序员们热爱。
SP使用非常简单:
//读操作
Context context = getActivity();
SharedPreferences sp = getSharedPreferences("fileName", Context.MODE_PRIVATE);
String result = sp.getString("key", "defaultValue");
//写操作
Context context = getActivity();
SharedPreferences sp = getSharedPreferences("fileName", Context.MODE_PRIVATE);
sp.edit().putString("key", "value").commit();
SP源码分析
SP是如何读取数据的
其实Context实现就是ContextImpl中,要想搞清楚SP是如何读取数据的,第一步当然是要了解ContextImpl.getSharedPreferences方法是如何实现的
/**
* Map from package name, to preference name, to cached preferences.
*/
//缓存所有应用的SP容器,该容器key对应应用名称,value则为每个应用存储所有sp的容器(ArrayMap)
private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
if (sSharedPrefs == null) {
//如果静态对象不存在,直接创建一个Map,以便后期用于保存sp
sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
}
//获取当前应用包名
final String packageName = getPackageName();
//从保存sp的容器中通过包名查找当前应用所有sp;每个app的所有sp都是保存在ArrayMap中,
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
if (packagePrefs == null) {
//如果从sp容器没找到保存当前应用sp的ArrayMap直接创建一个
packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
//将创建的对象保存到sp容器中
sSharedPrefs.put(packageName, packagePrefs);
}
// At least one application in the world actually passes in a null
// name. This happened to work because when we generated the file name
// we would stringify it to "null.xml". Nice.
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
//如果targetSdk版本好小于19,且传入的文件名为null的话,默认将文件名命名为"null"
if (name == null) {
name = "null";
}
}
//从当前应用的sp容器中通过文件名去查找sp
sp = packagePrefs.get(name);
if (sp == null) {
//如果没找到,直接创建一个文件名以name命名的xml文件
File prefsFile = getSharedPrefsFile(name);
//此处极为关键,该构造器是读取文件操作
sp = new SharedPreferencesImpl(prefsFile, mode);
//将创建sp对象保存到当前应用sp容器中
packagePrefs.put(name, sp);
return 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.
//如果读取模式是跨线程或targetSdk版本小于11,再次重新load下文件而已
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
从上面源码可以看出,getSharedPreferences方法是先根据当前应用名称来获取一个ArrayMap(存储sp的容器)如果没有直接创建并保存到内存中,然后再根据文件名来获取SharedPreferencesImpl的对象(没找到则直接创建SharedPreferencesImpl),这短短的三十几行代码还是比较简单的。
因为是静态变量存储键值数据的所以我们用SP存储的数据在内存中是一直存在;所以我们尽量用一个文件来存在数据,以达到减少内存对象
大家可以看到getSharePreferences返回的对象类型其实是SharedPreferencesImpl类型,只不过该类实现了SharedPreferences接口而已,接下来我们先看看SharedPreferencesImpl构造器做了啥东东。
SharedPreferencesImpl.java
final class SharedPreferencesImpl implements SharedPreferences {
private final File mFile;
private final File mBackupFile;
private final int mMode;
private Map<String, Object> mMap; // guarded by 'this'
private int mDiskWritesInFlight = 0; // guarded by 'this'
//文件是否加载成功
private boolean mLoaded = false; // guarded by 'this'
//文件的时间以及大小
private long mStatTimestamp; // guarded by 'this'
private long mStatSize; // guarded by 'this'
private final Object mWritingToDiskLock = new Object();
private static final Object mContent = new Object();
private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
SharedPreferencesImpl(File file, int mode) {
//给类成员变量赋值
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
//开启一个线程读取文件
startLoadFromDisk();
}
private static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
private void startLoadFromDisk() {
synchronized (this) {
//使用同步代码代码块,对mloader进行赋值
mLoaded = false;
}
//开启线程读取文件
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}
private void loadFromDiskLocked() {
//如果文件已经加载完毕直接返回
if (mLoaded) {
return;
}
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 = Os.stat(mFile.getPath());
if (mFile.canRead()) {
//读取文件
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
//使用XmlUtils工具类读取xml文件数据
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;//如果有数据,将数据已经赋值给类成员变量mMap,
mStatTimestamp = stat.st_mtime;//记录文件上次修改时间
mStatSize = stat.st_size;//记录文件大小(字节单位)
} else {
//没有数据直接创建一个hashmap对象
mMap = new HashMap<String, Object>();
}
//此处非常关键是为了通知其他线程文件已读取完毕,你们可以执行读/写操作了
notifyAll();
}
}
通过上述分析我们不难发现调用getSharedPreferences方法就已经开启一个线程去读取文件了
我们再来看看SharedPreferencesImpl.getString内部是如何执行的
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
//此处会阻塞当前线程,直到文件读取完毕
awaitLoadedLocked();
//从类成员变量mMap中直接读取数据,没有直接返回默认值
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
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();
} catch (InterruptedException unused) {
}
}
}
至此关于SP的读操作以全部分析完毕,相信大家对SP读操作有了更深入的认识了;下面我们继续再看看SP的写操作是怎么玩的
SP是如何写入数据的
通常写数据大体是这样的
//写操作
Context context = getActivity();
SharedPreferences sp = getSharedPreferences("fileName", Context.MODE_PRIVATE);
sp.edit().putString("key", "value").commit();
getSharedPreferences大家都很清楚了,我们现在edit方法到底做了什么
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();
}
可以看到edit方法非常简单,首先是通过同步代码块调用了awaitLoadedLocked方法,紧接着直接返回了一个EditorImpl实例对象,我们继续追踪看看EditorImpl类是put、commit方法
public final class EditorImpl implements Editor {
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;
}
}
public Editor remove(String key) {
synchronized (this) {
//注意此处并没有执行删除操作,而是将其对应key的value设置了当前this
//commitToMemory方法中会对此做特殊处理
mModified.put(key, this);
return this;
}
}
public Editor clear() {
synchronized (this) {
mClear = true;
return this;
}
}
public boolean commit() {
//第一步 commitToMemory方法可以理解为对SP中的mMap对象同步到最新数据状态
MemoryCommitResult mcr = commitToMemory();
//第二步 写文件;注意第二个参数为null,写文件操作会运行在当前线程
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;
}
...
}
从上面可以看到,其实putXXX方法知识把键值数据先存放到内存中去了(mModified对象中);比较有意思到是remove、clear方法,remove方法会将要删除的数据的value设置为EditorImpl自己(commitToMemory方法会对此做特殊处理);
clear方法也仅仅是设置一个标志位而已(commitToMemory方法中用到);
最关键的方法还是commit方法,我们可以看到其实commit方法主要分为四步,第一步将sp数据同步到最新状态并返回mcr对象;第二步将mcr对象中数据写入文件;第三步通知监听器数据已发生改变;最后一步就是返回写操作状态是否成功
public final class EditorImpl implements Editor {
//可以最后看,和commit主要区别就是apply的写文件操作会在一个线程中执行,不会阻塞UI线程
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
//将awaitCommit任务提交到一个队列中
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
//写文件操作完成后会执行此2行代码(如果写操作未结束,当前页面已经销毁写操作的数据会丢失?这个切入点主要看QueuedWork的waitToFinish方法何时调用就明白了,有兴趣同学可以自己研究下)
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);
}
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
//创建一个对象
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.
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.mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
//如果sp注册了多个监听器,将其赋值给mcr对象
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
mcr.keysModified = new ArrayList<String>();
mcr.listeners =
new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (this) {
//如果调用了clear方法,判断sp中mMap是否为空,不为空直接清空数据,并设置标志位,表示数据有改变; 此操作只会清空上次数据,不会对清空本次数据
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.
//如果对应的值为它自己EditorImpl或为null
if (v == this || v == null) {
//如果sp的数据结合没有该键值对,直接进入下一个循环
if (!mMap.containsKey(k)) {
continue;
}
//有该键值集合直接删除,因为remove方法其实存放的就是EditorImpl对象
mMap.remove(k);
} else {
//将要写入的键值数据,同步到sp的mMap中
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) {
mcr.keysModified.add(k);
}
}
//结尾清空EditorImpl中mModified数据
mModified.clear();
}
}
return mcr;
}
private void notifyListeners(final MemoryCommitResult mcr) {
//注意监听器的回调方法都是在UI线程执行的
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 {
// Run this function on the main thread.
ActivityThread.sMainThreadHandler.post(new Runnable() {
public void run() {
notifyListeners(mcr);
}
});
}
}
}
// 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();
}
}
SharePreferenceImpl.java
/**
* 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) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
//持有mWritingToDiskLock锁后,执行写文件操作
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
//写操作数减一
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
//文件写入操作完成后,执行后续动作;apply方法会执行到次
postWriteRunnable.run();
}
}
};
//判断是不是调用commit方法
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) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//如果是commit方法,直接在当前线程执行;可以看出如果当前是UI线程,会阻塞UI线程,引起界面卡顿
writeToDiskRunnable.run();
return;
}
}
//如果不是commit是apply方法,writeToDiskRunnable任务会被提交到一个单个线程的线程池中执行
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
// 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()) {
//如果文件存在且数据未发生改变,没有必要执行无用的写入操作,直接通知并返回即可
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;
}
//文件存在且备份文件不存在
if (!mBackupFile.exists()) {
//尝试将文件设置为备份文件(重命名)(此时只有备份文件,mFile对应的文件已经没有了);如果操作不成功,通知并返回;因为下面到操作是写入数据到mFile中,所以我们先要备份数据,这样即使写入数据失败时,我们还可以使用备份数据
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false);
return;
}
} else {
//如果备份文件存在,直接删除mFile,因为接下来要重新写入mFile了
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 {
//创建一个文件输出流,用于写入数据到mFile中
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
//创建失败直接返回
mcr.setDiskWriteResult(false);
return;
}
//写入数据到mFile中
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();//写入数据成功,则删除备份文件;因为所有数据都写入mFile中
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()) {//写入过程中出现异常则删除mFile,下次可以直接从备份文件中读取
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false);
}
private static FileOutputStream createFileOutputStream(File file) {
FileOutputStream str = null;
try {
str = new FileOutputStream(file);
} catch (FileNotFoundException e) {
File parent = file.getParentFile();
if (!parent.mkdir()) {
Log.e(TAG, "Couldn't create directory for SharedPreferences file " + file);
return null;
}
FileUtils.setPermissions(
parent.getPath(),
FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
-1, -1);
try {
str = new FileOutputStream(file);
} catch (FileNotFoundException e2) {
Log.e(TAG, "Couldn't create SharedPreferences file " + file, e2);
}
}
return str;
}
void startReloadIfChangedUnexpectedly() {
synchronized (this) {
// TODO: wait for any pending writes to disk?
if (!hasFileChangedUnexpectedly()) {
return;
}
//可以看到如果被文件被别的进程修改过,重新加载下文件而已
startLoadFromDisk();
}
}
// Has the file changed out from under us? i.e. writes that
// we didn't instigate.
//判断文件是不是已经被别人(别的进程)修改了?
private boolean hasFileChangedUnexpectedly() {
synchronized (this) {
if (mDiskWritesInFlight > 0) {
// If we know we caused it, it's not unexpected.
if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
//如果是自己修改,直接返回false
return false;
}
}
final StructStat stat;
try {
/*
* Metadata operations don't usually count as a block guard
* violation, but we explicitly want this one.
*/
BlockGuard.getThreadPolicy().onReadFromDisk();
stat = Os.stat(mFile.getPath());
} catch (ErrnoException e) {
return true;
}
//根据文件上次修改时间以及大小二个属性来判读是否别的进程修改过
synchronized (this) {
return mStatTimestamp != stat.st_mtime || mStatSize != stat.st_size;
}
}
相信大家应该SP的读写有了更深刻的认识!