每日一问:谈谈 SharedPreferences 不为人知的秘密(上)
总算又开启了我们的 每日一问系列,今天想来简单谈谈 SharedPreferences
那些不为人知的细节,让我们看看它到底有多少坑。不过为了追求短平快,这方面的讲解我们会采取两次推文来进行讲解。有任何使用疑问和见解,别忘了在文章下方留言哟,点个在看,让关注你的人看看你在干什么。
SharedPreferences
应该是任何一名 Android 初学者都知道的存储类了,它轻量,适合用于保存软件配置等参数。以键值对的 XML 文件形式存储在本地,程序卸载后也会一并清除,不会残留信息。
使用起来也非常简单。
// 读取
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
val string = sharedPreferences.getString("123","")
// 写入
val editor = sharedPreferences.edit()
editor.putString("123","123")
editor.commit()
当我们写下这样的代码的时候,IDE 极易出现一个警告,提示我们用 apply()
来替换 commit()
。原因也很简单,因为 commit()
是同步的,而 apply()
采用异步的方式通常来说效率会更高一些。但是,当我们把 editor.commit()
的返回值赋给一个变量的时候,这时候就会发现 IDE 没有了警告。这是因为 IDE 认为我们想要使用 editor.commit()
的返回值了,所以,通常来说,在我们不关心操作结果的时候,我们更倾向于使用 apply()
进行写入的操作。
获取 SharedPreferences 实例
我们可以通过 3 种方式来获取 SharedPreferences
的实例。
首先当然是我们最常见的写法。
getSharedPreferences("123", Context.MODE_PRIVATE)
Context
的任意子类都可以直接通过 getSharedPreferences()
方法获取到 SharedPreferences
的实例,接受两个参数,分别对应 XML 文件的名字和操作模式。其中 MODE_WORLD_READABLE
和 MODE_WORLD_WRITEABLE
这两种模式已在 Android 4.2 版本中被废弃。
Context.MODE_PRIVATE: 指定该
SharedPreferences
数据只能被本应用程序读、写;Context.MODE_WORLD_READABLE: 指定该
SharedPreferences
数据能被其他应用程序读,但不能写;Context.MODE_WORLD_WRITEABLE: 指定该
SharedPreferences
数据能被其他应用程序读;Context.MODE_APPEND:该模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件;
另外在 Activity
的实现中,还可以直接通过 getPreferences()
获取,实际上也就把当前 Activity 的类名作为文件名参数。
public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
此外,我们也可以通过 PreferenceManager
的 getDefaultSharedPreferences()
获取到。
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
public static String getDefaultSharedPreferencesName(Context context) {
return context.getPackageName() + "_preferences";
}
private static int getDefaultSharedPreferencesMode() {
return Context.MODE_PRIVATE;
}
可以很明显的看到,这个方式就是在直接把当前应用的包名作为前缀来进行命名的。
注意:如果在 Fragment 中使用
SharedPreferences
时,SharedPreferences
的初始化尽量放在onAttach(Activity activity)
里面进行 ,否则可能会报空指针,即getActivity()
会可能返回为空。
SharedPreferences 源码
有较多 SharedPreferences
使用经验的人,就会发现 SharedPreferences
其实具备挺多的坑,但这些坑主要都是因为不熟悉其中真正的原理所导致的,所以,笔者在这里,带大家一起揭开 SharedPreferences
的神秘面纱。
SharedPreferences 实例获取
前面讲了 SharedPreferences
有三种获取实例的方法,但归根结底都是调用的 Context
的 getSharedPreferences()
方法。由于 Android 的 Context
类采用的是装饰者模式,而装饰者对象其实就是 ContextImpl
,所以我们来看看源码是怎么实现的。
// 存放的是名称和文件夹的映射,实际上这个名称就是我们外面传进来的 name
private ArrayMap<String, File> mSharedPrefsPaths;
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);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File makeFilename(File base, String name) {
if (name.indexOf(File.separatorChar) < 0) {
return new File(base, name);
}
throw new IllegalArgumentException(
"File " + name + " contains a path separator");
}
可以很明显的看到,内部是采用 ArrayMap
来做的处理,而这个 mSharedPrefsPaths
主要是用于存放名称和文件夹的映射,实际上这个名称就是我们外面传进来的 name,这时候我们通过 name 拿到我们的 File,如果当前池子中没有的话,则直接新建一个 File,并放入到 mSharedPrefsPaths
中。最后还是调用的重载方法 getSharedPreferences(File,mode)
// 存放包名与ArrayMap键值对,初始化时会默认以包名作为键值对中的 Key,注意这是个 static 变量
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
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 = 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.
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;
}
可以看到,又采用了一个 ArrayMap
来存放文件和 SharedPreferencesImpl
组成的键值对,然后通过通过单例的方式返回一个 SharedPreferences
对象,实际上是 SharedPreferences
的实现类 SharedPreferencesImpl
,而且在其中还建立了一个内部缓存机制。
所以,从上面的分析中,我们能知道 对于一个相同的 name,我们获取到的都是同一个 SharedPreferencesImpl 对象。
SharedPreferencesImpl
在上面的操作中,我们可以看到在第一次调用 getSharedPreferences
的时候,我们会去构造一个 SharedPreferencesImpl
对象,我们来看看都做了什么。
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = 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<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) {
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();
}
}
}
注意看我们的 startLoadFromDisk
方法,我们会去新开一个子线程,然后去通过 XmlUtils.readMapXml()
方法把指定的 SharedPreferences
文件的所有的键值对都读出来,然后存放到一个 map 中。
而众所周知,文件的读写操作都是耗时的,可想而知,在我们第一次去读取一个 SharedPreferences
文件的时候花上了太多的时间会怎样。
SharedPreferences 的读取操作
上面讲了初次获取一个文件的 SharedPreferences
实例的时候,会先去把所有键值对读取到缓存中,这明显是一个耗时操作,而我们正常的去读取数据的时候,都是类似这样的代码。
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
val string = sharedPreferences.getString("123","")
SharedPreferences
的getXXX()
方法可能会报ClassCastException
异常,所以我们在同一个 name 的时候,对不一样的类型,必须使用不同的 key。但是putXXX
是可以用不同的类型值覆盖相同的 key 的。
那势必可能会导致这个操作需要等待一定的时间,我们姑且可以这么猜想,在 getXXX()
方法执行的时候应该是会等待前面的操作完成才能执行的。
因为 SharedPreferences
是一个接口,所以我们主要来看看它的实现类 SharedPreferencesImpl
,这里以 getString()
为例。
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
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 {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
可以看到,在 awaitLoadedLocked
方法里面我们使用了 mLock.wait()
来等待初始化的读取操作,而我们前面看到的 loadFromDiskLocked()
方法的最后也可以看到它调用了 mLock.notifyAll()
方法来唤醒后面这个阻塞的 getXXX()
。那么这里就会明显出现一个问题,我们的 getXXX()
方法是写在 UI 线程的,如果这个方法被阻塞的太久,势必会出现 ANR 的情况。所以我们一定在平时需要根据具体情况考虑是否需要把 SharedPreferences
的读写操作放在子线程中。
—————END—————
我是南尘,只做比心的公众号,欢迎关注我。
推荐阅读:
欢迎关注南尘的公众号:nanchen
做不完的开源,写不完的矫情,只做比心的公众号,如果你喜欢,你可以选择分享给大家。如果你有好的文章,欢迎投稿,让我们一起来分享。