SharedPreferences 源码分析及使用事项
作为Android 轻量级的存储工具,SharedPreferences被广泛使用,API 简洁明了,易学易用,为广大程序小哥哥们喜闻乐见。殊不知,一片和谐的环境下,蕴藏着不少危机,本文将从源码角度进行解析,并附上踩过的一些坑。
一般用法
SharedPreferences pref = mAppContext.getSharedPreferences(prefName, Context.MODE_PRIVATE);
源码分析
Conext#getSharedPreferences的内部实现,具体实现在
ContextImpl.getSharedPreferences()
@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) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);//一个context可以有N多SharedPreferences文件
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);//初始化后,以后从缓存读取file,再构建SharedPreference,mSharedPrefsPaths没有做销毁处理.如果SharedPreferences很多,map会很大,会占用更多内存。
再看getSharedPreferences(file, mode)方法
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);
if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {//8.0新特性,用户加锁时,会抛异常,实测暂未开启
if (isCredentialProtectedStorage()
&& !getSystemService(StorageManager.class).isUserKeyUnlocked(
UserHandle.myUserId())
&& !isBuggy()) {
throw new IllegalStateException("SharedPreferences in credential encrypted "
+ "storage are not available until after user is unlocked");
}
}
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();//SharedPreferencesImpl缓存池
sp = cache.get(file);
if (sp == null) {
sp = new SharedPreferencesImpl(file, mode);//一个name对应一个SharedPreferences,可理解为单例
cache.put(file, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||//跨进程,Google不推荐
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;
}
具体分析SharePerferenceImpl
// Lock ordering rules: //一共3把锁,注意加锁顺序
// - acquire SharedPreferencesImpl.mLock before EditorImpl.mLock
// - acquire mWritingToDiskLock before EditorImpl.mLock
构造器
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);//灾备文件
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();//核心方法
}
private void startLoadFromDisk() {
synchronized (mLock) {//加锁
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
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;
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);
} 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 是该SP文件,所包含的所有Key,Value
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
mLock.notifyAll();//Load完成,发一个notifyAll,表示已经准备好SP文件,阻塞结束
}
}
get方法
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) { //线程安全 如果同时有多个方法,操作SP,这里可能会阻塞,给我们一个启示,优先级高的SP文件,最好单独保存
awaitLoadedLocked();// 对于单进程来说,get方法,应该不会在这里阻塞
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 {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
Put方法
put方法,实际通过EditorImpl 完成,Editor是专为SharedPreferences 私人定制
@GuardedBy("mLock")
private final Map<String, Object> mModified = Maps.newHashMap();
public Editor putString(String key, @Nullable String value) {
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
从上面可以看出,put方法,仅仅是把key,value存入内存mModified,并没有保存至磁盘。有点类似于事务处理,必须是最后一步提交事务,才算是正式生效了。
最重要的两个方法
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);
}
这次有出比较要命的地方, QueuedWork.addFinisher(awaitCommit);虽然是异步操作,但也可能会阻塞主线程
要点在ActivityThread handleXXActivity方法中
private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {
ActivityClientRecord r = mActivities.get(token);
if (!checkAndUpdateLifecycleSeq(seq, r, "stopActivity")) {
return;
}
r.activity.mConfigChangeFlags |= configChanges;
StopInfo info = new StopInfo();
performStopActivityInner(r, info, show, true, "handleStopActivity");
if (localLOGV) Slog.v(
TAG, "Finishing stop of " + r + ": show=" + show
+ " win=" + r.window);
updateVisibility(r, show);
// Make sure any pending writes are now committed.
if (!r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
.........
再看 这个 QueuedWork.waitToFinish();
public static void waitToFinish() {
.........
try {
while (true) {
Runnable finisher;
synchronized (sLock) { //加锁,轮询,同步,忐忑不?
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
......
}
从上面能看出来,Activity Service 一些操作,是需要等到SP操作结束的。所以即使是异步的apply操作也是有可能阻塞主线程的。使用要慎重。
再来看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 {
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;
}
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */); 是异步的关键方法
几经辗转,会调用
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);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
总结一下:
1、 apply 方法也有可能阻塞主线程,尽量保证SP操作时间可控,不要存储过大文件
以我们工程为例,一个key,value就有18k之巨,直接导致后台监控到大量的SP相关的卡顿
2、不建议通过SP,实现进程共享,完全质量无保障
3、getXXX方法,本身也有可能阻塞(别的地方也在操作SP),所以高优先级SP文件,建议独立开来
4、apply 异步不需要等待执行结果,但也使用到了线程池,因此尽量合并提交
5、commit 操作,同步操作,除非希望立即获取返回结果,否则尽量使用apply
6、如何解决恐怖的apply 方法阻塞主线程的问题???
有不少开发规范都规定,尽量使用apply方法,除非想理解获得提交状态,否则不要用commit方法。
对于小型SP存储来说,是没有问题的。 但大型项目,通常SP文件品种繁多,且单个SP文件,所包含的各色key,value(单个可能都不大,但好虎架不住狼多),会导致另一个致命问题,ANR。
那么解决之道是什么呢?
答案是为了性能计,弃用apply。
替代方案为:仿照apply源码的做法,需要apply的地方,采用子线程,异步提交commit。这样,既避免了apply方法,影响ActivityThread重要方法问题,也避免了同步操作阻塞主线程。