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文件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335