什么是SharedPreference
SharedPreference(以下简称SP)是Android提供的一个轻量级的持久化存储框架,主要用于保存一些比较小的数据,例如配置数据。SP是以“健-值”对的形式来保存数据,其实质是把这些数据保存到XML文件中,每个“健-值”对就是XML文件的一个节点,通过调用SP生成的文件最终会放在手机内部存储的/data/data/
如何使用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来实现一个简单的记住登陆密码的功能。代码很简单,不做过多解释来。
//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();
}
}
});
}
}
//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 map = null;
try {
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
map = (Map) 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 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 keysModified = null;
Set listeners = null;
Map mapToWriteToDisk;
mapToWriteToDisk = mMap;
synchronized (mEditorLock) {
boolean changesMade = false;
for (Map.Entry 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的读取速度。