大家好,又见面了,我是你们的朋友全栈君。
SharedPreferences(简称sp)Android平台上一个轻量级的存储辅助类,它提供了key-value键值对的接口,用来保存应用的一些常用配置,在应用中通常做一些简单数据的持久化缓存。本文将详细的分析SharedPreferences的实现方式、存储机制、如何正确使用它以及sp的性能问题等方面。
SharedPreferences实现详解
我们在Android开发中,如果想要保存一个相对较小的键值对集合,则应使用SharedPreferences API。SharedPreferences对象指向包含键值对的文件,并提供读写这些键值对的简单方法。SharedPreferences API提供了string,set,int,long,float,boolean六种数据类型的数据访问接口。sp文件在存储区最终数据是以xml形式进行存储。
获取SharedPreferences对象
想要使用sp来存取数据,我们首先要了解如何去获取它,Android的Context类为我们提供了获取SharedPreferences对象的抽象接口。
Context对象的getSharedPreferences()方法可以获取一个SharedPreferences对象,之后我们就可以通过SharedPreferences来管理我们的键值对数据了。
Context中的方法定义:
代码语言:javascript复制 public abstract SharedPreferences getSharedPreferences(File file, @PreferencesMode int mode);
context类为我们提供了访问sp的抽象接口,真正的而实现实在ContextImpl类中。
ContextImpl中的方法实现:
我们来看源码实现:
代码语言:javascript复制 @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;
}
参数:
- file参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存放在“/data/data/<包名>/shared_prefs/”目录下。
- mode参数用于指定操作模式,它的可选值有2种,MODE_PRIVATE(默认值,指定文件是私有的,只可被当前应用访问或者相同user ID的进程)和MODE_MULTI_PROCESS(多进程共享模式)。
逻辑解析:
- getSharedPreferences首先会从内存缓存中查找是否已经存在我们想要获取的SharedPreferences实例。
- 如果不存在,则会为相应的file创建一个SharedPreferencesImpl实例,并且将它添加到缓存中。
- 判断如果参数mode的值是Context.MODE_MULTI_PROCESS或者App的targetSdkVersion小于Android 3.0,则执行调用sp的startReloadIfChangedUnexpectedly方法执行sp文件的重新加载工作,我们稍后分析。
MODE_MULTI_PROCESS模式
我们通常调用getSharedPreferences方法时,使用默认模式即可,也是google推荐的方式。Context.MODE_MULTI_PROCESS是多线程共享模式,理论上可以做到多进程数据共享功能,但是,此功能已废弃,不建议使用了。Context.MODE_MULTI_PROCESS模式是在Android 3.0之前版本的遗留功能,在之后某些版本的Android中无法可靠工作,而且也不提供任何协调跨进程并发修改的机制。我们不应该尝试使用该模式,如果我们有跨进程数据传输的需求,应该使用明确的跨进行数据共享机制,例如ContentProvider等来实现。
另外,MODE_MULTI_PROCESS模式需要进程在每次访问数据时都要进行io操作,以检查数据是否被其他进程改变,这样io操作造成了很大的性能消耗,如果我们在开发中误使用了该模式,应立即改成默认模式!
sp数据文件从存储分区加载到内存过程分析
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文件的加载。
android.app.SharedPreferencesImpl:
代码语言:javascript复制 @UnsupportedAppUsage
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);//这里注意,创建了一个备份文件。
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk(); //加载xml文件到内存中
}
SharedPreferencesImpl类的startReloadIfChangedUnexpectedly方法:
代码语言:javascript复制 @UnsupportedAppUsage
void startReloadIfChangedUnexpectedly() {
synchronized (mLock) {
// TODO: wait for any pending writes to disk?
if (!hasFileChangedUnexpectedly()) {
return;
}
startLoadFromDisk();
}
}
SharedPreferencesImpl的构造方法和startReloadIfChangedUnexpectedly都调用到了startLoadFromDisk()方法。
startLoadFromDisk()方法:
代码语言:javascript复制 @UnsupportedAppUsage
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
逻辑解析:
- startLoadFromDisk方法中,先重置mLoaded状态。
- 创建一个工作线程,线程名为SharedPreferencesImpl-load。
- 线程调用loadFromDisk()方法执行sp文件从存储分区到内存的加载工作。
loadFromDisk()方法:
代码语言:javascript复制 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();
}
}
}
逻辑解析:
- 判断备份文件是否存在,如果存在则代表原始文件数据出现错误,使用备份文件,替换掉原始文件。
- 如果文件存在并且可读取,则把字节流读取到内存中,并且使用XmlUtils.readMapXml工具方法对原始数据进行解析。
- 数据解析后得到一个Map对象,它保存了该sp文件中存储的所有键值对的信息。
- 最后把得到的Map对象赋值给mMap属性,mStatTimestamp和mStatTimestamp用于判断数据是否更新。
SharedPreferences数据在内存中的存储结构
SharedPreferences文件都是存放在“/data/data/<app包名>/shared_prefs/”目录下。sp文件的存储格式是.xml文件,当SharedPreferences文件创建时,就会在相应目录新建一个本地文件。
我们可以从ContextImpl中看到sp文件是如何管理的:
代码语言:javascript复制 private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
这里会定义一个ArrayMap成员变量,存储了当前应用的所有sp对象。这样做是系统为了性能考虑,在每个sp文件读取之后,都会把sp对象存储到一个map中作为缓存。
ArrayMap对象的创建及赋值过程:
代码语言:javascript复制 @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对象数据的存储结构是什么。
SharedPreferences对象数据的存储结构:
- 首先以包名为key,存储一个名为sSharedPrefsCache的ArrayMap队列中。
- 在ArrayMap中,又是以文件名为key,SharedPreferencesImpl对象(sp的实例对象)为value存储的。
- 每一个SharedPreferencesImpl对象都对应一个物理位置在的sp文件。
- SharedPreferencesImpl对象在创建时,或者sp文件有更新时,都会去存储区同步数据文件,并且把数据文件存储到一个map中(key/vaule分别对应我们存储时的key/value),该map就是上文中提到的SharedPreferencesImpl对象的mMap属性。
SharedPreferences的数据读取过程分析
我们以获取int值为例,来看sp的数据读取过程。
SharedPreferencesImpl类的getInt方法:
代码语言:javascript复制 @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文件中的数据已经加载到内存中,所以这里获取时,只需要简单的内存查询即可。
SharedPreferences数据存储过程分析
数据存储过程相对来说比较复杂,我们先来看如何使用sp来实现存储。
SharedPreferences数据存储示例
如果我们想要通过SharedPreferences存储数据,代码如下:
代码语言:javascript复制 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的文件:
代码语言:javascript复制<?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()来保存更改。
我们来看源码。
SharedPreferences.Editor的putInt方法分析
我们以putInt为例,来分析sp数据的存储过程。
SharedPreferences的edit()方法:
代码语言:javascript复制 public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();//等待锁释放
}
return new EditorImpl();
}
该方法new一个EditorImpl对象并返回,所以SharedPreferences.Editor的实现类是EditorImpl。
EditorImpl的putInt方法
代码语言:javascript复制 @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方法
EditorImpl的apply方法:
代码语言:javascript复制 @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);
}
逻辑解析:
- 调用commitToMemory()来把数据更新至内存中。
- apply()方法使用异步的方式来实现数据的写入过程(写入存储分区)。
- 调用SharedPreferencesImpl的enqueueDiskWrite方法来执行数据写入工作,注意这里的第二个参数,如果不为null,则代表异步写入。
我们再来看commit()方法。
EditorImpl的commit方法
代码语言:javascript复制 @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;
}
逻辑解析:
- commit方法同样先调用了commitToMemory()来把数据更新至内存中。
- 然后调用SharedPreferencesImpl的enqueueDiskWrite方法来执行数据写入工作,注意这里是同步写入,第二个参数为null。
apply和commit方法都调用了两个关键方法:commitToMemory和SharedPreferencesImpl的enqueueDiskWrite方法,我们逐个分析。
EditorImpl的commitToMemory方法
代码语言:javascript复制 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);//返回结果
}
逻辑解析:
- mMap中存储的是该sp文件的所有数据,是sp文件在内存中的映射。
- 判断其他线程是否也正在进行写入操作,如果是,则把mMap的键值对复制出一份。
- 将mMap赋值给mapToWriteToDisk变量,后面直接在mapToWriteToDisk上进行操作。
- 如果调用了Editor的clear,则将map中的数据进行清除。
- 遍历mModified中的数据,mModified保存了本次事务提交的所有修改,上文中的putInt的数据,就存在该Map中。
- 判断,key和value都是空值,则跳过该条数据。
- key值存在,value为null,则将数据删除。
- key在map中已经存在,并且value没有改变,则跳过。
- 最后将key和value写入mapToWriteToDisk中。
- 如果数据已经改变,则设置changesMade变量为true。
- 最后返回已修改的内存数据对象MemoryCommitResult。
SharedPreferencesImpl的enqueueDiskWrite方法
数据在内存中更新之后,最后一步就是写入存储分区了,我们来看它对应的方法。
SharedPreferencesImpl的enqueueDiskWrite方法:
代码语言:javascript复制 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文件正在被其他线程执行数据写入,则执行异步写入。
Queued类的queue方法
我们来看异步写入的执行:
代码语言:javascript复制 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来实现的,我们来看它的初始化过程:
代码语言:javascript复制 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方法
写入存储分区真正的执行方法是SharedPreferencesImpl的writeToFile方法:
代码语言:javascript复制 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方法来完成的,逻辑较多,根据代码逻辑,我们简单总结一些它的过程:
- 首先使用fileExists变量来判断sp文件是否存在。
- 如果存在,判断本次是否需要执行写入操作,用变量needsWrite表示。mDiskStateGeneration是一个long类型的值,记录着最后一次数据提交的状态值,用来判断是否存在更新。
- 如果是commit提交的,则needsWrite赋值为true,执行数据写入。
- 如果是apply提交的,则判断是否是最新的一次写入请求如果是,则needsWrite赋值为true,如果不是(是一个中间提交值,之后还有提交),则不执行写入存储分区操作,以优化性能。
- 如果needsWrite的值为false则不需要执行数据写入,结束本次任务。
- 接下来判断sp文件备份是否存在,如果备份不存在,则把sp原始文件重命名为备份文件。如果备份存在,则将file文件删除,因为它可能是错误数据。
- 2~6是sp文件存在时的处理逻辑。到了这里,sp对应的原始文件已经被删除了,只存在备份文件了(如果存在的话)。
- 接下来执行写入存储分区,首先创建一个空文件,以存储sp数据。
- 执行xml数据解析,将内存中的key-value键值对存储到str的数据流中。
- 将数据流写入到存储分区中。
- 写入完成后,将备份文件删除。
- 更新mDiskStateGeneration的值,代表了最后一次写入时的状态值。
- 最后,如果磁盘写入失败,则删除原始sp文件。
好了,到了这里,SharedPreferences的实现原理我们也就分析完了,那么在使用过程时,你是否也了解了SharedPreferences的正确打开方式呢?
SharedPreferences性能问题及最佳实践
sp文件的io操作
- sp文件存储在“/data/data/<app包名>/shared_prefs/”目录下,存储格式是以.xml文件的形式存在。
- sp文件在创建SharedPreferencesImpl对象时,会把文件从磁盘分区加载到内存中,并且存储到Map中。
- sp文件读取是在子线程中进行的,子线程的优先级等同于它的父线程优先级。
- sp文件在更新时,首先会更新到内存的Map对象中,根据提交的方式,commit通常会强制、同步写入,apply会执行异步写入工作,并且会等待100ms。
- sp在异步提交时,使用的是ThreadHandler,线程优先级为前台任务的单线程操作,如果任务很多,怎会等待。
- sp文件在写入时,会删除旧文件,新建新的文件,重新执行写入。
- sp文件的写入更新方式是,全文件内容替换。
- Context.MODE_MULTI_PROCESS模式时,每次调用getSharedPreferences获取SharedPreferences对象时,都会检查数据是否更新,如果更新,则从磁盘重新加载文件到内存中。
SharedPreferences的性能及最佳实践
sp的性能问题:
- sp文件存储在App私有目录,所以会随着App卸载而删除。
- sp文件存储的数据格式是.xml,每次从磁盘读取和写入操作,都需要解析xml,效率不高。
- sp的大量使用会占用大量的内存,因为它会把所有用到的sp文件内容都同步到内存中。
- sp错误使用,会导致大量的io操作,影响系统性能,例如,频繁的commit或apply。
- Context.MODE_MULTI_PROCESS模式的误用,会产生大量的io操作,严重影响性能。
- sp是在新建线程执行初始化工作,如果App启动时,在主线程执行大量的sp初始化工作,会创建大量的线程,且线程优先级同UI线程,这样会造成sp线程抢占UI线程资源,造成启动过慢等问题。
- sp如果是在优先级较低子线程中执行sp的初始化工作,则sp加载过程可能会变的很长。
- sp在提交时,如果在ui线程中使用commit同步提交,则可能会导致因等待而产生的ANR问题。
- sp每次更新到磁盘都是整体写入,性能影响较大。
- sp在执行数据写入时,都会创建EditorImpl对象,大量的提交操作会创建大量的EditorImpl对象,占用大量内存。
- sp跨进程访问模式,不可靠,已废弃。
- 当单个sp文件大于50k时(经验值,不同机器差别较大),io会变的非常缓慢。
- sp文件在执行apply写入时,至少要等到100ms以上。
sp的最佳实践
- 推荐使用sp存储一些数据量较小的应用配置类信息。
- 不要使用sp的Context.MODE_MULTI_PROCESS模式;不要指望使用sp来进行跨进程数据操作。
- 单个sp文件大小最好保持在10kb以内,最大不要超过50kb。
- 将不同的业务数据保存在不同的sp文件内,不要一个文件存储所有数据。
- sp数据更新时,最好多次修改后,统一执行一次commit或apply,以减少io次数。
- sp文件数量也要进行控制,以减少线程数量和内存使用。
- ui线程中使用sp数据要注意时效性,最好在使用之前,预加载到内存。
- sp加载时,会在子线程执行,子线程的优先级等同于父线程,一定要注意加载的时间。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/169755.html原文链接:https://javaforall.cn