一、概述
SharedPreferences(簡稱SP)是Android中很常用的數據存儲方式,SP采用key-value(鍵值對)形式,主要用於輕量級的數據存儲,尤其適合保存應用的配置參數,但不建議使用SP來存儲大規模的數據,可能會降低性能。
SP采用xml文件格式來保存數據,改文件所在目錄位於/data/data/shared_prefs/。
二、使用
1.得到SharedPreferences對象
private SharedPreferences mSharedPreferences; private final static String PREFRENCE_FILE_KEY = "com.zhangmiao.shared_preferences"; mSharedPreferences = getSharedPreferences(PREFRENCE_FILE_KEY, MODE_PRIVATE);
2.添加數據
final SharedPreferences.Editor editor = mSharedPreferences.edit(); editor.putInt("id",1); editor.putString("name","小熊"); editor.putInt("age",24); editor.commit();
3.獲取數據
TextView textView = (TextView)findViewById(R.id.text); String message = "id = " + mSharedPreferences.getInt("id",-1) + ",name = " + mSharedPreferences.getString("name","無") + ",age = " + mSharedPreferences.getInt("age",-1)+"。"; textView.setText(message);
4.查看生成的sharedpreferences.xml文件
我使用的是adb命令查看的文件,命令如下:(系統是window10)
adb shell
run-as com.zhangmiao.myapplication(應用包名)
ls(查看xml文件的名稱)
cat com.zhangmiao.shared_preferences.xml(查看xml文件)
三、解析SharedPreferences
1.獲取方式
1.1.getSharedPreferences(String name, int mode)
@Override public SharedPreferences getSharedPreferences(String name, int mode) { return mBase.getSharedPreferences(name, mode); }
這里的mBase就是Context類,Context的具體實現類是ContextImpl類,所以直接查看ContextImpl類中的getSharedPreferences方法。
@Override public SharedPreferences getSharedPreferences(String name, int mode) { // At least one application in the world actually passes in a null // name. This happened to work because when we generated the file name // we would stringify it to "null.xml". Nice. if (mPackageInfo.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT) { if (name == null) { name = "null"; } } File file; synchronized (ContextImpl.class) { if (mSharedPrefsPaths == null) { mSharedPrefsPaths = new ArrayMap<>(); } file = mSharedPrefsPaths.get(name); if (file == null) { file = getSharedPreferencesPath(name);【1.3】 mSharedPrefsPaths.put(name, file); } } return getSharedPreferences(file, mode);【1.2】 }
第一步:判斷版本號是否小於17,如果是,判斷name是否為null,如果是,則設置name為“null”。
第二步:同步ContextImpl.class,如果mSharedPrefsPaths為null,則初始化mSharedPrefsPath,判斷name對應的file是否為null,則調用getSharedPreferencesPath(name)初始化file,並加入mSharedPrefsPaths中。
第三步:返回getSharedPreferences(file, mode)方法。
1.2.public SharedPreferences getSharedPreferences(File file, int mode)
@Override public SharedPreferences getSharedPreferences(File file, int mode) { checkMode(mode);【1.4】 SharedPreferencesImpl sp; synchronized (ContextImpl.class) { final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();【1.5】 sp = cache.get(file); if (sp == null) { sp = new SharedPreferencesImpl(file, mode); cache.put(file, sp); return sp; } } if ((mode & Context.MODE_MULTI_PROCESS) != 0 || getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.HONEYCOMB) { // If somebody else (some other process) changed the prefs // file behind our back, we reload it. This has been the // historical (if undocumented) behavior. sp.startReloadIfChangedUnexpectedly(); } return sp; }
第一步:檢查mode的類型,如果沒有MODE_WORLD_READABLE與MODE_WORLD_WRITEABLE,則拋出異常。
第二步:使用getSharedPreferencesCacheLocked()方法,得到cache數組映射,得到file對應的sp,如果為空,初始化並家務cache中。
第三步:調用sp的startReloadIfChangedUnexpectedly()方法。
1.3.public File getSharedPreferencesPath(String name)
@Override public File getSharedPreferencesPath(String name) { return makeFilename(getPreferencesDir(), name + ".xml"); }
繼續查看makeFileName()方法
private File makeFilename(File base, String name) { if (name.indexOf(File.separatorChar) < 0) { return new File(base, name); } throw new IllegalArgumentException( "File " + name + " contains a path separator"); }
如果name包含文件分隔符則生成xml文件,否則拋出異常。
1.4.private void checkMode(int mode)
private void checkMode(int mode) { if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) { if ((mode & MODE_WORLD_READABLE) != 0) { throw new SecurityException("MODE_WORLD_READABLE no longer supported"); } if ((mode & MODE_WORLD_WRITEABLE) != 0) { throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported"); } } }
如果sdk的版本大於等於24,則mode包含MODE_WORLD_READABLE與MODE_WORLD_WRITEABLE則拋出異常。意思是從版本24開始,創建的SP文件模式,不允許MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE模塊。
當設置MODE_MULTI_PROCESS模式,則每次getSharedPreferences過程,會檢查SP文件上次修改時間和文件大小,一旦所有修改則會重新從磁盤加載文件。
1.5.private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked()
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() { if (sSharedPrefsCache == null) { sSharedPrefsCache = new ArrayMap<>(); } final String packageName = getPackageName(); ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName); if (packagePrefs == null) { packagePrefs = new ArrayMap<>(); sSharedPrefsCache.put(packageName, packagePrefs); } return packagePrefs; }
第一步:如果sSharedPrefsCache為null,則初始化它。
第二步:調用getPackageName()方法得到packageName。
第三步:得到sSharedPrefsCache中packageName對應的ArrayMap對象packagePrefs。
第四步:如果packagePrefs為null,則初始化它,並且添加到sSharedPrefsCache中。
總結:獲取SharedPreference只要實現在ContextImpl中,mSharedPrefsPaths包含name對應的File,sSharedPrefsCache中包含packageName對應的packagePrefs,packagePrefs中包含File對應的SharedPreferencesImpl。
而獲取過程主要時生成File文件,初始化上面的三個ArrayMap,將本應用相關的數據添加進去,以及檢測版本與mode,並拋出異常。
2.添加數據
添加數據使用到SharedPreferences.Editor類,SharedPreferences的實現是在ShredPreferencesImpl中,SharedPreferences.Editor的具體實現在ShredPreferencesImpl.EditorImpl中。
2.1.public Editor edit()
public Editor edit() { // TODO: remove the need to call awaitLoadedLocked() when // requesting an editor. will require some work on the // Editor, but then we should be able to do: // // context.getSharedPreferences(..).edit().putString(..).apply() // // ... all without blocking. synchronized (this) { awaitLoadedLocked();【2.2】 } return new EditorImpl(); }
同步this,調用awaitLoadedLocked()方法,返回EditorImpl對象。
2.2.private void awaitLoadedLocked()
private void awaitLoadedLocked() { if (!mLoaded) { // Raise an explicit StrictMode onReadFromDisk for this // thread, since the real read will be in a different // thread and otherwise ignored by StrictMode. BlockGuard.getThreadPolicy().onReadFromDisk(); } while (!mLoaded) { try { wait(); } catch (InterruptedException unused) { } } }
第一步:如果mLoaded為false,則調用BlockGuard.getThreadPolicy().onReadFromDisk()方法,忽略無用的線程。
第二步:如果mLoaded為false,沒有加載完成,則等待。
2.3.put***(String key, *** value)系列方法
put***()方法基本相同,這里以putInt(String key, int value)為例介紹:
public Editor putInt(String key, int value) { synchronized (this) { mModified.put(key, value); return this; } }
同步this,將數據添加到mModified中,this是Editor對象。
2.4.public Editor remove(String key)
public Editor remove(String key) { synchronized (this) { mModified.put(key, this); return this; } }
同步this,添加到mModified的數據為key與this。
2.5.public Editor clear()
public Editor clear() { synchronized (this) { mClear = true; return this; } }
設置mClear為true。
2.6.public boolean commit()
public boolean commit() { MemoryCommitResult mcr = commitToMemory();【2.7】 SharedPreferencesImpl.this.enqueueDiskWrite(【2.9】 mcr, null /* sync write on this thread okay */); try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) { return false; } notifyListeners(mcr);【2.8】 return mcr.writeToDiskResult; }
第一步:調用commitToMemory()方法,得到mcr對象。
第二步:調用SharedPreferencesImpl的enqueueDiskWrite()方法。
第三步:調用mcr,writtenToDiskLatch.await()方法。
第四步:調用notifyListeners(mcr)方法。
2.7.private MemoryCommitResult commitToMemory()
private MemoryCommitResult commitToMemory() { MemoryCommitResult mcr = new MemoryCommitResult(); synchronized (SharedPreferencesImpl.this) { // We optimistically don't make a deep copy until // a memory commit comes in when we're already // writing to disk. if (mDiskWritesInFlight > 0) { // We can't modify our mMap as a currently // in-flight write owns it. Clone it before // modifying it. // noinspection unchecked mMap = new HashMap<String, Object>(mMap); } mcr.mapToWriteToDisk = mMap; mDiskWritesInFlight++; boolean hasListeners = mListeners.size() > 0; if (hasListeners) { mcr.keysModified = new ArrayList<String>(); mcr.listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); } synchronized (this) { if (mClear) { if (!mMap.isEmpty()) { mcr.changesMade = true; mMap.clear(); } mClear = false; } for (Map.Entry<String, Object> e : mModified.entrySet()) { String k = e.getKey(); Object v = e.getValue(); // "this" is the magic value for a removal mutation. In addition, // setting a value to "null" for a given key is specified to be // equivalent to calling remove on that key. if (v == this || v == null) { if (!mMap.containsKey(k)) { continue; } mMap.remove(k); } else { if (mMap.containsKey(k)) { Object existingValue = mMap.get(k); if (existingValue != null && existingValue.equals(v)) { continue; } } mMap.put(k, v); } mcr.changesMade = true; if (hasListeners) { mcr.keysModified.add(k); } } mModified.clear(); } } return mcr; }
第一步:同步SharedPreferencesImpl.this。
第二步:如果mDiskWritesInFlinght大於0,則初始化mMap。
第三步:設置mcr.mapToWriteToDisk為mMap,mDiskWritesInFlight加1。
第四步:如果存在listener,初始化mcr的keysModified和listeners。
第五步:同步this,如果mClear為true,調用mMap.clear();遍歷mModified,如果value為this,則調用mMap.remove(k)方法,移除關鍵字;如果不是this,先判斷mMap是否包含k與value,不包含,則調用mMap.put(k,v),在mcr.keysModified中添加k。
第六步:調用mModified.clear()方法。
2.8.private void notifyListeners(final MemoryCommitResult mcr)
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(new Runnable() { public void run() { notifyListeners(mcr); } }); } }
第一步:如果mcr.listeners為null或者mcr.keysModified為null或者mcr.keysModified.size()等於0,則直接返回。
第二步:如果當前looper是mainLooper,遍歷mcr.keysModified,得到mcr.keysModified.get(i)為key,遍歷scr.Listeners,調用listener.onSharedPreferenceChanged()方法。
第三步:如果不是mainLooper,則在主線程中調用notifyListeners(mcr)方法。
2.9.private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable)
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) { final Runnable writeToDiskRunnable = new Runnable() { public void run() { synchronized (mWritingToDiskLock) { writeToFile(mcr);【2.10】 } synchronized (SharedPreferencesImpl.this) { mDiskWritesInFlight--; } if (postWriteRunnable != null) { postWriteRunnable.run(); } } }; final boolean isFromSyncCommit = (postWriteRunnable == null); // Typical #commit() path with fewer allocations, doing a write on // the current thread. if (isFromSyncCommit) { boolean wasEmpty = false; synchronized (SharedPreferencesImpl.this) { wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { writeToDiskRunnable.run(); return; } } QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); }
第一步:創建writeToDiskRunnable線程,同步mWritingToDiskLock,調用writeFile(mcr)執行文件寫入操作,同步SharedPreferencesImpl.this,將mDiskWritesInFlight減一,如果postWriteRunnable不為null,則運行postWriteRunnable。
第二步:如果isFromSyncCommit為true(commit方法會進入),同步ShreadPreferencesImpl.this,設置wasEmpty的值(因為commitToMemory過程會加1,則wasEmpty為true);如果wasEmpty為true,運行wirteToDiskRunnable線程,然后返回。
第三步:調用QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable)方法。
2.10.private void writeToFile(MemoryCommitResult mcr)
// Note: must hold mWritingToDiskLock private void writeToFile(MemoryCommitResult mcr) { // Rename the current file so it may be used as a backup during the next read if (mFile.exists()) { if (!mcr.changesMade) { // If the file already exists, but no changes were // made to the underlying map, it's wasteful to // re-write the file. Return as if we wrote it // out. mcr.setDiskWriteResult(true); return; } if (!mBackupFile.exists()) { if (!mFile.renameTo(mBackupFile)) { Log.e(TAG, "Couldn't rename file " + mFile + " to backup file " + mBackupFile); mcr.setDiskWriteResult(false); return; } } else { mFile.delete(); } } // Attempt to write the file, delete the backup and return true as atomically as // possible. If any exception occurs, delete the new file; next time we will restore // from the backup. try { FileOutputStream str = createFileOutputStream(mFile); if (str == null) { mcr.setDiskWriteResult(false); return; } XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); FileUtils.sync(str); str.close(); ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0); try { final StructStat stat = Os.stat(mFile.getPath()); synchronized (this) { mStatTimestamp = stat.st_mtime; mStatSize = stat.st_size; } } catch (ErrnoException e) { // Do nothing } // Writing was successful, delete the backup file if there is one. mBackupFile.delete(); mcr.setDiskWriteResult(true); return; } catch (XmlPullParserException e) { Log.w(TAG, "writeToFile: Got exception:", e); } catch (IOException e) { Log.w(TAG, "writeToFile: Got exception:", e); } // Clean up an unsuccessfully written file if (mFile.exists()) { if (!mFile.delete()) { Log.e(TAG, "Couldn't clean up partially-written file " + mFile); } } mcr.setDiskWriteResult(false); }
第一步:如果mFile存在,如果mcr.changesMade為false(沒有key發生改變,則直接飯后),設置mcr的DiskWriteResult為true,返回。如果mBackupFile不存在,則將mFile重命名為mBackupFile,設置mcr的DiskWriteResult為false,如果mBackupFile存在,則直接刪除mFile。
第二步:將mMap全部信息寫去文件中,調用Context.setFilePermissionsFromMode()方法。
第三步:得到Os.stat(mFile.getPath())的stat,設置mStatTimestamp與mStatSize。
第四步:寫入成功后,刪除mBackupFile備份文件。
第五步:設置mcr的DiskWriteResult為true,返回寫入成功,喚醒等待線程。
第六步:如果文件mFile存在,則刪除mFile,如果寫入文件的操作失敗,則刪除未成功寫入的文件。
第七步:設置mcr的DiskWriteResult為true,返回寫入失敗,喚醒等待線程。
2.11.public void apply()
public void apply() { final MemoryCommitResult mcr = commitToMemory();【2.7】 final Runnable awaitCommit = new Runnable() { public void run() { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } } }; QueuedWork.add(awaitCommit); Runnable postWriteRunnable = new Runnable() { public void run() { awaitCommit.run(); QueuedWork.remove(awaitCommit); } }; SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); // Okay to notify the listeners before it's hit disk // because the listeners should always get the same // SharedPreferences instance back, which has the // changes reflected in memory. notifyListeners(mcr);【2.8】 }
第一步:調用commitToMemory()方法。
第二步:創建線程awaitCommit調用mcr.writtenToDiskLatch.await()方法。
第三步:在QueuedWork添加awaitCommit線程。
第四步:創建線程postWriteRunnable調用awaitCommit.run()方法,從QueuedWork中移除awaitCommit。
第五步:調用SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable)方法。
第六步:調用notifyListeners(mcr)方法。
總結:所有的put、clear、remove都加了同步鎖,如果有多次調用putString,建議使用putStringSet方法,減少鎖的消耗。
edit()每次都是創建新的EditorImpl對象,getSharedPreferences()是從ContextImpl.sSharedPrefsCache唯一的SPI對象。
commit與apply的區別:
(1)apply沒有返回值;commit有返回值能知道修改是否提交成功。
(2)apply是將修改提交到內存中,再異步提交到磁盤文件;commit是同步地提交到磁盤文件。
(3)多並發的提交commit是,需等待正在處理的commit數據更新到磁盤文件后才會繼續往下執行,從而降低效率;而apply只是原子更新到內存,后調用apply函數會直接覆蓋前面內存數據,從一定程度上提高很多效率。
3.獲取數據
3.1.public Map<String, ?> getAll()
public Map<String, ?> getAll() { synchronized (this) { awaitLoadedLocked(); //noinspection unchecked return new HashMap<String, Object>(mMap); } }
第一步:調用awaitLoadedLocked()方法。
第二步:返回mMap的HashMap。
3.2.public String get***(String key, @Nullable *** defValue)
get***方法的實現基本相同,這里以getString()方法為例介紹。
@Nullable public String getString(String key, @Nullable String defValue) { synchronized (this) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; } }
第一步:同步this。
第二步:調用awaitLoadedLocked()方法。
第三步,從mMap中獲取key對應的value值,如果為空,則返回defValue,不為空。則返回從mMap中得到的值。
總結:所有的get都加了同步鎖,
四、優化建議
1.強烈建議不要在sp里面存儲特別大的key/value,有助於減少卡頓。
2.請不要高頻地使用apply,盡可能地批量提交,commit直接在主線程操作,也需要主要不要高頻的使用。
3.不要使用MODE_MULTI_PROCESS。
4.高頻寫操作的key與高頻讀操作的key可以適當地拆分文件,用於減少同步鎖競爭。
5.不要一上來就執行getSharedPreferences().edit(),應當分成量大步驟來做,中間可以執行其他代碼。
6.不要連續多次edit(),應該一次獲取edit(),然后多次執行putXXX(),減少內存波動。
7.每次commit時會把所有的數據更新到文件,所以整個文件是不應該過大的,影響整體性能。
參考文章:http://gityuan.com/2017/06/18/SharedPreferences/