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