什么是SharedPreference
SharedPreference(以下簡(jiǎn)稱SP)是Android提供的一個(gè)輕量級(jí)的持久化存儲(chǔ)框架撕攒,主要用于保存一些比較小的數(shù)據(jù)己英,例如配置數(shù)據(jù)吱七。SP是以“健-值”對(duì)的形式來(lái)保存數(shù)據(jù)契邀,其實(shí)質(zhì)是把這些數(shù)據(jù)保存到XML文件中伴挚,每個(gè)“健-值”對(duì)就是XML文件的一個(gè)節(jié)點(diǎn)阎抒,通過(guò)調(diào)用SP生成的文件最終會(huì)放在手機(jī)內(nèi)部存儲(chǔ)的/data/data/<package name>/shared_prefs目錄下醒叁。
如何使用SharedPreference
獲取SharedPreference
使用SP的第一步是獲取SP對(duì)象司浪。在Android中,我們可以通過(guò)以下三種方式來(lái)獲取SP對(duì)象把沼。
1.Context類中的getSharedPreferences方法
public SharedPreferences getSharedPreferences(String name, int mode) {
...
}
這個(gè)方法接收兩個(gè)參數(shù)啊易,第一個(gè)參數(shù)是SP的文件名,我們知道SP是以XML文件的形式進(jìn)行存儲(chǔ)的饮睬,每一個(gè)SharedPreference實(shí)例都對(duì)應(yīng)了一個(gè)XML文件租谈,這里的name就是XML文件的名字。第二個(gè)參數(shù)用于指定文件的操作模式捆愁,最開(kāi)始的時(shí)候SP是可以跨進(jìn)程訪問(wèn)的割去,所以SP有MODE_PRIVATE,MODE_WORLD_READABLE昼丑,MODE_MULTI_PROCESS等多種操作模式呻逆,只不過(guò)出于安全性考慮,谷歌目前只保留了MODE_PRIVATE這一種模式菩帝,其他模式均已被廢棄咖城。在MODE_PRIVATE模式下茬腿,只有應(yīng)用本身可以訪問(wèn)SharedPreference文件,其他應(yīng)用無(wú)權(quán)訪問(wèn)宜雀。
2.Activity的getPreferences方法
public SharedPreferences getPreferences(int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
這個(gè)方法只接收一個(gè)參數(shù)切平,即SP文件的操作模式,那SharedPreference的名字是啥呢州袒,通過(guò)源碼可以看到揭绑,這里使用了當(dāng)前類的類名來(lái)作為SP的文件名。例如郎哭,當(dāng)前類名為MainActivity他匪,那么對(duì)應(yīng)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";
}
這個(gè)方法接收Context參數(shù)夸研,并使用當(dāng)前包名_preferences作為SP的文件名邦蜜。使用MODE_PRIVATE作為操作模式。
以上三個(gè)方法其實(shí)大同小異亥至,主要區(qū)別在于最終生成的SP的文件名有差異悼沈。
使用SharedPreference進(jìn)行讀寫(xiě)數(shù)據(jù)
1.讀取數(shù)據(jù)
使用SharedPreference讀取數(shù)據(jù)很簡(jiǎn)單,分為兩個(gè)步驟:
(1) 獲取SharedPreference對(duì)象(使用上述的三種方式)
(2) 調(diào)用SharedPreference對(duì)象的get方法讀取對(duì)應(yīng)類型的數(shù)據(jù)姐扮。
2.寫(xiě)入數(shù)據(jù)
使用SharedPreference寫(xiě)入數(shù)據(jù)分為四步:
(1) 獲取SharedPreference對(duì)象
(2) 獲取SharedPreferences.Editor對(duì)象
(3) 調(diào)用SharedPreferences.Editor對(duì)象的put方法寫(xiě)入數(shù)據(jù)
(4) 調(diào)用SharedPreferences.Editor對(duì)象的apply/commit方法提交更改
示例
我們平常在登陸賬號(hào)的時(shí)候一般都會(huì)有一個(gè)記住密碼的功能絮供,下面就用SP來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的記住登陸密碼的功能。代碼很簡(jiǎn)單茶敏,不做過(guò)多解釋來(lái)壤靶。
<!--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,"賬號(hào)或者密碼錯(cuò)誤",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.獲取對(duì)象
首先看下獲取SP對(duì)象的源碼。無(wú)論使用哪種方式來(lái)獲取SP對(duì)象惊搏,最終都是通過(guò)調(diào)用SharedPreferencesImpl來(lái)構(gòu)建SP對(duì)象的贮乳。創(chuàng)建SP對(duì)象代碼如下。
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
這個(gè)方法主要就是定義了一個(gè)備份文件對(duì)象恬惯,然后調(diào)用了startLoadFromDisk方法向拆,繼續(xù)來(lái)看startLoadFromDisk
方法。
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
這里調(diào)用了loadFromDisk
方法酪耳,開(kāi)啟了一個(gè)異步線程浓恳,因?yàn)榧虞dSP文件是IO耗時(shí)操作,不能放在主線程碗暗,否則會(huì)導(dǎo)致主線程阻塞颈将。繼續(xù)看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會(huì)優(yōu)先使用備份文件厨疙,然后就是讀取并解析XML文件洲守,通過(guò) XmlUtils.readMapXml方法讀取XML文件并解析成Map對(duì)象疑务,這里就是創(chuàng)建SP對(duì)象的關(guān)鍵,也就是說(shuō)創(chuàng)建SP對(duì)象的過(guò)程其實(shí)就是把SP文件加載到Map(內(nèi)存)中的過(guò)程梗醇。加載完成之后知允,會(huì)調(diào)用mLock同步鎖的notifyAll方法,來(lái)使其他阻塞在這個(gè)同步鎖的線程解除阻塞叙谨。同時(shí)温鸽,把mLoaded置為true,表示加載文件完成手负。到此涤垫,創(chuàng)建SP對(duì)象的過(guò)程就結(jié)束了,我們最終得到了一個(gè)Map竟终,后續(xù)的讀取操作都會(huì)基于這個(gè)Map來(lái)進(jìn)行蝠猬。
從上面過(guò)程可以看到SharedPreference最終會(huì)以Map的形式加載到內(nèi)存中,所以SharedPreference適合用于存儲(chǔ)小數(shù)據(jù)统捶,并不適合存儲(chǔ)較大的數(shù)據(jù)榆芦。否則一方面會(huì)消耗內(nèi)存,一方面在加載文件的過(guò)程可能導(dǎo)致主線程阻塞喘鸟。
2.讀取數(shù)據(jù)
創(chuàng)建SP對(duì)象完成后匆绣,我們實(shí)際上獲得來(lái)一個(gè)裝載SP數(shù)據(jù)的Map,讀取數(shù)據(jù)的過(guò)程實(shí)際就是從Map取數(shù)據(jù)的過(guò)程什黑,以getString方法為例崎淳。
public String getString(String key, String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
這里讀取數(shù)據(jù)不難理解,就是Map的get操作兑凿,有一個(gè)地方需要注意的凯力,就是awaitLoadedLocked
方法。我們看一下這個(gè)方法礼华。
private void awaitLoadedLocked() {
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
從上面分析我們可以知道咐鹤,mLoaded表示SP文件是加載完成,如果沒(méi)有加載完成圣絮,這個(gè)方法就會(huì)進(jìn)入while循環(huán)祈惶,并調(diào)用mLock.wait()來(lái)阻塞當(dāng)前線程。在loadFromDisk方法中我們可以看到扮匠,當(dāng)加載文件完成后捧请,會(huì)調(diào)用 mLock.notifyAll()來(lái)使其他阻塞在mLock同步鎖的線程解除阻塞。所以棒搜,等到SP文件加載完成后疹蛉,這個(gè)方法就會(huì)解除阻塞,如果沒(méi)有讀取完成力麸,調(diào)用getString的線程會(huì)阻塞在這個(gè)同步鎖上可款。這也解釋來(lái)為什么在第一次從SP讀取數(shù)據(jù)的時(shí)候有可能會(huì)耗時(shí)比較久育韩,后面讀取數(shù)據(jù)幾乎不耗時(shí)。就是因?yàn)镾P文件沒(méi)有加載完成闺鲸,導(dǎo)致線程阻塞引起的筋讨,后續(xù)讀取因?yàn)槎际侵苯訌膬?nèi)存中(mMap)中讀取,所以幾乎不會(huì)耗時(shí)摸恍。
2.寫(xiě)入數(shù)據(jù)
在向SP寫(xiě)入數(shù)據(jù)的時(shí)候悉罕,我們首先獲取了一個(gè)Editor對(duì)象,這個(gè)Editor對(duì)象的作用是什么呢立镶?來(lái)看下獲取Editor對(duì)象的源碼壁袄。
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
這里和讀取數(shù)據(jù)一樣,首先也是調(diào)用awaitLoadedLocked
方法來(lái)等待SP文件加載完成谜慌。然后就是調(diào)用EditorImpl來(lái)創(chuàng)建editor對(duì)象然想。看一下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() {
...
}
}
這個(gè)類很簡(jiǎn)單变泄,主要就是創(chuàng)建來(lái)一個(gè)Map(mModified),并定義來(lái)一些put方法恼琼,還有就是定義來(lái)一個(gè)apply方法和一個(gè)commit方法妨蛹。
在向SharedPreference寫(xiě)入數(shù)據(jù)的時(shí)候,我們是調(diào)用editor的put方法來(lái)寫(xiě)入數(shù)據(jù)的晴竞,以putString方法為例蛙卤。
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
這里可以看到,寫(xiě)入數(shù)據(jù)時(shí)噩死,并沒(méi)有把數(shù)據(jù)直接寫(xiě)如文件颤难,而是把數(shù)據(jù)放在了mModified這個(gè)表里邊,這個(gè)表是在內(nèi)存里的已维。
執(zhí)行寫(xiě)入數(shù)據(jù)的最后一步是調(diào)用editor的supply/commit方法來(lái)提交變更行嗤,那么這兩個(gè)方法有什么區(qū)別呢?首先來(lái)看一下commit方法垛耳。
public boolean commit() {
...
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);
...
}
commit方法首先是調(diào)用commitToMemory構(gòu)造存儲(chǔ)對(duì)象栅屏,然后調(diào)用enqueueDiskWrite將進(jìn)行持久化存儲(chǔ)。首先來(lái)看一下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);
}
}
}
我們最終向文件寫(xiě)入的內(nèi)容是mapToWriteToDisk堂鲜,這個(gè)map包含兩部分栈雳,第一部分是創(chuàng)建SP對(duì)象時(shí)從文件加載到內(nèi)存的map,第二部分是創(chuàng)建editor對(duì)象的時(shí)創(chuàng)建的mModified缔莲,editor的所有put操作都是放在了這個(gè)map里邊哥纫,把兩個(gè)map合并之后就得到了最終要向文件寫(xiě)入的map,所以SP每次提交數(shù)據(jù)修改并不是增量寫(xiě)入數(shù)據(jù)痴奏,而是將新增數(shù)據(jù)和原有數(shù)據(jù)合并之后全量寫(xiě)入蛀骇。
后面接著看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);
}
這里首先創(chuàng)建了一個(gè)Runnable對(duì)象writeToDiskRunnable,在這個(gè)對(duì)象的run方法里邊執(zhí)行文件寫(xiě)入操作松靡。然后如果isFromSyncCommit為true且當(dāng)前只有一個(gè)寫(xiě)入操作,就直接在當(dāng)前線程執(zhí)行writeToDiskRunnable的run方法建椰,也就是說(shuō)在當(dāng)前線程執(zhí)行寫(xiě)入文件操作雕欺。否則就傳入QueuedWork進(jìn)行異步寫(xiě)入。那么isFromSyncCommit什么時(shí)候?yàn)閠rue呢棉姐,就是在postWriteRunnable=null的時(shí)候屠列,這時(shí)再回頭看commit方法,這個(gè)方法在調(diào)用enqueueDiskWrite方法時(shí)伞矩,postWriteRunnable參數(shù)傳入的是null笛洛,看到這里也就明白了,commit是同步IO操作乃坤,也就是在調(diào)用commit方法的線程直接執(zhí)行寫(xiě)入操作苛让。
再來(lái)看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);
}
分析過(guò)commit方法之后湿诊,apply方法就很簡(jiǎn)單了狱杰,首先apply方法也是構(gòu)建了一個(gè)mcr對(duì)象,然后定義了一個(gè)postWriteRunnable對(duì)象并調(diào)用了enqueueDiskWrite方法厅须,根據(jù)上面對(duì)enqueueDiskWrite方法的分析仿畸,postWriteRunnable!=null會(huì)使isFromSyncCommit為false,進(jìn)而在異步線程執(zhí)行文件寫(xiě)入操作朗和。
所以在使用SharedPreference存儲(chǔ)數(shù)據(jù)的時(shí)候错沽,最好使用apply方法提交修改,而不是commit眶拉,因?yàn)閏ommit是在當(dāng)前線程執(zhí)行IO操作千埃,有可能會(huì)導(dǎo)致線程卡頓甚至出現(xiàn)ANR。而apply是異步寫(xiě)入的镀层,不會(huì)阻塞當(dāng)前線程執(zhí)行镰禾。
使用SharedPreference的建議
- 不要使用SP存儲(chǔ)大文件及存儲(chǔ)大量的key和value,因?yàn)樽罱KSharedPreference是會(huì)把所有數(shù)據(jù)加載到內(nèi)存的唱逢,存儲(chǔ)大數(shù)據(jù)或者大量數(shù)據(jù)會(huì)造成界面卡頓或者ANR吴侦,SP是輕量級(jí)存儲(chǔ)框架,如果要存儲(chǔ)較大數(shù)據(jù)坞古,請(qǐng)考慮數(shù)據(jù)庫(kù)或者文件存儲(chǔ)方式备韧。
- apply進(jìn)行存儲(chǔ),而不是commit方法痪枫,因?yàn)閍pply是異步寫(xiě)入磁盤(pán)的织堂,所以效率上會(huì)比commit好叠艳,但是如果需要即存即用的話還是盡量使用commit。
- 如果修改數(shù)據(jù)易阳,盡量批量寫(xiě)入后再調(diào)用apply或者commit附较,從源碼分析可以看到,無(wú)論是apply或者是commit潦俺,都是將修改的數(shù)據(jù)和原有數(shù)據(jù)合并拒课,并執(zhí)行全量寫(xiě)入操作贸典。多次調(diào)用apply或者commit不僅會(huì)發(fā)起多次IO操作继效,還會(huì)導(dǎo)致不必要的數(shù)據(jù)寫(xiě)入。
- 不要把所有數(shù)據(jù)都存儲(chǔ)在一個(gè)SP文件里邊掠归,SP文件越大肖爵,讀寫(xiě)速度越慢卢鹦。因此,不同功能模塊的數(shù)據(jù)最好用不同的文件存儲(chǔ)劝堪,這樣可以提高SP的加載和寫(xiě)入速度冀自。。
- 盡量不要存儲(chǔ)json或者h(yuǎn)tml數(shù)據(jù)秒啦,因?yàn)閖son或者h(yuǎn)tml在存儲(chǔ)時(shí)會(huì)引來(lái)額外的字符轉(zhuǎn)義開(kāi)銷凡纳,如果數(shù)據(jù)比較大,會(huì)大大降低sp的讀取速度帝蒿。