一、概述
SharedPreferences简称Sp(后面都会称Sp),是一种轻量级的数据存储方式,采用Key/value的方式
进行映射,最终会在手机的/data/data/package_name/shared_prefs/目录下以xml的格式存在。
Sp通常用于记录一些参数配置、行为标记等!因为其使用简单,所以大多数开发者用起来很爽!但是
请注意:千万不要使用Sp去存储量大的数据,也千万不要去让你的Sp文件超级大,否则会大大影响应用性能,
甚至出现ANR,没错是ANR(下面会分析)。
二、使用分析
1.获取一个SharedPreferences
/**
*param:name在/data/data/package_name/shared_prefs/目录下生成的文件的名字(如果该文件不存在就会创建,如果存在则更新)
*param:mode该文件的访问模式(Context.MODE_PRIVATE:默认的创建模式,只能由创建它的或者UID相同的应用程序访问,其余三种已经废弃,故不介绍)
*传送门:https://developer.android.com/reference/android/content/Context.html#MODE_PRIVATE
*/
SharedPreferences sharedPreferences = getSharedPreferences(NAME_SP_TEST, Context.MODE_PRIVATE);
2.通过Eidt更新数据
传送门:https://developer.android.com/reference/android/content/SharedPreferences.Editor
/**
*获取一个Edit对象,所有对数据的操作都需要经过Edit
*/
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(“Lbjfan”,true);
/**
*apply和commit是两种不同的写入方式(源码会说到),这时候更新才会生效(有则添加,无则更新)
*/
editor.apply();//editor.commit();
三.源码分析
1.getSharedPreferences();
public SharedPreferences getSharedPreferences(String name, int mode) {
Class var4 = ContextImpl.class;
SharedPreferencesImpl sp;
synchronized(ContextImpl.class) {
//一个全局的静态对象,定义如下:private static ArrayMap> sSharedPrefs;
if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap();
}
//根据应用程序的包名在程序的执行环境中取到该应用程序对应的SharedPreferences文件组
String packageName = this.getPackageName();
ArrayMap packagePrefs = (ArrayMap)sSharedPrefs.get(packageName);
//如果不存在就创建,并缓存到全局的ArrayMap(sSharedPrefs)中
if (packagePrefs == null) {
packagePrefs = new ArrayMap();
sSharedPrefs.put(packageName, packagePrefs);
}
if (this.mPackageInfo.getApplicationInfo().targetSdkVersion < 19 && name == null) {
name = "null";
}
//根据文件的Name去拿到对应的SharedPreferencesImpl对象,如果没有就创建,并放入到自己的上一级缓存packagePrefs中
sp = (SharedPreferencesImpl)packagePrefs.get(name);
if (sp == null) {
File prefsFile = this.getSharedPrefsFile(name);
sp = new SharedPreferencesImpl(prefsFile, mode);
packagePrefs.put(name, sp);
return sp;
}
}
if ((mode & 4) != 0 || this.getApplicationInfo().targetSdkVersion < 11) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
总结起来就是:由于ContextImpl是应用程序的执行环境,每个应用程序里面可以包含有多个SharedPreference文件。因此,为了更好的定位SharedPreference文件,首先根据应用程序进行筛选,得到ArrayMap 然后再通过SharedPreference文件名进行筛选,得到SharedPreferencesImpl。可以看到,SharedPreferencesImpl只会被创建一次,之后会被保存在缓存中,后续的获取操作都是从缓存中获取SharedPreferencesImpl实例对象。
获取文件目录的函数,注意到shared_prefs这个关键词
private File getPreferencesDir() {
Object var1 = this.mSync;
synchronized(this.mSync) {
if (this.mPreferencesDir == null) {
this.mPreferencesDir = new File(this.getDataDirFile(), "shared_prefs");
}
return this.mPreferencesDir;
}
}
2.SharedPreferencesImpl的构造函数
构造函数主要进行文件备份,加载状态初始化等准备操作,并开始将文件从磁盘加载到内存
SharedPreferencesImpl(File file, int mode) {
this.mFile = file;
//同名的.bak文件用于当发生异常时,用于恢复数据
this.mBackupFile = makeBackupFile(file);
this.mMode = mode;
this.mLoaded = false;
this.mMap = null;
this.startLoadFromDisk();
}
从磁盘读取文件
private void startLoadFromDisk() {
//同步一个标记位,是否已经从磁盘读取文件,读取到以后就不用再去读取
synchronized (mLock) {
mLoaded = false;
}
//开启一个子线程去从磁盘加载文件,文件读取属于耗时操作,主线程容易ANR
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
3.具体读文件的操作
private void loadFromDisk() {
synchronized (mLock) {
//保证只加载一次
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;
Throwable thrown = null;
//具体的文件读取操作,然后解析XML,存储在Map中!这也是为何SharedPrefernces不是线程安全的,因为使用的是Map数据结构
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map) XmlUtils.readMapXml(str);
} 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置为空,让等待加载完成的线程被唤醒
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中
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();
}
}
}
4.数据获取
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
//等待文件加载到内存
awaitLoadedLocked();
//直接从内存中的Map中读取
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阻塞
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
小结:一般情况下使用Sp获取数据操作的操作都是在主线程进行的,如果第一次获取数据的时候,Sp文件过大,那么子线程从磁盘获取文件的操作比较耗时,导致主线程等待的时间大于5秒,此时就会发生ANR
5.数据更新
SharedPreferences.Editor editor = sharedPreferences.edit();//Edit的实现类EditorImpl
editor.putString();
editor.apply() or editor.commit()
构造方法
public Editor edit() {
//
synchronized (mLock) {
//同样使调用Edit的线程等待,如果我们在主线程中切Sp文件没有被加载到内存中也会造成阻塞,引发ANR
awaitLoadedLocked();
}
return new EditorImpl();
}
put方法
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
//更新到内存中
mModified.put(key, value);
return this;
}
}
apply和commit方法
commit方法(有返回值,告诉开发者成功还是失败)
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 {
//等待写入结果,主线程调用,写入时间过长会导致阻塞,发生ANR
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;
}
构建需要写入的数据
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
// 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);
}
//讲内存中Map的值赋给需要写入文件的Map
mapToWriteToDisk = mMap;
//写入文件的次数
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
//对Key/Value的处理,有则更新,无则添加
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) {
//如果已经存在相同的key/value,则直接返回,否则添加到需要写入磁盘的map中
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
创建并开启一个线程写入磁盘
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) {
//同时写入磁盘的计数-1
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);
}
写入文件的操作
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
long startTime = 0;
long existsTime = 0;
long backupExistsTime = 0;
long outputStreamCreateTime = 0;
long writeTime = 0;
long fsyncTime = 0;
long setPermTime = 0;
long fstatTime = 0;
long deleteTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
boolean fileExists = mFile.exists();
if (DEBUG) {
existsTime = System.currentTimeMillis();
// Might not be set, hence init them to a default value
backupExistsTime = existsTime;
}
// Rename the current file so it may be used as a backup during the next read
if (fileExists) {
boolean needsWrite = false;
// Only need to write if the disk state is older than this commit
// 只有磁盘上文件状态比当前文件更旧时,才执行更新操作
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
needsWrite = true;
} else {
synchronized (mLock) {
// No need to persist intermediate states. Just wait for the latest state to
// be persisted.
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
// 不需要立即写入,则在MemoryCommitResult中记录该结果,然后直接返回
if (!needsWrite) {
mcr.setDiskWriteResult(false, true);
return;
}
boolean backupFileExists = mBackupFile.exists();
if (DEBUG) {
backupExistsTime = System.currentTimeMillis();
}
if (!backupFileExists) {
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false, 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.
// 当尝试写入文件时,删除备份文件,并返回true。如果在写入过程中发生了异常,则删除新的文件,下一次从备份文件中恢复。
try {
FileOutputStream str = createFileOutputStream(mFile);
if (DEBUG) {
outputStreamCreateTime = System.currentTimeMillis();
}
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
writeTime = System.currentTimeMillis();
FileUtils.sync(str);
fsyncTime = System.currentTimeMillis();
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
if (DEBUG) {
setPermTime = System.currentTimeMillis();
}
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (mLock) {
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
if (DEBUG) {
fstatTime = System.currentTimeMillis();
}
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
if (DEBUG) {
deleteTime = System.currentTimeMillis();
}
mDiskStateGeneration = mcr.memoryStateGeneration;
//记录写入成功了,唤醒等待写入结果的线程
mcr.setDiskWriteResult(true, true);
if (DEBUG) {
Log.d(TAG, "write: " + (existsTime - startTime) + "/"
+ (backupExistsTime - startTime) + "/"
+ (outputStreamCreateTime - startTime) + "/"
+ (writeTime - startTime) + "/"
+ (fsyncTime - startTime) + "/"
+ (setPermTime - startTime) + "/"
+ (fstatTime - startTime) + "/"
+ (deleteTime - startTime));
}
long fsyncDuration = fsyncTime - writeTime;
mSyncTimes.add((int) fsyncDuration);
mNumSync++;
if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
}
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, false);
}
在将writeToFile()方法中,首先将当前文件重命名为备份文件,然后从当前文件中获取文件输出流,并将MemoryCommitResult中保存的备份数据,写入到文件输出流中。如果写入成功,则删除备份文件,返回真。如果写入失败,则删除当前文件,下一次从备份文件中恢复过来。
通知调用者是否写入成功是通过setDiskWriteResult()方法来完成的,在该方法中,通过MemoryCommitResult的writeToDiskResult变量来保存写入结果,写入成功为真,写入失败为假。不管写入成功还是失败,都会让writtenToDiskLatch闭锁计数减1,唤醒在闭锁上等待的线程。
因此,整个commit操作可以概括为:
1.创建写入数据
2.如果当前没有写入操作,则通过线程直接写入,如果有则添加到单任务队列中等待
3.保存写入结果,并唤醒等待写入的线程
4.通知所有的监听者写入完成
5.返回给调用这写入结果
apply操作
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);
}
};
//与commit方法不同,通过postWriteRunnable异步等待
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);
}
综上:commit同步写入,有返回值,但是会造成调用它的线程阻塞,apply异步写入,无返回值!
四.Sp滥用带来的性能问题
1.第一次读取时,造成主线程阻塞(文件过大,主线程等待时间过长),引起ANR
2.文件太大,加载到内存中一直存在,占用大量内存
3.直接在主线程进行commit操作,造成阻塞,引发ANR
4.多次apply,造成锁竞争,浪费系统资源
5.Sp文件过大,每次更新都更新整个文件
五.Sp最佳实践方案
1.对Sp进行合适的拆分
2.在合适的时机进行异步初始化
3.批量修改一次提交
4.在主线程谨慎使用commit
SharedPreferences作为一种轻量级的数据存储方式,极大的方便了我们的操作!但请一定谨慎的使用!