SharedPreferences源码分析

一、SharedPreference简介

SharedPreference是Android系统提供的轻量级数据存储方案,常被简称为SP。采用key-value的数据存储方式,数据存储媒介是XML文件。用于存储App的配置、账号等轻量级数据信息,是常用的外部存储数据方案。

二、SharedPreference用法

SharedPreference用法比较简单,包括读取、和写入两种用法。

1)写入

SharedPreference myPreference=getSharedPreferences("config", Context.MODE_PRIVATE);//获取SharedPreferences实例
Editor editor = myPreference.edit();//获取Edit
 //put值
 editor.putString("string", "string");
 editor.putInt("key", 0);
 editor.putBoolean("boolean", true);
 //提交数据
 editor.commit();// 或者editor.apply()
复制代码

commit和apply这两种方式都可以进行提交,区别在于commit是同步提交并且有返回值,apply是异步提交且无返回值。

2)读取

SharedPreference myPreference=getSharedPreferences("config", Context.MODE_PRIVATE);
String name=myPreference.getString("string", "default");
int age=myPreference.getInt("key", 0);//第二个参数是默认值
复制代码

三、源码分析

1)SharedPreferences接口,这个接口已经定义了所有关于SP读写操作的方法,写操作通过内部接口Editor定义,核心代码如下:

public interface SharedPreferences {
    public interface OnSharedPreferenceChangeListener {
        void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }
    public interface Editor {
        Editor putString(String key, @Nullable String value);
        ........
        Editor putBoolean(String key, boolean value);
        Editor remove(String key);
        Editor clear();
        boolean commit();
        void apply();
    }
    Map<String, ?> getAll();
    String getString(String key, @Nullable String defValue);
   .......
    boolean getBoolean(String key, boolean defValue);
    boolean contains(String key);
    Editor edit();
    void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
    void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
}
复制代码

2)实现类SharedPreferencesImpl,SharedPreferences接口中只是定义方法结构,功能实现由实现类SharedPreferencesImpl来完成。核心代码如下:

final class SharedPreferencesImpl implements SharedPreferences {
  .......
  private final File mFile;
  private final int mMode;
  private final Object mLock = new Object();
  private final Object mWritingToDiskLock = new Object();
  @GuardedBy("mLock")
  private Map<String, Object> mMap;
  @GuardedBy("mLock")
  private boolean mLoaded = false;
  ......
  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);
          }
   }
   ......
      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) {
      } catch (Throwable t) {
          thrown = t;
      }
      synchronized (mLock) {
          mLoaded = true;
          mThrowable = thrown;
          try {
              if (thrown == null) {
                  if (map != null) {
                      mMap = map;
                      mStatTimestamp = stat.st_mtim;
                      mStatSize = stat.st_size;
                  } else {
                      mMap = new HashMap<>();
                  }
              }
          } catch (Throwable t) {
              mThrowable = t;
          } finally {
              mLock.notifyAll();
          }
      }
  }

  void startReloadIfChangedUnexpectedly() {
      synchronized (mLock) {
          // TODO: wait for any pending writes to disk?
          if (!hasFileChangedUnexpectedly()) {
              return;
          }
          startLoadFromDisk();
      }
  }

  @GuardedBy("mLock")
  private void awaitLoadedLocked() {
      if (!mLoaded) {
          BlockGuard.getThreadPolicy().onReadFromDisk();
      }
      while (!mLoaded) {
          try {
              mLock.wait();
          } catch (InterruptedException unused) {
          }
      }
      if (mThrowable != null) {
          throw new IllegalStateException(mThrowable);
      }
  }

  @Override
  @Nullable
  public String getString(String key, @Nullable String defValue) {
      synchronized (mLock) {
          awaitLoadedLocked();
          String v = (String)mMap.get(key);
          return v != null ? v : defValue;
      }
  }
......
  @Override
  public boolean getBoolean(String key, boolean defValue) {
      synchronized (mLock) {
          awaitLoadedLocked();
          Boolean v = (Boolean)mMap.get(key);
          return v != null ? v : defValue;
      }
  }

