SharedPreference原理,commit与apply的区别是什么?使用时需要有哪些注意?
这道题想考察什么?
1、对SharedPreference原理的掌握;
2、项目开发中对使用的技术是否有深入了解,掌握其隐患,而不仅仅只是调API;
考察的知识点
IO、并发编程、数据序列化、数据持久化与性能优化
考生如何回答
SharedPreferences
作为Android系统的轻量级数据存储方式之一,能够比较方便的存取一些简单的Key-Value数据。先来对SP按照使用流程进行梳理:
SharedPreferences
保存的文件为XML数据,其内容如下:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="key1">value1</string>
<int name="key2">value2</string>
<!-- ...... -->
</map>
初始化
首先SharedPreference
本身是一个接口,在Android当中的实现类为:android.app.SharedPreferencesImpl
。context.getSharedPreferences(String name, int mode)
获得一个SP实例的时候,会以传入的name
作为文件名,创建/data/data/<程序包名>/shared_prefs/{name}.xml 对应的File对象,同时以此File对象,执行SharedPreferencesImpl
构造方法并且将加入到缓存中。
class ContextImpl extends Context {
//使用全局的静态Map对象来保存SharedPreferencesImpl的所有实现
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
//文件名和文件存储的Map对象
private ArrayMap<String, File> mSharedPrefsPaths;
//...省略代码
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
//...省略代码
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
//如果缓存没有,创建
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
//同样的如果缓存中找不到sp,那么创建一个
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.
//如果小于3.0版本或者多进程,将重新加载刷新数据
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
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;
}
//...省略代码
}
而SharedPreferencesImpl
的构造方法为:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);//备份文件,用于备份和恢复内容
mMode = mode;
mLoaded = false;
mMap = null;//内存缓存
startLoadFromDisk();//从磁盘加载存储的内容
}
构造方法里面创建了mMap
的内存缓存,后面存放及获取值的时候都是从这个缓存里面维护的, startLoadFromDisk()
就是从文件读取存储内容赋值给mMap
的,看看其实现:
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();//在新的线程中进行内容的读取
}
}.start();
}
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;//如果正在加载就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;
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);//xml解析并赋值给map
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
/* ignore */
}
synchronized (mLock) {
mLoaded = true;
if (map != null) {
mMap = map;//将内容再次赋值给mMap对象,这样mMap将负责在内存中的数据维护
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {//如果没有内容的话,就直接创建一个Map对象
mMap = new HashMap<>();
}
mLock.notifyAll();
}
}
所以SP在初始化时,其实就是进行利用IO对一个XML文件进行读取,IO操作本身时耗时操作(为什么IO是耗时操作?),因此需要开启子线程。在子线程中需要对读取到的内容进行XML解析,将解析到的数据保存在Map集合当中。
读取数据
在初始化完成之后,获得了mMap集合,读取数据则可以直接从此map集合中获取,以获取Int型数据为例:
@Override
public int getInt(String key, int defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}
@GuardedBy("mLock")
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);
}
}
因为初始化是在IO子线程中进行,因此我们需要考虑多线程并发问题,在getInt
与初始化时均有使用同一对象:mLock
进行了加锁的操作。并且我们注意到在IO子线程中,读取文件代码时未加锁,为了防止在其他线程中调用getInt
获取值时进入synchronized
代码块,但是mMap还没有赋值,在synchronized
代码块中还进行了wait()
等待。那么此处如若SP在进行初始化时,我们在主线程读取数据,读取数据需要等待sp的初始化完成。因此我们应该避免使用SP保存复杂、大量的数据。
写入数据
保存新数据或者更新原本的Key需要使用SharedPreferences.edit()
:可以返回一个Editor
对象,而Editor
的接口实现类为EditorImpl
。
@Override
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 (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
此处我们需要小心的是避免频繁的调用edit()
方法,因为每次调用都会产生一个EditorImpl
对象,存在内存抖动的风险。
在获得Editor对象后,使用putXX
方法进行新数据的写入:
@Override
public Editor putInt(String key, int value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
在写入时,putXX
仅仅只是在内存中使用Map集合mModified
记录新数据。最终需要将数据持久化到文件中,需要执行commit或者apply方法。
commit提交
public boolean commit() {
//将内容先提交到mMap中
MemoryCommitResult mcr = commitToMemory();
//之后再写入文件
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
//等待写入完成
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
//...省略代码
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
apply提交
public void apply() {
final long startTime = System.currentTimeMillis();
//将数据同步到内存缓存
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
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() {
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);
}
从源码中能够发现,其实无论是 SP 的 commit 还是 apply 最终都会调用 enqueueDiskWrite
方法,区别是 commit 方法调用传递的第二个参数为 null。enqueueDiskWrite
方法内部也是根据第二个参数来区分 commit 和 apply 的,如果是 commit 则会同步的执行 writeToFile,apply 则会将 writeToFile 加入到一个任务队列中异步的执行:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//根据postWriteRunnable参数来判断是同步提交还是异步提交
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
//这是真正的写入文件的方法
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//直接在当前线程运行同步执行 writeToFile
writeToDiskRunnable.run();
return;
}
}
//将 writeToFile 加入到一个任务队列中异步的执行
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
writeToFile 执行完成会释放等待锁,之后会回调传递进来的第二个参数 Runnable 的 run 方法,并将 QueuedWork
中的这个等待任务移除。
SP 调用 apply 方法,会创建一个等待锁放到 QueuedWork(QueuedWork.addFinisher(awaitCommit))
中,并将真正数据持久化封装成一个任务放到异步队列中执行,任务执行结束会释放锁。而Activity onPuase、onStop 以及 Service 处理 onStop,onStartCommand 等情况下,就会执行 QueuedWork.waitToFinish() 等待所有的等待锁释放。 因此apply 调用次数过多也会容易引起 ANR 问题。
//ActivityThread
@Override
public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
int configChanges, PendingTransactionActions pendingActions, String reason) {
ActivityClientRecord r = mActivities.get(token);
if (r != null) {
if (userLeaving) {
performUserLeavingActivity(r);
}
r.activity.mConfigChangeFlags |= configChanges;
performPauseActivity(r, finished, reason, pendingActions);
// Make sure any pending writes are now committed.
if (r.isPreHoneycomb()) {
//sp中的apply未完成,则会有awaitCommit在QueuedWork中
QueuedWork.waitToFinish();
}
mSomeActivitiesChanged = true;
}
}
commit与apply的区别
- apply没有返回值,因此无法获知提交结果,而commit返回boolean表明修改是否提交成功;
- apply使用异步真正提交到文件, 而commit是同步的提交,因此需要等待正在处理的commit保存到文件后才会执行后续的代码。
使用注意事项
1、getXXX() 方法可能会导致主线程阻塞
根据上文中初始化得知,getXXX需要等待初始化完成,因此如果SP数据量过多,需要更多的时间,此时调用getXXX会等待初始化完成。
2、SP 不能保证类型安全
因为使用相同的 key 进行操作的时候,putXXX 方法可以使用不同类型的数据覆盖掉相同的 key。
3、SP 加载的数据会一直留在内存中
初始化时缓存在mMap中,占用内存。
4、避免频繁调用editor与commit方法
每次editor都会创建EditorImpl对象,而每次commit则会将所有数据进行序列化并且使用IO(写文件)。
5、apply方法也可能ANR
因为apply会调用QueuedWork.addFinisher(awaitCommit),在Activity执行onStop时会需要等待apply完成执行完awaitCommit才能继续往后执行。
最后
有需要面试题的朋友可以关注一下哇哇,以上都可以分享!!!
网友评论