- 概述
- 源码分析
- 总结
一、概述
Android系统里面大致有三种数据存储方案:
- 文件存储。
- 数据库存储。
- SharedPreferences存储。
SharedPreferences是这三种方案中最轻量级的存储方案。具有以下特点:
- 使用键值对的方式存储数据。
- 全量读写。全量读写的特性决定了SharedPreferences适合于小数据存储,轻便快捷。
二、源码分析
2.1 初始化
我们可以通过Context的getSharedPreferences方法获取一个SharedPreferences对象。
SharedPreferences本身是一个顶层接口定义了一系列的get方法以及获取Editor对象对内容进行提交的方法。
Context本身也是个抽象类,其具体实现是ContextImpl。接下来直接分析ContextImpl中的getSharedPreferences方法。
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//创建sSharedPrefs对象--全局cache
//以应用包名作为key,记录所有应用创建的SharedPreferences对象。
if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
}
final String packageName = getPackageName();
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
//创建packagePrefs对象--应用cache
//记录当前应用创建的SharedPreferences对象
if (packagePrefs == null) {
packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
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) {
if (name == null) {
name = "null";
}
}
sp = packagePrefs.get(name);
//根据name创建具体的SharedPreferencesImpl对象
//记录在packagePrefs
if (sp == null) {
//如果文件不存在,则创建文件
File prefsFile = getSharedPrefsFile(name);
sp = new SharedPreferencesImpl(prefsFile, mode);
packagePrefs.put(name, sp);
return sp;
}
}
//MODE_MULTI_PROCESS模式下,判断是否需要重新加载文件
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;
}
getSharedPreferences方法接受两个输入参数,name和mode。name表示当前的SharedPreferences文件的名称,mode表示接受的访问方式。mode支持以下几种类型:
- MODE_PRIVATE:默认模式,仅限创建SharedPreferences的应用内可以访问。
- MODE_WORLD_READABLE:共享读,所有的应用有读权限。已经废弃,不推荐使用
- MODE_WORLD_WRITEABLE:共享写,所有的应用有写权限。已经废弃,不推荐使用
- MODE_MULTI_PROCESS:多进程模式。设置了这个flag,在加载文件的时候会根据文件的lastModifiedTime和size判断文件是否被修改。如果检查到文件被修改了,则会重新从磁盘加载SharedPreferences文件。
2.1.1 getSharedPrefsFile
@Override
public File getSharedPrefsFile(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
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");
}
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
mPreferencesDir = new File(getDataDirFile(), "shared_prefs");
}
return mPreferencesDir;
}
}
getSharedPrefsFile根据名字在mPreferencesDir目录下创建文件。如果mPreferencesDir为空则先创建mPreferencesDir,其路径为:/data/data/package_name/shared_prefs。
2.1.2 SharedPreferencesImpl初始化
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");
}
SharedPreferencesImpl在初始化过程中创建后缀名为.bak的备份文件,调用startLoadFromDisk加载文件。
2.1.3 startReloadIfChangedUnexpectedly
void startReloadIfChangedUnexpectedly() {
synchronized (this) {
// TODO: wait for any pending writes to disk?
if (!hasFileChangedUnexpectedly()) {
return;
}
startLoadFromDisk();
}
}
首先判断文件有没有改变,文件改变则调用startLoadFromDisk方法从磁盘加载文件。
2.1.4 hasFileChangedUnexpectedly
// 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.");
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;
}
}
根据文件的修改时间和大小判断文件是否有改变。
2.1.5 startLoadFromDisk
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}
开启子线程加载文件。具体的加载过程交给loadFromDiskLocked执行。
2.1.6 loadFromDiskLocked
loadFromDiskLocked是真正执行从磁盘加载的逻辑。
private void loadFromDiskLocked() {
//mLoaded是一个标记量,用来判断是否已经加载过
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");
}
//读取xml文件,将其保存在map中
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);
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;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<String, Object>();
}
notifyAll();
}
-
通过mLoaded标记量判断是否已经加载,已经加载过不再重复加载。
-
如果有备份文件,则直接从备份文件读取。删除原文件,将备份文件重命名为原文件名。
-
对Xml文件进行解析,将解析完的数据保存在变量mMap中,mMap类型为HashMap.
-
通知所有处于等待状态的线程。这里用到的是多线程里面的生产者-消费者模式。getXXX执行的是获取操作,如果数据没有准备好(通过mLoaded标记量判断)则wait。等到数据setXXX(初始化,初始化过程中从磁盘读取数据为异步操作)完成notify生产者获取。
2.2 Editor写数据
SharedPreferences内部所有的写请求都是通过Editor去实现的。通过edit方法可以获得Editor对象。Editor本身也是接口,其对应的具体实现类为EditorImpl。
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();
}
2.2.1 Editor初始化
public final class EditorImpl implements Editor {
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;
}
- mModified对应的是Editor的内存cache,所有的put的操作都是直接修改内存里面的数据,在经过commit或者apply提交修改到文件本身。
- clear是一个清除标记量
2.2.2 putXXX
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
public Editor putInt(String key, int value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
...
put系列方法提供了直接对内存cache进行修改的操作。
2.2.3 commit
public boolean commit() {
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;
}
commit的内部的执行逻辑:
- commitToMemory首先同步提交到内存的修改;
- enqueueDiskWrite执行同步磁盘文件更新。
2.2.3.1 commitToMemory
MemoryCommitResult
// 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();
}
}
- changesMade:用来判断key是否发生改变。
- keysModified:保存发生改变的key。
- writtenToDiskLatch:同步门闩,apply提交修改时,用来控制一次只有一个线程进行writeToFile操作。
- mapToWriteToDisk:记录当前需要写入xml文件的数据。
- writeToDiskResult:标记量表示内存提交到磁盘的写入操作是否成功。
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++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
mcr.keysModified = new ArrayList<String>();
mcr.listeners =
new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
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();
// "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.
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
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);
}
}
mModified.clear();
}
}
return mcr;
}
- commitToMemory返回的是一个MemoryCommitResult对象,MemoryCommitResult中有一个成员变量mapToWriteToDisk用来记录需要更新的磁盘的数据,其类型为HashMap。
- commitToMemory会首先创建一个MemoryCommitResult对象mcr。并且将SharedPreferenceImpl里面的mMap记录在mapToWriteToDisk变量中。
- 检测清除标记量mClear是否为true。如果设置mClear为true,则更新mcr当中changesMade变量为true。同时清除SharedPreferenceImpl的成员变量mMap。
- 然后遍历Editor里面保存更改的数据mModified,将其中的数据添加到mMap中,添加完成后清除mModified。
2.2.3.2 enqueueDiskWrite
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();
}
}
};
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) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
- commit调用enqueueDiskWrite时,传入的postWriteRunnable对象为空。isFromSyncCommit为true表明执行同步写操作。直接执行writeToDiskRunnable对象的run方法,将操作转发给writeToFile方法。
- apply对象执行的是异步写操作,通过单一线程线程池执行writeToDiskRunnable任务。
2.2.3.3 writeToFile
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) { //没有key发生改变,则直接返回
// 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命名为备份文件
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 {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false);
return;
}
//将mMap全部数据写入文件
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);
}
- 如果key没有发生改变,则直接返回。
- 将mMap全部数据写入xml文件,如果写入成功则删除备份文件,如果写入失败则删除mFile。
2.2.4 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);
// 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);
}
apply的内部的执行逻辑:
- commitToMemory首先同步提交到内存的修改;
- enqueueDiskWrite执行异步磁盘文件更新,这里所有的文件IO都是通过单线程控制的。QueuedWork保存的是等待提交的任务。enqueueDiskWrite通过postWriteRunnable参数是否为空来进行同步或者异步操作。为空时进行同步写,否则将任务提交到单一线程线程池进行任务写。
2.2.5 clear
public Editor clear() {
synchronized (this) {
mClear = true;
return this;
}
}
clear操作只是提供对mClear标记量进行修改,将其设置为true。
2.3 读取数据
2.3.1 getString
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
getXXX系列方法执行的是数据获取操作,先判断数据有没有加载完成。如果数据加载完成则直接从内存中或者。mMap就是对应的数据初始化完成之后对应的内存cache。
2.3.2 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();
} catch (InterruptedException unused) {
}
}
}
如果没有加载完成则处于等待状态。
三、总结
- SharedPreferences是Android内置的一个轻量级的数据持久化框架。其内部以键值对的形式保存数据。生成的目标文件在/data/data/package_name/shared_prefs目录下。
- 所有对SharedPreferences进行Put和Get的操作都是针对内存操作。SharedPreferences内部通过Editor对数据进行写操作,Editor内部支持两种方式写数据:
- commit:同步更新内存,同步更新磁盘。
- apply:同步更新内存,异步更新磁盘。
- 进行apply提交更改时,如果进行的更改的任务比较多,耗时比较长的话。在页面跳转时会调用QueuedWork.waitToFinish的方法以等待任务执行完成,这会带来ANR隐患,尤其是在一些性能比较差的设备上。解决方案中有一种最简单的方式就是使用异步commit,尽量避免使用apply。
网友评论