......
  @Override
  public Editor edit() {
      synchronized (mLock) {
          awaitLoadedLocked();
      }
      return new EditorImpl();
  }
......
}
复制代码

1.先来分析sp的读取操作,从startLoadFromDisk方法中可以看到,读取数据开启了一个新的线程,同时通过lock对象加锁来保证同步。通过loadFromDisk方法从xml中解析数据,数据解析后存储在Map数据集合中,解析方式是xml的pull解析。当解析结束之后释放锁。

2.接下来分析根据key获取sp中存储值的操作,获取不同的数据类型操作其实都是一样的,这里以getString为例,通过代码可以发现首先会通过等待加载完成锁来判断是否加载完成,如果数据已经从xml文件中异步解析完成,则从Map集合中读取数据。

从读取数据中可以发现一点,xml中的数据是一次性读入map中,并且通常实在SharedPreferencesImpl构造中进行的,这里就发现了sp的缓存策略。读取数据的时候不是每读取一次就解析一次xml文件,只有第一次读取时候解析xml文件,之后都是读取缓存的数据。这也说明为什么官方建议sp中不要存储大量数据,数据量大了会导致xml解析性能下降,第一次读取的时候花费时间过长。

3)Editor实现类EditorImpl核心代码如下。

public final class EditorImpl implements Editor {
       private final Object mEditorLock = new Object();
       @GuardedBy("mEditorLock")
       private final Map<String, Object> mModified = new HashMap<>();
       @GuardedBy("mEditorLock")
       private boolean mClear = false;

       @Override
       public Editor putString(String key, @Nullable String value) {
           synchronized (mEditorLock) {
               mModified.put(key, value);
               return this;
           }
       }
      ......
       @Override
       public Editor putBoolean(String key, boolean value) {
           synchronized (mEditorLock) {
               mModified.put(key, value);
               return this;
           }
       }

       @Override
       public Editor remove(String key) {
           synchronized (mEditorLock) {
               mModified.put(key, this);
               return this;
           }
       }

       @Override
       public Editor clear() {
           synchronized (mEditorLock) {
               mClear = true;
               return this;
           }
       }

       @Override
       public void apply() {
           final long startTime = System.currentTimeMillis();
           final MemoryCommitResult mcr = commitToMemory();
           final Runnable awaitCommit = new Runnable() {
                   @Override
                   public void run() {
                       try {
                           mcr.writtenToDiskLatch.await();
                       } catch (InterruptedException ignored) {
                       }
                   }
               };

           QueuedWork.addFinisher(awaitCommit);
           Runnable postWriteRunnable = new Runnable() {
                   @Override
                   public void run() {
                       awaitCommit.run();
                       QueuedWork.removeFinisher(awaitCommit);
                   }
               };
           SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
           notifyListeners(mcr);
       }

