提起SharedPreferences
,应该所有的开发者都觉得很简单吧,会不会想:这个有啥好说的,还写博客?是不是装X啊?
其实我也觉得SharedPreferences
很简单,除非你要用它实现多进程之间的数据共享。说到多进程之间使用SharedPreferences
,会不会有人想说:创建它的时候不是有个参数Context.MODE_MULTI_PROCESS
,这不是android官方提供的多进程支持吗?呵呵,少年,too young too simple啊!
Note: currently this class does not support use across multiple processes. This will be added later.
MODE_MULTI_PROCESS does not work reliably in some versions of Android, and furthermore does not provide any mechanism for reconciling concurrent modifications across processes. Applications should not attempt to use it. Instead, they should use an explicit cross-process data management approach such asContentProvider.
以上摘自android官方文档SharedPreferences和Context介绍部分
抛开多进程之间使用,在自己的App中使用SharedPreferences
还是很简单的。当然这篇文章我不是说开发中如何使用它,而是记录自己在优化我们的App启动过程中发现的一些问题--SharedPreferences
导致的问题。
进入正题
引子
SharedPreferences
是Android SDK提供的工具,可以存储应用的一些配置信息,这些信息会以键值对的形式保存在/sdcard/data/data/packageName/shared_prefs/
路径下的一个xml
文件中。它提供了多种数据类型的存储,包括:int
、long
、boolean
、float
、String
以及Set<String>
。
我真正想记录的
获取SharedPreferences
的实例一般会有两种方式:
context.getSharedPreference(name, mode);
-
PreferenceManager.getDefaultSharedPreferences(context);
这两种方式其实具体实现是一样的,只不过一个是开发者自己定义名字,另一个是使用包名+"_preference"作为存储文件名。
getSharedPreference(name, mode)
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
PreferenceManager.getDefaultSharedPreferences(context)
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return
context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
private static String getDefaultSharedPreferencesName(Context context) {
return context.getPackageName() + "_preferences";
}
private static int getDefaultSharedPreferencesMode() {
return Context.MODE_PRIVATE;
}
我们的App中充斥着大量的PreferenceManager.getDefaultSharedPreferences(context)
使用方式,可能是大家觉得方便,又是API级的Manager管理,所以使用起来很放心,然而这也正是问题的根本所在。问题在哪里呢?接下来便告诉你。
因为在做App的优化,自然少不了使用MethodTrace,在分析启动流程时发现一个很诡异的点,那就是一个读取配置操作竟然耗时400+ms的时间(有图为证),
我当时就被震惊了,这里竟然耗时这么久!作为一个码农,这时必须要看源码呀,可惜通过AS并不能直接看到实现逻辑,不过通过Google还是可以轻松找到的,有兴趣的看这里。
SharedPreferences
是一个抽象类,所以具体实现的逻辑便在其子类SharedPreferencesImpl
中,先看下它的构造方法:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();//可以看到,当你使用时,这里变开始从sdcard读取xml文件
}
startLoadFromDisk()
具体实现代码如下:
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}
private void loadFromDiskLocked() {
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 (XmlPullParserException e) {
Log.w(TAG, "getSharedPreferences", e);
} catch (FileNotFoundException e) {
Log.w(TAG, "getSharedPreferences", e);
} catch (IOException e) {
Log.w(TAG, "getSharedPreferences", e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
}
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<String, Object>();
}
notifyAll();
}
再看看get方法:
public String getString(String key, String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
从代码可以看到,在get之前是要调用awaitLoadedLocked ()
方法的,那这个方法具体做了什么呢?看代码:
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();
} catch (InterruptedException unused) {
}
}
}
也许你已经看出来了,在真正get数据之前是要判断文件加载状态的。如果此时没加载出来,那就要等待。这时你有没有理解为什么第一次调用要花费400+ms的时间(就是MethodTrace图上显示的时间,虽然MethodTrace方法本身会增加方法的调用时间,但基本上还是可以反应出实际调用时间长短的)了吗?是不是心里在想:你的App中使用大量PreferenceManger获取SharedPreferences对象来存储配置信息,导致该文件变得很大,文件大肯定加载时间要长嘛。Bingo,you get it。前面铺垫那么多,就是为了这一点,文件大自然要等待很久,所以如果你的App对初始化时间敏感,并且你配置文件中存了太多的东西,那么你应该考虑把初始化所需要的配置放在单独的文件中(自然是使用context.getSharedPreferences()
方法,传入单独的名称),而不是所有的配置放一起了。
后记:SharedPreferencesImpl
的实例是有缓存的,cache在ContextImpl
类中,所以在load之后,下次再调用SharedPreferences.getXXX()
方法时你将看不到漫长的wait时间了。
网友评论