
大家好,又见面了,我是你们的朋友全栈君。
SharedPreferences(简称sp)Android平台上一个轻量级的存储辅助类,它提供了key-value键值对的接口,用来保存应用的一些常用配置,在应用中通常做一些简单数据的持久化缓存。本文将详细的分析SharedPreferences的实现方式、存储机制、如何正确使用它以及sp的性能问题等方面。
我们在Android开发中,如果想要保存一个相对较小的键值对集合,则应使用SharedPreferences API。SharedPreferences对象指向包含键值对的文件,并提供读写这些键值对的简单方法。SharedPreferences API提供了string,set,int,long,float,boolean六种数据类型的数据访问接口。sp文件在存储区最终数据是以xml形式进行存储。
想要使用sp来存取数据,我们首先要了解如何去获取它,Android的Context类为我们提供了获取SharedPreferences对象的抽象接口。
Context对象的getSharedPreferences()方法可以获取一个SharedPreferences对象,之后我们就可以通过SharedPreferences来管理我们的键值对数据了。
public abstract SharedPreferences getSharedPreferences(File file, @PreferencesMode int mode);context类为我们提供了访问sp的抽象接口,真正的而实现实在ContextImpl类中。
我们来看源码实现:
@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) {
checkMode(mode);
if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
if (isCredentialProtectedStorage()
&& !getSystemService(UserManager.class)
.isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
throw new IllegalStateException("SharedPreferences in credential encrypted "
+ "storage are not available until after user is unlocked");
}
}
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) {
// 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方法时,使用默认模式即可,也是google推荐的方式。Context.MODE_MULTI_PROCESS是多线程共享模式,理论上可以做到多进程数据共享功能,但是,此功能已废弃,不建议使用了。Context.MODE_MULTI_PROCESS模式是在Android 3.0之前版本的遗留功能,在之后某些版本的Android中无法可靠工作,而且也不提供任何协调跨进程并发修改的机制。我们不应该尝试使用该模式,如果我们有跨进程数据传输的需求,应该使用明确的跨进行数据共享机制,例如ContentProvider等来实现。
另外,MODE_MULTI_PROCESS模式需要进程在每次访问数据时都要进行io操作,以检查数据是否被其他进程改变,这样io操作造成了很大的性能消耗,如果我们在开发中误使用了该模式,应立即改成默认模式!
SharedPreferences工具类为我们提供了管理sp数据的接口,从而简化了数据存取操作。sp数据文件最终是以.xml文件的格式,存储到App数据私有目录:/data/data/<app包名>/shared_prefs/目录下,那么sp文件是如何从存储区加载到内存中的呢?
在Context的getSharedPreferences方法获取SharedPreferences对象时,我们发现如果参数mode的值是Context.MODE_MULTI_PROCESS或者App的targetSdkVersion小于Android 3.0,则执行调用sp的startReloadIfChangedUnexpectedly方法执行sp文件的重新加载工作,这里就涉及到了sp文件加载过程,同样SharedPreferencesImpl类的构造过程也会涉及到sp文件的加载。
@UnsupportedAppUsage
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);//这里注意,创建了一个备份文件。
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk(); //加载xml文件到内存中
} @UnsupportedAppUsage
void startReloadIfChangedUnexpectedly() {
synchronized (mLock) {
// TODO: wait for any pending writes to disk?
if (!hasFileChangedUnexpectedly()) {
return;
}
startLoadFromDisk();
}
}SharedPreferencesImpl的构造方法和startReloadIfChangedUnexpectedly都调用到了startLoadFromDisk()方法。
@UnsupportedAppUsage
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
} private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) { //判断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); //获取输入流
map = (Map<String, Object>) XmlUtils.readMapXml(str);//解析xml文件到map中
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
// An errno exception means the stat failed. Treat as empty/non-existing by
// ignoring.
} catch (Throwable t) {
thrown = t;
}
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
// It's important that we always signal waiters, even if we'll make
// them fail with an exception. The try-finally is pretty wide, but
// better safe than sorry.
try {
if (thrown == null) {
if (map != null) { //一切顺利,这里开始赋值
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
// In case of a thrown exception, we retain the old map. That allows
// any open editors to commit and store updates.
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll();
}
}
}SharedPreferences文件都是存放在“/data/data/<app包名>/shared_prefs/”目录下。sp文件的存储格式是.xml文件,当SharedPreferences文件创建时,就会在相应目录新建一个本地文件。
我们可以从ContextImpl中看到sp文件是如何管理的:
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;这里会定义一个ArrayMap成员变量,存储了当前应用的所有sp对象。这样做是系统为了性能考虑,在每个sp文件读取之后,都会把sp对象存储到一个map中作为缓存。
@GuardedBy("ContextImpl.class")
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}从代码中我们可以看出,系统对SharedPreferences对象数据的存储结构是什么。
我们以获取int值为例,来看sp的数据读取过程。
@Override
public int getInt(String key, int defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}SharedPreferencesImpl类的getInt方法可以获取一个在sp文件中存储的int值。这里可以看到,源码中是直接从mMap中读取的,而这个mMap是SharedPreferencesImpl在创建时初始化的。这种做法,可以避免每次读取时,系统和存储分区的交互,从而大幅度提升了性能。
其他几种类型的数据读取逻辑类似,这里可以看到,读取过程相对来说非常简单,当SharedPreferencesImpl实例创建完成后,sp的xml文件中的数据已经加载到内存中,所以这里获取时,只需要简单的内存查询即可。
数据存储过程相对来说比较复杂,我们先来看如何使用sp来实现存储。
如果我们想要通过SharedPreferences存储数据,代码如下:
SharedPreferences.Editor editor = getSharedPreferences("person", MODE_PRIVATE).edit();
editor.putString("name","budaye");
editor.putInt("age",18);
editor.apply();代码执行之后,系统会在/data/data/<app包名>/shared_prefs/目录下,创建一个名为person.xml的文件:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="name">budaye</string>
<int name="age" value="18" />
</map>sp的数据存储,需要借助SharedPreferences的内部类Editor来实现,并且最后要使用apply()或commit()来保存更改。
我们来看源码。
我们以putInt为例,来分析sp数据的存储过程。
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();//等待锁释放
}
return new EditorImpl();
}该方法new一个EditorImpl对象并返回,所以SharedPreferences.Editor的实现类是EditorImpl。
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
@Override
public Editor putInt(String key, int value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}这里只是简单的将key和value的值put到了mModified中,mModified是一个Map,它存储者一次事务提交的所有将要变更的数据列表。
EditorImpl的apply方法:
@Override
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) {
}
if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
……
notifyListeners(mcr);
}我们再来看commit()方法。
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
if (DEBUG) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " committed after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}apply和commit方法都调用了两个关键方法:commitToMemory和SharedPreferencesImpl的enqueueDiskWrite方法,我们逐个分析。
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); //如果其他线程也正在进行写入操作,我们先把mMap的键值对复制出一份。
}
mapToWriteToDisk = mMap; //直接在mapToWriteToDisk上进行操作
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
if (mClear) {//如果调用了Editor的clear,则先将map中的数据进行清除。
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) { //遍历mModified中的数据
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 (!mapToWriteToDisk.containsKey(k)) {//key和value都是空值,则跳过该条数据
continue;
}
mapToWriteToDisk.remove(k);//key值存在,value为null,则将数据删除。
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {//key在map中已经存在,并且value没有改变,则跳过
continue;
}
}
mapToWriteToDisk.put(k, v);//将key/value写入map中
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);//返回结果
}数据在内存中更新之后,最后一步就是写入存储分区了,我们来看它对应的方法。
SharedPreferencesImpl的enqueueDiskWrite方法:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null); //判断是否需要同步
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}首先判断是否需要同步,这里可以看到apply是异步的,commit是同步的(有条件的同步)。 如果只有一个线程在执行写入操作mDiskWritesInFlight的值是1,则直接调用writeToFile方法执行写入工作。 否则,调用QueuedWork.queue方法添加到任务队列中执行,等待执行。 这里commit同步提交也是有条件的,如果commit时,该sp文件正在被其他线程执行数据写入,则执行异步写入。
我们来看异步写入的执行:
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);//如果是applay提交,这里直接delay了100毫秒再执行
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}异步任务是通过HandlerThread来实现的,我们来看它的初始化过程:
private static Handler getHandler() {
synchronized (sLock) {
if (sHandler == null) {
HandlerThread handlerThread = new HandlerThread("queued-work-looper",
Process.THREAD_PRIORITY_FOREGROUND);
handlerThread.start();
sHandler = new QueuedWorkHandler(handlerThread.getLooper());
}
return sHandler;
}
}这里创建了一个HandlerThread来执行异步,也就是任务队列是单线程的,并且线程优先级是前台线程优先级。
写入存储分区真正的执行方法是SharedPreferencesImpl的writeToFile方法:
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
……
boolean fileExists = mFile.exists(); //sp文件是否存在
……
if (fileExists) {
boolean needsWrite = false; //本次是否需要执行写入操作
// Only need to write if the disk state is older than this commit
if (mDiskStateGeneration < mcr.memoryStateGeneration) { //mDiskStateGeneration是一个long类型的值,记录着最后一次数据提交的状态值。
if (isFromSyncCommit) {//如果是同步写入操作,这里指commit提交的,直接执行写入存储分区操作
needsWrite = true;
} else {
synchronized (mLock) {
// No need to persist intermediate states. Just wait for the latest state to
// be persisted.
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) { //如果是异步写入,这里指apply提交的,则判断是否是最新的一次写入请求,如果不是,则不执行写入存储分区操作,以优化性能。
needsWrite = true;
}
}
}
}
if (!needsWrite) { //不需要执行数据写入
mcr.setDiskWriteResult(false, true);
return;
}
boolean backupFileExists = mBackupFile.exists();//判断备份是否存在
if (DEBUG) {
backupExistsTime = System.currentTimeMillis();
}
if (!backupFileExists) {//如果备份不存在,则把sp原始文件重命名为备份文件。
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false, false);
return;
}
} else {//备份存在,则将file文件删除,因为它可能是错误数据。
mFile.delete();
}
}
//到了这里,sp对应的原始文件已经被删除了,只存在备份文件了。
// 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);//创建一个空的原始文件,以存储sp数据。
if (DEBUG) {
outputStreamCreateTime = System.currentTimeMillis();
}
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);//执行xml数据解析,将内存中的key-value键值对存储到str的数据流中。
writeTime = System.currentTimeMillis();
FileUtils.sync(str);//将数据流写入到存储分区中。
fsyncTime = System.currentTimeMillis();
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
……
mBackupFile.delete();//写入完成后,将备份文件删除。
……
mDiskStateGeneration = mcr.memoryStateGeneration;//更新mDiskStateGeneration的值,代表了最后一次写入时的值
mcr.setDiskWriteResult(true, true);
……
return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
// 如果磁盘写入失败,则删除原始sp文件。
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false, false);
}sp文件写入存储分区(磁盘)的工作是由SharedPreferencesImpl的writeToFile方法来完成的,逻辑较多,根据代码逻辑,我们简单总结一些它的过程:
好了,到了这里,SharedPreferences的实现原理我们也就分析完了,那么在使用过程时,你是否也了解了SharedPreferences的正确打开方式呢?
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/169755.html原文链接:https://javaforall.cn