面试问题001-SharedPreferences线程安全吗?

469 阅读3分钟

首先可以肯定的讲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()方法就不会产生阻塞

SharedPreferences总结

SharedPreferences

参考链接

原生SharedPreferences ANR问题的分析

项目中多次操作SharedPreferences导致ANR场景的解决