       private MemoryCommitResult commitToMemory() {
           long memoryStateGeneration;
           List<String> keysModified = null;
           Set<OnSharedPreferenceChangeListener> listeners = null;
           Map<String, Object> mapToWriteToDisk;

           synchronized (SharedPreferencesImpl.this.mLock) {
             
               if (mDiskWritesInFlight > 0) {
                   mMap = new HashMap<String, Object>(mMap);
               }
               mapToWriteToDisk = mMap;
               mDiskWritesInFlight++;
               boolean hasListeners = mListeners.size() > 0;
               if (hasListeners) {
                   keysModified = new ArrayList<String>();
                   listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
               }

               synchronized (mEditorLock) {
                   boolean changesMade = false;
                   if (mClear) {
                       if (!mapToWriteToDisk.isEmpty()) {
                           changesMade = true;
                           mapToWriteToDisk.clear();
                       }
                       mClear = false;
                   }

                   for (Map.Entry<String, Object> e : mModified.entrySet()) {
                       String k = e.getKey();
                       Object v = e.getValue();
                       if (v == this || v == null) {
                           if (!mapToWriteToDisk.containsKey(k)) {
                               continue;
                           }
                           mapToWriteToDisk.remove(k);
                       } else {
                           if (mapToWriteToDisk.containsKey(k)) {
                               Object existingValue = mapToWriteToDisk.get(k);
                               if (existingValue != null && existingValue.equals(v)) {
                                   continue;
                               }
                           }
                           mapToWriteToDisk.put(k, v);
                       }
                       changesMade = true;
                       if (hasListeners) {
                           keysModified.add(k);
                       }
                   }
                   mModified.clear();
                   if (changesMade) {
                       mCurrentMemoryStateGeneration++;
                   }
                   memoryStateGeneration = mCurrentMemoryStateGeneration;
               }
           }
           return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                   mapToWriteToDisk);
       }

       @Override
       public boolean commit() {
           long startTime = 0;
           MemoryCommitResult mcr = commitToMemory();
           SharedPreferencesImpl.this.enqueueDiskWrite(
               mcr, null /* sync write on this thread okay */);
           try {
               mcr.writtenToDiskLatch.await();
           } catch (InterruptedException e) {
               return false;
           }
           notifyListeners(mcr);
           return mcr.writeToDiskResult;
       }
       private void notifyListeners(final MemoryCommitResult mcr) {
           if (mcr.listeners == null || mcr.keysModified == null ||
               mcr.keysModified.size() == 0) {
               return;
           }
           if (Looper.myLooper() == Looper.getMainLooper()) {
               for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
                   final String key = mcr.keysModified.get(i);
                   for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
                       if (listener != null) {
                           listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);
                       }
                   }
               }
           } else {
               // Run this function on the main thread.
               ActivityThread.sMainThreadHandler.post(() -> notifyListeners(mcr));
           }
       }
   }
   private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                 final Runnable postWriteRunnable) {
       final boolean isFromSyncCommit = (postWriteRunnable == null);
       final Runnable writeToDiskRunnable = new Runnable() {
               @Override
               public void run() {
                   synchronized (mWritingToDiskLock) {
                       writeToFile(mcr, isFromSyncCommit);
                   }
                   synchronized (mLock) {
                       mDiskWritesInFlight--;
                   }
                   if (postWriteRunnable != null) {
                       postWriteRunnable.run();
                   }
               }
           };

       if (isFromSyncCommit) {
           boolean wasEmpty = false;
           synchronized (mLock) {
               wasEmpty = mDiskWritesInFlight == 1;
           }
           if (wasEmpty) {
               writeToDiskRunnable.run();
               return;
           }
       }
       QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
   }
复制代码

EditorImpl类是用来完成sp的存值操作,在所有的put方法中都通过mModified的Map集合来维护修改的数据,存储上主要来分析commit和apply操作。

1.commit是进行同步存储,方法中直接通过commitToMemory方法将数据写入map集合中,然后enqueueDiskWrite同步写入xml中。

2.apply是进行异步存储,也是先通过commitToMemory方法将数据写入map集合中,然后通过enqueueDiskWrite方法异步写入xml中。

接下来分析如何将数据保存到内存和如何同步和异步完成数据写入xml,从commitToMemory方法中看到会对mModified集合进行遍历将mapToWriteToDisk集合(也就是mMap集合,因为指向统一内存地址)数据进行修改。执行结束后对mModified集合进行清空,最后返回MemoryCommitResult,写入XML文件需要这个MemoryCommitResult结果。

enqueueDiskWrite方法就比较简单了,apply和commit方法会传入enqueueDiskWrite方法一个Runnable,通过这个Runnable方法判断是同步还是异步,传入的是null则是同步,否则则是异步。同步则立刻执行写入文件,异步则通过QueuedWork进行异步调度写入。QueuedWork异步调度的原理是通过HandlerThread来完成,这里不进行分析。

四、总结

SharedPreferences是我们开发中常用的轻量级数据本地存储,小容量app配置数据可以用sp来存储,如果存储的key-value对过多,建议拆分sp,不要都存入一个sp中,会降低首次读取的性能。写入数据时如果不需要返回值则用apply进行提交,异步来将数据写入xml文件。

转载于:https://juejin.im/post/5ce371ed51882533135c0649

    原文作者:weixin_34406796
    原文地址: https://blog.csdn.net/weixin_34406796/article/details/91430787
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