首先可以肯定的讲SharedPreferemces是线程安全的,其底层是通过synchronized关键词锁类对象实现的,那么具体的实现细节是怎样的呢?我们一起来看下
SharedPreferences的获取
SharedPreferences sharedPreferences = context.getSharedPreferences("hello",MODE_PRIVATE);
Android开发的小伙伴都知道,我们可以通过如下代码获取SharedPreferences对象,通过源码,我们可以看到SharedPreferences.java实际上是一个接口,通过Context.getSharedPreferences我们会拿到这个接口的实例对象,那么在系统中又是怎么管理SharedPreferences接口的实例对象的呢?
查看API 32 ContextImpl中getSharedPreferences源码如下:
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// 在Android 4.4以下,修正SharedPreference文件名称
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
// 从缓存中查找指定name的文件
file = mSharedPrefsPaths.get(name);
if (file == null) {
// 根据指定name在指定目录新建name.xml文件
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
// 获取文件对应的SharedPreferences接口的实例对象
return getSharedPreferences(file, mode);
}
下面我们分开来看新建name.xml文件和获取文件对应的SharedPreferences接口的实例对象的过程。
getSharedPreferencesPath新建name.xml文件
getSharedPreferencesPath实现代码如下所示:
@Override
public File getSharedPreferencesPath(String name) {
// 从这里可以看出SharedPreferences最终的数据存储形式为xml文件
return makeFilename(getPreferencesDir(), name + ".xml");
}
@UnsupportedAppUsage
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
// getDataDir返回的是data/data/包名的应用私有目录
mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
// ensurePrivateDirExists是确保目录可见的检查,经过这一步操作
// 创建了data/data/应用包名/shared_prefs这个目录
return ensurePrivateDirExists(mPreferencesDir);
}
}
private File makeFilename(File base, String name) {
// 如果文件名中不包含路径分隔符,则构造路径对应的file对象
// 否则抛出异常,注意这里并没有创建xml文件
if (name.indexOf(File.separatorChar) < 0) {
final File res = new File(base, name);
// We report as filesystem access here to give us the best shot at
// detecting apps that will pass the path down to native code.
BlockGuard.getVmPolicy().onPathAccess(res.getPath());
return res;
}
throw new IllegalArgumentException(
"File " + name + " contains a path separator");
}
结合上述代码和注释,我们已知如下几点:
- SharedPreferences底层使用xml文件存储数据
- SharedPreferences文件存储在data/data/应用包名/shared_prefs/目录下,文件名称为开发者指定的字符串
getSharedPreferences获取文件对应的SharedPreferences接口的实例对象
getSharedPreferences代码如下所示
// 全局静态变量,按包名缓存SharedPreferences对象
@GuardedBy("ContextImpl.class")
@UnsupportedAppUsage
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
// synchronized类级锁
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
// 尝试从缓存换取SharedPreferences接口的实现类对象
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");
}
}
// 创建SharedPreferences接口的实现类对象并缓存
// 由于java是引用传递,所以通过cache.put直接修改了sSharedPrefsCache中的数据
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) {
sp.startReloadIfChangedUnexpectedly();
}
// 1.从这里返回sp对象可以看出,SharedPreferences接口的实现类为SharedPreferencesImpl
return sp;
}
@GuardedBy("ContextImpl.class")
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
// 按包名获取当前已缓存的SharedPreferenceImpl对象
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
结合代码和注释可以看出:
- 针对同文件的SharedPreferences接口的实现类对象而言,在创建后会缓存下来,由于使用static变量进行缓存,故缓存在常量池中,全局唯一,进而针对指定进程和指定的SharedPreferences文件而言,SharedPreferenceImpl实例对象全局单例
SharedPreferences的数据修改和写入
由前文可知我们通过getSharedPreferences获取到的是SharedPreferencesImpl类的对象,而我们一般情况下通过如下代码进行SharedPreferences的写入操作:
SharedPreferences sharedPreferences = getSharedPreferences("hello",MODE_PRIVATE);
// 写入方式1
sharedPreferences.edit().putBoolean("boolean_value",true).commit();
// 写入方式2
sharedPreferences.edit().putBoolean("boolean_value",true).apply();
基本了解SharedPreferences的同学都知道两种写入方式的区别:
| 写入方式 | 数据提交位置 | 写入文件时机 | 执行操作的线程 |
|---|---|---|---|
| commit | 提交到内存,立即触发写入文件 | 立即写入 | 调用commit的线程 |
| apply | 先提交到内存,系统空闲或特定时机写入文件 | 延时写入 | 名为queued-work-looper的HandlerThread线程 |
接下来我们来分别看下获取Editor以及putBoolean,commit,apply的实现原理。
SharedPreferences.edit()
sharedPreferences.edit()返回的是Editor接口的实现类实例对象,通过代码可知,Editor接口的实现类为EditorImpl,是SharedPrefrencesImpl类的内部类,edit()函数如下所示:
@Override
public Editor edit() {
// 同步锁,先进行数据同步,确保已有的SharedPreferences
// 内容已从文件读取到内存,正常情况下创建SharedPreferencesImpl时就已经
synchronized (mLock) {
awaitLoadedLocked();
}
// 返回Editor接口的实现类实例对象
return new EditorImpl();
}
EditorImpl.putBoolean
EditorImpl.putXXX代码基本相似,这里以putBoolean为例进行说明,EditorImpl.putBoolean代码如下所示:
@Override
public Editor putBoolean(String key, boolean value) {
synchronized (mEditorLock) {
// 将修改的数据和值缓存到mModified这个Map中
mModified.put(key, value);
return this;
}
}
可以看出,在写入数据的代码块中使用了同步对象锁,所以说SharedPreferences时多线程安全的。
EditorImpl.commit
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
// 将修改内容提交到内存缓存
MemoryCommitResult mcr = commitToMemory();
// 将缓存内容同步到xml文件
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
// 等待写入完成后被唤醒,使用CountDownLatch实现
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) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
// 内存缓存写入xml文件的runnable
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
// writeFile完成后,会执行CountDown操作
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// commit时传入的runnable为null,所以在当前线程直接写入xml文件,执行if条件块中代码
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
// apply时将写入的操作放入HandlerThread中排队
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
结合代码和注释可以看出,当调用commit时,文件写入操作在当前线程执行。
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);mcr.writtenToDiskLatch的声明如上所示,可以看出,这里是使用CountDownLatch来实现等待文件写入完成的操作的,更多CountDownLatch的操作可以参考线程间通信方式2一文中关于CountDownLatch的介绍。
EditorImpl.apply
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
// 将修改内容提交到内存缓存
final MemoryCommitResult mcr = commitToMemory();
// 等待写入完成的runnable
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);
}
};
// 将缓存内容同步到xml文件
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
结合上文中enqueueDiskWrite的说明和apply函数的注释可以看出,在apply操作时,文件写入操作在QueuedWork中指定的HandlerThread中执行,从这里也可以看出,在apply和commit时会调用mcr.writtenToDiskLatch.await();等待锁,直到写入文件完成。
在Android8以前,QueuedWork使用SingleThreadExecutor实现,在ActivityThread中和组件生命周期相关的方法中,都需要等待writeToFinish完成才能正常回调,进而拉大了发生ANR的概率,几个典型的执行writeToFinish的场景如下:
- handleServiceArgs
- handleStopService
- handlePauseActivity
- handleStopActivity
- handleSleeping
ANR形成的主要原因是apply执行时间过长,导致主线程发起的commit操作等待超时,进而ANR。
在Android 8以后将QueuedWork中的线程改为HandlerThread实现,针对UI线程调用时,将任务提交给HandlerThread执行,直接返回结果,降低发生ANR可能性。
在Android 8 以前遇到SharedPreference writeToFinish相关的ANR问题,可以考虑代理应用内的ShapredPreferences写入操作,不管是commit还是apply,都在自己管理的子线程直接进行commit,或者实现一个自定义的ConcurrentLinkedQueue,重写poll()方法强制返回null,然后动态代理掉QueuedWork中的sPendingWorkFinishers,waitToFinish()方法就不会产生阻塞