什么是SharedPreference
SharedPreference(以下簡稱SP)是Android提供的一個輕量級的持久化存儲框架,主要用于保存一些比較小的數據,例如配置數據。SP是以“健-值”對的形式來保存數據,其實質是把這些數據保存到XML文件中,每個“健-值”對就是XML文件的一個節點,通過調用SP生成的文件最終會放在手機內部存儲的/data/data/<package name>/shared_prefs目錄下。
如何使用SharedPreference
獲取SharedPreference
使用SP的第一步是獲取SP對象。在Android中,我們可以通過以下三種方式來獲取SP對象。
1.Context類中的getSharedPreferences方法
public SharedPreferences getSharedPreferences(String name, int mode) {
...
}
這個方法接收兩個參數,第一個參數是SP的文件名,我們知道SP是以XML文件的形式進行存儲的,每一個SharedPreference實例都對應了一個XML文件,這里的name就是XML文件的名字。第二個參數用于指定文件的操作模式,最開始的時候SP是可以跨進程訪問的,所以SP有MODE_PRIVATE,MODE_WORLD_READABLE,MODE_MULTI_PROCESS等多種操作模式,只不過出于安全性考慮,谷歌目前只保留了MODE_PRIVATE這一種模式,其他模式均已被廢棄。在MODE_PRIVATE模式下,只有應用本身可以訪問SharedPreference文件,其他應用無權訪問。
2.Activity的getPreferences方法
public SharedPreferences getPreferences(int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
這個方法只接收一個參數,即SP文件的操作模式,那SharedPreference的名字是啥呢,通過源碼可以看到,這里使用了當前類的類名來作為SP的文件名。例如,當前類名為MainActivity,那么對應SP的文件名就是MainActivity.xml。
3.PreferenceManager的getDefaultSharedPreferences方法
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
private static int getDefaultSharedPreferencesMode() {
return Context.MODE_PRIVATE;
}
public static String getDefaultSharedPreferencesName(Context context) {
return context.getPackageName() + "_preferences";
}
這個方法接收Context參數,并使用當前包名_preferences作為SP的文件名。使用MODE_PRIVATE作為操作模式。
以上三個方法其實大同小異,主要區別在于最終生成的SP的文件名有差異。
使用SharedPreference進行讀寫數據
1.讀取數據
使用SharedPreference讀取數據很簡單,分為兩個步驟:
(1) 獲取SharedPreference對象(使用上述的三種方式)
(2) 調用SharedPreference對象的get方法讀取對應類型的數據。
2.寫入數據
使用SharedPreference寫入數據分為四步:
(1) 獲取SharedPreference對象
(2) 獲取SharedPreferences.Editor對象
(3) 調用SharedPreferences.Editor對象的put方法寫入數據
(4) 調用SharedPreferences.Editor對象的apply/commit方法提交更改
示例
我們平常在登陸賬號的時候一般都會有一個記住密碼的功能,下面就用SP來實現一個簡單的記住登陸密碼的功能。代碼很簡單,不做過多解釋來。
<!--activity_main.xml-->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Account" />
<EditText
android:id="@+id/account"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Password" />
<EditText
android:id="@+id/password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight = "1"
android:layout_gravity = "center_vertical"
android:inputType="textPassword"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/remember_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="remember password"/>
</LinearLayout>
<Button
android:id="@+id/login"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="login"/>
</LinearLayout>
//MainActivity.java
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;
public class MainActivity extends Activity {
private EditText mAccountEdit;
private EditText mPasswordEdit;
private Button mLoginBtn;
private CheckBox mRememberPasswordCbx;
private SharedPreferences mSharedPreferences;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mAccountEdit = findViewById(R.id.account);
mPasswordEdit = findViewById(R.id.password);
mLoginBtn = findViewById(R.id.login);
mRememberPasswordCbx = findViewById(R.id.remember_password);
mSharedPreferences = getSharedPreferences("admin",MODE_PRIVATE);
boolean isRememberPassword = mSharedPreferences.getBoolean("RememberPassword",false);
if(isRememberPassword){
String account = mSharedPreferences.getString("Account","");
String password = mSharedPreferences.getString("Password","");
mAccountEdit.setText(account);
mPasswordEdit.setText(password);
mRememberPasswordCbx.setChecked(true);
}
mLoginBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String account = mAccountEdit.getText().toString();
String password = mPasswordEdit.getText().toString();
SharedPreferences.Editor edit = mSharedPreferences.edit();
if (account.equals("admin") && password.equals("888888")) {
if(mRememberPasswordCbx.isChecked()){
edit.putString("Account",account);
edit.putString("Password",password);
edit.putBoolean("RememberPassword",true);
}else {
edit.clear();
}
edit.apply();
Intent intent = new Intent(MainActivity.this, UserActivity.class);
startActivity(intent);
}else {
Toast.makeText(MainActivity.this,"賬號或者密碼錯誤",Toast.LENGTH_LONG).show();
}
}
});
}
}
<!--activity_user.xml-->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="login success"/>
</LinearLayout>
//UserActivity
import android.app.Activity;
import android.os.Bundle;
public class UserActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user);
}
}
SharedPreference源碼分析
1.獲取對象
首先看下獲取SP對象的源碼。無論使用哪種方式來獲取SP對象,最終都是通過調用SharedPreferencesImpl來構建SP對象的。創建SP對象代碼如下。
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
這個方法主要就是定義了一個備份文件對象,然后調用了startLoadFromDisk方法,繼續來看startLoadFromDisk
方法。
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
這里調用了loadFromDisk
方法,開啟了一個異步線程,因為加載SP文件是IO耗時操作,不能放在主線程,否則會導致主線程阻塞。繼續看loadFromDisk方法。
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
Map<String, Object> map = null;
try {
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
}
...
}
}
...
synchronized (mLock) {
mLoaded = true;
try {
if (map != null) {
mMap = map;
} else {
mMap = new HashMap<>();
}
} catch (Throwable t) {
} finally {
mLock.notifyAll();
}
}
}
這里省略了部分代碼,可以看到如果有備份文件,SP會優先使用備份文件,然后就是讀取并解析XML文件,通過 XmlUtils.readMapXml方法讀取XML文件并解析成Map對象,這里就是創建SP對象的關鍵,也就是說創建SP對象的過程其實就是把SP文件加載到Map(內存)中的過程。加載完成之后,會調用mLock同步鎖的notifyAll方法,來使其他阻塞在這個同步鎖的線程解除阻塞。同時,把mLoaded置為true,表示加載文件完成。到此,創建SP對象的過程就結束了,我們最終得到了一個Map,后續的讀取操作都會基于這個Map來進行。
從上面過程可以看到SharedPreference最終會以Map的形式加載到內存中,所以SharedPreference適合用于存儲小數據,并不適合存儲較大的數據。否則一方面會消耗內存,一方面在加載文件的過程可能導致主線程阻塞。
2.讀取數據
創建SP對象完成后,我們實際上獲得來一個裝載SP數據的Map,讀取數據的過程實際就是從Map取數據的過程,以getString方法為例。
public String getString(String key, String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
這里讀取數據不難理解,就是Map的get操作,有一個地方需要注意的,就是awaitLoadedLocked
方法。我們看一下這個方法。
private void awaitLoadedLocked() {
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
從上面分析我們可以知道,mLoaded表示SP文件是加載完成,如果沒有加載完成,這個方法就會進入while循環,并調用mLock.wait()來阻塞當前線程。在loadFromDisk方法中我們可以看到,當加載文件完成后,會調用 mLock.notifyAll()來使其他阻塞在mLock同步鎖的線程解除阻塞。所以,等到SP文件加載完成后,這個方法就會解除阻塞,如果沒有讀取完成,調用getString的線程會阻塞在這個同步鎖上。這也解釋來為什么在第一次從SP讀取數據的時候有可能會耗時比較久,后面讀取數據幾乎不耗時。就是因為SP文件沒有加載完成,導致線程阻塞引起的,后續讀取因為都是直接從內存中(mMap)中讀取,所以幾乎不會耗時。
2.寫入數據
在向SP寫入數據的時候,我們首先獲取了一個Editor對象,這個Editor對象的作用是什么呢?來看下獲取Editor對象的源碼。
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
這里和讀取數據一樣,首先也是調用awaitLoadedLocked
方法來等待SP文件加載完成。然后就是調用EditorImpl來創建editor對象。看一下EditorImpl類的定義。
public final class EditorImpl implements Editor {
private final Object mEditorLock = new Object();
private final Map<String, Object> mModified = new HashMap<>();
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
...
@Override
public void apply() {
...
}
@Override
public void commit() {
...
}
}
這個類很簡單,主要就是創建來一個Map(mModified),并定義來一些put方法,還有就是定義來一個apply方法和一個commit方法。
在向SharedPreference寫入數據的時候,我們是調用editor的put方法來寫入數據的,以putString方法為例。
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
這里可以看到,寫入數據時,并沒有把數據直接寫如文件,而是把數據放在了mModified這個表里邊,這個表是在內存里的。
執行寫入數據的最后一步是調用editor的supply/commit方法來提交變更,那么這兩個方法有什么區別呢?首先來看一下commit方法。
public boolean commit() {
...
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);
...
}
commit方法首先是調用commitToMemory構造存儲對象,然后調用enqueueDiskWrite將進行持久化存儲。首先來看一下commitToMemory
方法
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
mapToWriteToDisk = mMap;
synchronized (mEditorLock) {
boolean changesMade = 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);
}
}
}
我們最終向文件寫入的內容是mapToWriteToDisk,這個map包含兩部分,第一部分是創建SP對象時從文件加載到內存的map,第二部分是創建editor對象的時創建的mModified,editor的所有put操作都是放在了這個map里邊,把兩個map合并之后就得到了最終要向文件寫入的map,所以SP每次提交數據修改并不是增量寫入數據,而是將新增數據和原有數據合并之后全量寫入。
后面接著看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();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
這里首先創建了一個Runnable對象writeToDiskRunnable,在這個對象的run方法里邊執行文件寫入操作。然后如果isFromSyncCommit為true且當前只有一個寫入操作,就直接在當前線程執行writeToDiskRunnable的run方法,也就是說在當前線程執行寫入文件操作。否則就傳入QueuedWork進行異步寫入。那么isFromSyncCommit什么時候為true呢,就是在postWriteRunnable=null的時候,這時再回頭看commit方法,這個方法在調用enqueueDiskWrite方法時,postWriteRunnable參數傳入的是null,看到這里也就明白了,commit是同步IO操作,也就是在調用commit方法的線程直接執行寫入操作。
再來看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);
}
分析過commit方法之后,apply方法就很簡單了,首先apply方法也是構建了一個mcr對象,然后定義了一個postWriteRunnable對象并調用了enqueueDiskWrite方法,根據上面對enqueueDiskWrite方法的分析,postWriteRunnable!=null會使isFromSyncCommit為false,進而在異步線程執行文件寫入操作。
所以在使用SharedPreference存儲數據的時候,最好使用apply方法提交修改,而不是commit,因為commit是在當前線程執行IO操作,有可能會導致線程卡頓甚至出現ANR。而apply是異步寫入的,不會阻塞當前線程執行。
使用SharedPreference的建議
- 不要使用SP存儲大文件及存儲大量的key和value,因為最終SharedPreference是會把所有數據加載到內存的,存儲大數據或者大量數據會造成界面卡頓或者ANR,SP是輕量級存儲框架,如果要存儲較大數據,請考慮數據庫或者文件存儲方式。
- apply進行存儲,而不是commit方法,因為apply是異步寫入磁盤的,所以效率上會比commit好,但是如果需要即存即用的話還是盡量使用commit。
- 如果修改數據,盡量批量寫入后再調用apply或者commit,從源碼分析可以看到,無論是apply或者是commit,都是將修改的數據和原有數據合并,并執行全量寫入操作。多次調用apply或者commit不僅會發起多次IO操作,還會導致不必要的數據寫入。
- 不要把所有數據都存儲在一個SP文件里邊,SP文件越大,讀寫速度越慢。因此,不同功能模塊的數據最好用不同的文件存儲,這樣可以提高SP的加載和寫入速度。。
- 盡量不要存儲json或者html數據,因為json或者html在存儲時會引來額外的字符轉義開銷,如果數據比較大,會大大降低sp的讀取速度。