概述
SharedPreferences(簡稱SP)是Android中常用的資料存盤方式,SP采用key-value(鍵值對)形式,主要用于輕量級的資料存盤,尤其適合保存應用的配置引數,但不建議使用SP來存盤大規模的資料,可能會降低性能,
SP采用XML檔案格式來保存資料,該檔案位于 /data/data/<packageName>/shared_prefs/,
使用示例
// 加載SP檔案資料,“my_prefs”為檔案名
SharedPreferences sp = getSharedPreferences("my_prefs", Context.MODE_PRIVATE);
// 保存資料
Editor editor = sp.edit();
editor.putString("blog", "www.xiaox.com");
// 提交資料:同步方式,有回傳值表示資料保存是否成功
boolean result = editor.commit();
// 提交資料:異步方式,沒有回傳值//
editor.apply()
// 讀取資料
String blog = sp.getString("blog", "");
my_prefs.xml檔案內容:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="blog">www.xiaox.com</string>
</map>
架構
類圖

說明:SharedPreferences與Editor只是兩個介面,SharedPreferencesImpl和EditorImp分別實作了對應的介面,另外,ContextImpl記錄著SharedPreferences的重要資料,如下:
- sSharedPrefsCache:以包名為key,二級key是SP檔案,以SharedPreferencesImp為value的嵌套map結構,sSharedPrefsCache是靜態成員變數,每個行程只有唯一的一份,且由ContextImpl.class鎖保護,
- mSharedPrefsPaths:記錄所有的SP檔案,以檔案名為key,具體檔案為value的map結構,
- mPreferencesDir:是值SP所在目錄,即
/data/data/<packageName>/shared_prefs/
作業流程

