SharedPreferences源碼分析

一、SharedPreference簡介
SharedPreference是Android系統(tǒng)提供的輕量級數(shù)據(jù)存儲方案,常被簡稱為SP久窟。采用key-value的數(shù)據(jù)存儲方式,數(shù)據(jù)存儲媒介是XML文件本缠。用于存儲App的配置斥扛、賬號等輕量級數(shù)據(jù)信息,是常用的外部存儲數(shù)據(jù)方案。

二稀颁、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);
 //提交數(shù)據(jù)
 editor.commit();// 或者editor.apply()

commit和apply這兩種方式都可以進行提交棱烂,區(qū)別在于commit是同步提交并且有返回值,apply是異步提交且無返回值阶女。

2)讀取

SharedPreference myPreference=getSharedPreferences("config", Context.MODE_PRIVATE);
String name=myPreference.getString("string", "default");
int age=myPreference.getInt("key", 0);//第二個參數(shù)是默認值

三颊糜、源碼分析
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)實現(xiàn)類SharedPreferencesImpl,SharedPreferences接口中只是定義方法結構憔杨,功能實現(xiàn)由實現(xiàn)類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方法中可以看到芍秆,讀取數(shù)據(jù)開啟了一個新的線程惯疙,同時通過lock對象加鎖來保證同步。通過loadFromDisk方法從xml中解析數(shù)據(jù)妖啥,數(shù)據(jù)解析后存儲在Map數(shù)據(jù)集合中,解析方式是xml的pull解析对碌。當解析結束之后釋放鎖荆虱。
 2.接下來分析根據(jù)key獲取sp中存儲值的操作,獲取不同的數(shù)據(jù)類型操作其實都是一樣的朽们,這里以getString為例怀读,通過代碼可以發(fā)現(xiàn)首先會通過等待加載完成鎖來判斷是否加載完成,如果數(shù)據(jù)已經從xml文件中異步解析完成骑脱,則從Map集合中讀取數(shù)據(jù)菜枷。
 從讀取數(shù)據(jù)中可以發(fā)現(xiàn)一點,xml中的數(shù)據(jù)是一次性讀入map中叁丧,并且通常實在SharedPreferencesImpl構造中進行的啤誊,這里就發(fā)現(xiàn)了sp的緩存策略。讀取數(shù)據(jù)的時候不是每讀取一次就解析一次xml文件拥娄,只有第一次讀取時候解析xml文件蚊锹,之后都是讀取緩存的數(shù)據(jù)。這也說明為什么官方建議sp中不要存儲大量數(shù)據(jù)稚瘾,數(shù)據(jù)量大了會導致xml解析性能下降牡昆,第一次讀取的時候花費時間過長。

3)Editor實現(xiàn)類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集合來維護修改的數(shù)據(jù)柱宦,存儲上主要來分析commit和apply操作。
    1.commit是進行同步存儲播瞳,方法中直接通過commitToMemory方法將數(shù)據(jù)寫入map集合中掸刊,然后enqueueDiskWrite同步寫入xml中。
    2.apply是進行異步存儲狐史,也是先通過commitToMemory方法將數(shù)據(jù)寫入map集合中痒给,然后通過enqueueDiskWrite方法異步寫入xml中。
   接下來分析如何將數(shù)據(jù)保存到內存和如何同步和異步完成數(shù)據(jù)寫入xml骏全,從commitToMemory方法中看到會對mModified集合進行遍歷將mapToWriteToDisk集合(也就是mMap集合苍柏,因為指向統(tǒng)一內存地址)數(shù)據(jù)進行修改。執(zhí)行結束后對mModified集合進行清空姜贡,最后返回MemoryCommitResult试吁,寫入XML文件需要這個MemoryCommitResult結果。
    enqueueDiskWrite方法就比較簡單了楼咳,apply和commit方法會傳入enqueueDiskWrite方法一個Runnable熄捍,通過這個Runnable方法判斷是同步還是異步,傳入的是null則是同步母怜,否則則是異步余耽。同步則立刻執(zhí)行寫入文件,異步則通過QueuedWork進行異步調度寫入苹熏。QueuedWork異步調度的原理是通過HandlerThread來完成碟贾,這里不進行分析。

四轨域、總結
SharedPreferences是我們開發(fā)中常用的輕量級數(shù)據(jù)本地存儲袱耽,小容量app配置數(shù)據(jù)可以用sp來存儲,如果存儲的key-value對過多干发,建議拆分sp朱巨,不要都存入一個sp中,會降低首次讀取的性能枉长。寫入數(shù)據(jù)時如果不需要返回值則用apply進行提交冀续,異步來將數(shù)據(jù)寫入xml文件。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末搀暑,一起剝皮案震驚了整個濱河市沥阳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌自点,老刑警劉巖桐罕,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡功炮,警方通過查閱死者的電腦和手機溅潜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來薪伏,“玉大人滚澜,你說我怎么就攤上這事〖藁常” “怎么了设捐?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長塘淑。 經常有香客問我萝招,道長,這世上最難降的妖魔是什么存捺? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任槐沼,我火速辦了婚禮,結果婚禮上捌治,老公的妹妹穿的比我還像新娘岗钩。我一直安慰自己,他們只是感情好肖油,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布兼吓。 她就那樣靜靜地躺著,像睡著了一般森枪。 火紅的嫁衣襯著肌膚如雪周蹭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天疲恢,我揣著相機與錄音,去河邊找鬼瓷胧。 笑死显拳,一個胖子當著我的面吹牛,可吹牛的內容都是我干的搓萧。 我是一名探鬼主播杂数,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼瘸洛!你這毒婦竟也來了揍移?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤反肋,失蹤者是張志新(化名)和其女友劉穎那伐,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡罕邀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年畅形,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片诉探。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡日熬,死狀恐怖,靈堂內的尸體忽然破棺而出肾胯,到底是詐尸還是另有隱情竖席,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布敬肚,位于F島的核電站毕荐,受9級特大地震影響,放射性物質發(fā)生泄漏帘皿。R本人自食惡果不足惜东跪,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鹰溜。 院中可真熱鬧虽填,春花似錦、人聲如沸曹动。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽墓陈。三九已至恶守,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間贡必,已是汗流浹背兔港。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留仔拟,地道東北人衫樊。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像利花,于是被迫代替她去往敵國和親科侈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353