一、SharePreference介绍
SharePreference是安卓数据持久化保存的方案之一,它以key-value的形式将数据保存在一个xml文件中。一般只保存数据量较小的数据,不适合跨进程调用。
二、SharePreference源码分析
SharePreference使用
SharedPreferences sharedPreferences = getSharedPreferences("test_sp",MODE_PRIVATE);
SharedPreferences.Editor edit = sharedPreferences.edit();
edit.putBoolean("isOpen",true);
edit.commit();//或者使用 edit.apply()
使用getSharedPreferences方法获取SharedPreferences对象。
ContextImpl.java
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// 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";
}
}
File file;
synchronized (ContextImpl.class) {
//mSharedPrefsPaths保存sharepreference文件的map,如果为null则创建
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
//获取name对应的sharepreference文件
file = mSharedPrefsPaths.get(name);
if (file == null) {
//获取的文件为空则调用getSharedPreferencesPath方法创建文件
file = getSharedPreferencesPath(name);
//创建的文件保存在map中
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
看下getSharedPreferences的处理:
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//获取缓存的本应用包名下的所有SharedPreference
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
//根据File获取sp
sp = cache.get(file);
if (sp == null) {
//检查mode,如果targetSdkVersion >= Build.VERSION_CODES.N
// 且 mode == MODE_WORLD_READABLE 或者 mode == MODE_WORLD_WRITEABLE 则抛出异常
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并保存到cache中,new SharedPreferencesImpl会去读取xml中的数据。
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
//多进程使用时会重新加载xml文件,但是不推荐使用 因为一个进程更新了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;
}
SharedPreferencesImpl的构造函数回去读取xml文件中的数据
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
//从硬盘中舰载文件
startLoadFromDisk();
}
这里开启了一个子线程去读取,并设置 mLoaded = false
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
这个方法中就可以看到读取文件和xml文件解析
private void loadFromDisk() {
...
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);
} 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) {
//加载完成标记为设置为true
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();
}
}
}
从上面的代码也可以证明SP只适合存储少量的数据,如果xml保存的数据太多肯定会由于文件加载太慢而造成ANR的风险。SP的get方法就可以验证这个问题,只有xml文件内容加载完成之后才能返回get的结果。
@Override
public int getInt(String key, int defValue) {
synchronized (mLock) {
//等待数据加载完成
awaitLoadedLocked();
Integer v = (Integer)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 {
//主线程等待
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
下面看下看下commit和apply两个提交方法的区别
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;
}
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//commit方法中postWriteRunnable传入的时null
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
//把数据写到xml文件中
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
//isFromSyncCommit == true
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
//mDiskWritesInFlight 在commitToMemory中+1
wasEmpty = mDiskWritesInFlight == 1;
}
//wasEmpty == true
if (wasEmpty) {
//writeToDiskRunnable执行进行写文件
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
...
// 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 (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);
}
apply方法
可以看到apply方法先调用了commitToMemory方法然后调用了enqueueDiskWrite方法
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);
// 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);
}
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//apply方法中postWriteRunnable传入的不等于null
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
//把数据写到xml文件中
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
//isFromSyncCommit == false 不进入下面的方法
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
//mDiskWritesInFlight 在commitToMemory中+1
wasEmpty = mDiskWritesInFlight == 1;
}
//wasEmpty == true
if (wasEmpty) {
//writeToDiskRunnable执行进行写文件
writeToDiskRunnable.run();
return;
}
}
//直接放到队列中执行。
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
//延迟100ms发送
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
看下getHandler方法,可以知道sHandler 消息处理放在了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;
}
}
processPendingWork方法中取出队列中的runable然后调用run方法。
private static class QueuedWorkHandler extends Handler {
static final int MSG_RUN = 1;
QueuedWorkHandler(Looper looper) {
super(looper);
}
public void handleMessage(Message msg) {
if (msg.what == MSG_RUN) {
processPendingWork();
}
}
}
private static void processPendingWork() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {
work = sWork;
sWork = new LinkedList<>();
// Remove all msg-s as all work will be processed now
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
if (DEBUG) {
Log.d(LOG_TAG, "processing " + work.size() + " items took " +
+(System.currentTimeMillis() - startTime) + " ms");
}
}
}
}
commit方法如果提交过大的数据因为在主线程中执行,当cpu使用率过高的时候有可能造成ANR。apply虽然在子线程中执行但是activity启动时会等待QueuedWork中的队列中没有要执行的任务时才会跳转。
总结:
-
SharePreferences 是Android基于xml实现的一种数据持久化手段。
-
SharePreferences 不适合存储过大的数据, 因为所有持久化数据都是一次性加载到内存, 数据过大容易造成内存溢出。
-
SharePreferences 的commit与apply一个是同步一个是异步(大部分场景下),SharePreferences 的 commit 方法是直接在当前线程执行文件写入操作, 而 apply 方法是在工作线程执行文件写入, 尽可能使用apply , 因为不会阻塞当前线程。
-
SharePreferences 并不支持跨进程, 因为它不能保证更新本地数据后被另一个进程所知道,而且跨进程的操作标记已经被弃用。
-
SharePreferences 批量更改数据时,只需要保留最后一个apply即可,避免添加多余的写文件任务。
-
每个SharePreferences 存储的键值对不宜过多, 否则在加载文件数据到内存时会耗时过长, 而阻塞SharePreferences 的相关get或put方法, 造成ui卡顿。
-
commit有相应的返回值,可以知道操作是否成功,apply没有返回值。
网友评论