說明:
- putXxx()操作:把資料寫入到EditorImpl.mModified;
- apply()或者commit()操作: a. 先呼叫
commitToMemory(),將資料同步到SharedPreferencesImpl的mMap,并保存到MemoryCommitResult的mapToWriteToDisk; b. 再呼叫enqueueDiskWrite(),寫入到磁盤檔案;在這之前把原有資料保存到.bak后綴的檔案,用于在寫磁盤的程序出現任何例外可恢復資料, - getXxx()操作:從SharedPreferencesImpl.mMap讀取資料,
原始碼分析(API 28)
獲取SharedPreferences
可以通過 Activity.getPreferences(mode)、 PreferenceManager.getDefaultSharedPreferences(context)或者 Context.getSharedPreferences(name,mode)來獲取SharedPreferences實體, 最終呼叫的是ContextImpl的getSharedPreferences(name, mode),
ContextImpl#getSharedPreferences(name, mode):
class ContextImpl extends Context {
@GuardedBy("ContextImpl.class")
private ArrayMap<String, File> mSharedPrefsPaths;
// ...
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// ...
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
// 先從mSharedPrefsPaths查詢是否存在相應檔案
file = mSharedPrefsPaths.get(name);
if (file == null) {
// 如果檔案不存在,則創建新的檔案
file = getSharedPreferencesPath(name);
// 將新創建的檔案保存到mSharedPrefsPaths,以檔案名為key
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@Override
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
// 創建目錄/data/data/<packageName>/shared_prefs/
mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
return ensurePrivateDirExists(mPreferencesDir);
}
}
}
ContextImpl#getSharedPreferences(file, mode):
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
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");
}
}
// 創建SharedPreferencesImpl
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) {
// 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;
}
@GuardedBy("ContextImpl.class")
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;
}
SharedPreferencesImpl初始化:
SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
mFile = file;
// 創建.bak后綴的備份檔案,用于在發生例外時,可以通過備份檔案來恢復資料
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
SharedPreferencesImpl#startLoadFromDisk():
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
// 通過作業執行緒讀取檔案資料到mMap
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);
}
}
// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}
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) {
// An errno exception means the stat failed. Treat as empty/non-existing by
// ignoring.
}
catch (Throwable t) {
thrown = t;
}
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
// It's important that we always signal waiters, even if we'll make
// them fail with an exception. The try-finally is pretty wide, but
// better safe than sorry.
try {
if (thrown == null) {
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
// In case of a thrown exception, we retain the old map. That allows
// any open editors to commit and store updates.
}
catch (Throwable t) {
mThrowable = t;
}
finally {
mLock.notifyAll();
}
}
}
獲取SharedPreferences總結:
- 首次使用則創建相應xml檔案;
- 異步加載檔案內容到記憶體,此時執行getXxx()和edit()方法都是阻塞等待的,直到檔案資料全部加載到記憶體;
- 一旦資料完全加載到記憶體,后續的getXxx()則是直接訪問記憶體,
獲取資料
SharedPreferencesImpl#getString(key, defValue):
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
// 檢查資料是否加載完成
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
// 當沒有加載完成,則進入等待狀態
mLock.wait();
}
catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
編輯資料
獲取Editor編輯器實體:SharedPreferencesImpl#edit()
public Editor edit() {
synchronized (mLock) {
// 等待資料加載完成
awaitLoadedLocked();
}
// 創建EditorImpl實體
return new EditorImpl();
}
EditorImpl#putString(key, value):
public final class EditorImpl implements Editor {
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
@GuardedBy("mEditorLock")
private Boolean mClear = false;
// ...
// 插入資料
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
// 插入資料,暫存到mModified
mModified.put(key, value);
return this;
}
}
// 移除資料
public Editor remove(String key) {
synchronized (mEditorLock) {
mModified.put(key, this);
return this;
}
}
// 清空全部資料
public Editor clear() {
synchronized (mEditorLock) {
mClear = true;
return this;
}
}
}
保存資料
保存資料,主要是呼叫
commit()和apply()方法來完成的,
EditorImpl#commit()
public Boolean commit() {
// ...
// 將資料更新到記憶體
MemoryCommitResult mcr = commitToMemory();
// 將記憶體資料同步到檔案
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */
);
try {
// 進入等待狀態,直到寫入檔案的操作完成
mcr.writtenToDiskLatch.await();
}
catch (InterruptedException e) {
return false;
}
// 通知監聽器,并在主執行緒回呼onSharedPreferenceChanged()方法
notifyListeners(mcr);
// 回傳檔案操作的結果
return mcr.writeToDiskResult;
}
EditorImpl#commitToMemory()
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;
// 當mClear為true,則直接清空mMap
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();
// this是一個特殊值,當v為空,相當于remove該條資料
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的資料
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
EditorImpl#enqueueDiskWrite()
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();
}
}
};
// 使用commit方法,會進入這個分支,在當前執行緒執行
if (isFromSyncCommit) {
Boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
// 使用apply方法,會執行該句,將任務放入單執行緒的執行緒池中執行
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
EditorImpl#writeToFile()
private void writeToFile(MemoryCommitResult mcr, Boolean isFromSyncCommit) {
// ...
Boolean fileExists = mFile.exists();
if (fileExists) {
Boolean needsWrite = false;
// Only need to write if the disk state is older than this commit
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
needsWrite = true;
} else {
synchronized (mLock) {
// No need to persist intermediate states. Just wait for the latest state to
// be persisted.
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
// 沒有改變,直接回傳
if (!needsWrite) {
mcr.setDiskWriteResult(false, true);
return;
}
Boolean backupFileExists = mBackupFile.exists();
if (!backupFileExists) {
// 當備份檔案不存在,則把mFile重命名為備份檔案
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false, false);
return;
}
} else {
// 否則,直接洗掉mFile
mFile.delete();
}
}
try {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
// 將mMap全部資訊寫入檔案
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
writeTime = System.currentTimeMillis();
FileUtils.sync(str);
fsyncTime = System.currentTimeMillis();
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (mLock) {
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
}
}
catch (ErrnoException e) {
// Do nothing
}
if (DEBUG) {
fstatTime = System.currentTimeMillis();
}
// 寫入成功,洗掉備份檔案
mBackupFile.delete();
mDiskStateGeneration = mcr.memoryStateGeneration;
// 回傳寫入成功,喚醒等待執行緒
mcr.setDiskWriteResult(true, true);
return;
}
catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
// 如果檔案寫入操作失敗,則洗掉未成功寫入的檔案
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
// 回傳寫入失敗,喚醒等待執行緒
mcr.setDiskWriteResult(false, false);
}
EditorImpl#apply()
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);
}
性能優化
IO瓶頸
IO瓶頸是造成SP性能差的最大原因,解決IO瓶頸,80%的性能問題就解決了,
SP的IO瓶頸包括 讀取資料到記憶體與 資料寫入磁盤兩部分,
1.讀取資料到記憶體有兩個場景會觸發:
-
SP檔案沒有被加載到記憶體時,呼叫getSharedPreferences方法會初始化檔案并讀入記憶體,
-
版本低于Android-H或使用了MULTI_PROCESS標記時,每次呼叫getSharedPreference方法時都會讀入,
優化:
我們可以優化的便是b了,每次加載資料到記憶體太過影響效率,但H以下版本已經很低了,基本可以忽略, 對于MULTI_PROCESS,可以采用ContentProvider等其他方式,效率更好,而且可避免SP資料丟失的情況,
2.資料寫入到磁盤也有兩個場景會觸發:
-
Editor的commit方法,每次執行時同步寫入磁盤,
-
Editor的apply方法,每次執行時在單執行緒池中寫入磁盤,異步寫入,
優化:
commit和apply的方法區別在于同步寫入和異步寫入,以及是否需要回傳值, 在不需要回傳值的情況下,使用apply方法可以極大的提高性能, 同時,多個寫入操作可以合并為一個commit/apply,將多個寫入操作合并后也能提高IO性能,
鎖性能差
-
SP的get操作,會鎖定SharedPreferences物件,互斥其他操作,
-
SP的put操作,edit()及commitToMemory會鎖定SharedPreferences物件,put操作會鎖定Editor物件,寫入磁盤更會鎖定一個寫入鎖,
優化:
由于鎖的緣故,SP操作并發時,耗時會增加,減少鎖耗時,是一個優化點, 由于讀寫操作的鎖均是SP實體物件的,將資料分拆到不同的sp檔案中,便是減少鎖耗時的直接方案, 降低單檔案訪問頻率,多檔案均攤訪問,以減少鎖耗時,
優化總結
- 強烈建議不要在sp里面存盤特別大的key/value, 有助于減少卡頓/anr;
- 請不要高頻地使用apply, 盡可能地批量提交;commit直接在主執行緒操作, 更要注意了;
- 不要使用MODEMULTIPROCESS;
- 高頻寫操作的key與高頻讀操作的key可以適當地拆分檔案, 由于減少同步鎖競爭;
- 不要一上來就執行getSharedPreferences().edit(), 應該分成兩大步驟來做, 中間可以執行其他代碼;
- 不要連續多次edit(), 應該獲取一次獲取edit(),然后多次執行putxxx(), 減少記憶體波動; 經常看到大家喜歡封裝方法, 結果就導致這種情況的出現;
- 每次commit時會把全部的資料更新的檔案, 所以整個檔案是不應該過大的, 影響整體性能,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/382915.html
標籤:其他
上一篇:【Unity終極奧義】Unity打包去掉啟影片面Logo,無需破解,一學就會
下一篇:2021年度年終總結篇
