SharedPreferences是Andorid中的一个轻量级的数据存储方式。一般用来存储一些简单的数据类型,比如int,String,Boolean。SharedPreferences的内部使用ArrayMap键值对的形式来临时存储数据,最终ArrayMap的数据会通过IO流写入到XML文件中,这个XML文件在手机中的位置是: /data/data/shared_prefs/
一.SharedPreferences的使用
SharedPreferences sf = getSharedPreferences("demo",Context.MODE_PRIVATE);
SharedPreferences.Editor mEditor = sf.edit();
mEditor.putString("name","leidong");
mEditor.putBoolean("man",true);
mEditor.putInt("age",35);
mEditor.commit();//mEditor.apply();
String name = sf.getString("name","");
boolean man = sf.getBoolean("man",true);
int age = sf.getInt("age",0);
Mode:
//私有模式。XML文件每次都覆盖写入
public static final int MODE_PRIVATE = 0x0000;
//文件开放读写权限。不安全,官方已经遗弃这个用法
public static final int MODE_WORLD_READABLE = 0x0001;
public static final int MODE_WORLD_WRITEABLE = 0x0002;
//添加模式。检查文件是否存在,存在就往文件追加内容,不存在就创建新的文件。
public static final int MODE_APPEND = 0x8000
//跨进程模式,官方已经遗弃这个用法
public static final int MODE_MULTI_PROCESS = 0x0004;
二.SharedPreferences的原理
1. SharedPreferences对象的获取
Activity & ContextWrapper
//如果不指定xml的名字,其默认值就是当前activity的className
public SharedPreferences getPreferences(int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
ContextImpl.java
public SharedPreferences getSharedPreferences(String name, int mode) {
// At least one application in the world actually passes in a null
// name. This happened to work because when we generated the file name
// we would stringify it to "null.xml". Nice.
if (mLoadedApk.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
//生成XML文件并创建其File对象
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
//返回/data/data/shared_prefs/目录的file对象
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
return ensurePrivateDirExists(mPreferencesDir);
}
}
//ContextImpl使用sSharedPrefsCache集合保存了新创建的SharedPreferencesImpl对象,每次获取
//SharedPreferencesImpl对象之前,先查询sSharedPrefsCache集合,如果集合中有,则直接返回,如
//果没有,则新创建一个SharedPreferencesImpl对象,并加入到sSharedPrefsCache集合中。
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) {
//检查mode,如果是MODE_MULTI_PROCESS和MODE_WORLD_WRITEABLE,直接抛出
// SecurityException,不允许获取此xml对应的SharedPreferencesImpl对象。
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");
}
}
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
//如果是MODE_MULTI_PROCESS模式,多进程读写xml的情况下,为预防从sSharedPrefsCache集合中取
//出的sSharedPreferencesImpl对象中的数据跟xml中的数据不同步,因此,每次都从磁盘中读取xml内
//容,再返回新的sSharedPreferencesImpl对象。
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;
}
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.java
//标记作用,xml是否已经被加载到内存中
private boolean mLoaded = false;
//构造函数,初始化,创建备份文件mBackupFile ,读取磁盘中的xml文件
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);//备份文件
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
//起一个新线程,加载磁盘的xml文件内容
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
//
private void loadFromDisk() {
synchronized (mLock) {
//如果xml已经被加载过,直接返回
if (mLoaded) {
return;
}
//如果备份存在,说明之前读写操作被中断,把mBackupFile当成mFile来用,直接使用备份文件中的内容。
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);//读取xml中的内容到内存中,赋值给map
} 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;//标记为已经加载过xml文件
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();//唤醒其他正在等待mLock锁的进程
}
}
}
//获取SharedPreferences中保存的键值对数据
public int getInt(String key, int defValue) {
synchronized (mLock) {
awaitLoadedLocked();//如果mLoaded为false,线程会一直处于等待的状态
Integer v = (Integer)mMap.get(key);//获取读取到内存中的值(mMap键值对)
return v != null ? v : defValue;
}
}
2. SharedPreferences.Editor对数据的存储操作
private final Map<String, Object> mModified = new HashMap<>();
public Editor edit() {
// TODO: remove the need to call awaitLoadedLocked() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
//
// context.getSharedPreferences(..).edit().putString(..).apply()
//
// ... all without blocking.
//如果该线程获取到了mLock对象锁,但是mLoaded为false,也就是加载xml过程没结束,那么线程会一直等待
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
//如果mLoaded为false,线程会一直处于等待的状态
private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
//存储的键值对数据只是保存到了内部类Editor的mModified集合中
public Editor putInt(String key, int value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
//把内部类Editor的mModified集合中的数据保存到SharedPreferencesImpl类的mMap集合中。
MemoryCommitResult mcr = commitToMemory();
//把mMap集合中的数据写入到xml文件中
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
if (DEBUG) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " committed after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
notifyListeners(mcr);//通知注册监听者
return mcr.writeToDiskResult;
}
public void apply() {
final long startTime = System.currentTimeMillis();
//把内部类Editor的mModified集合中的数据保存到SharedPreferencesImpl类的mMap集合中。
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
//把mMap集合中的数据写入到xml文件中
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);//通知注册监听者
}
//把mMap集合中的数据写入到xml文件中。
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//如果是apply()形式,则postWriteRunnable != null,isFromSyncCommit 为true。
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);//写入磁盘xml的操作被封装在新起的线程中
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
//如果是commit()的形式,且当前没有写磁盘任务(mDiskWritesInFlight == 1),则直接调用
//writeToDiskRunnable.run()执行writeToFile()写入操作,不起新线程。
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//如果是apply()的形式,所有的线程都加入到QueuedWork中,以队列的形式保存,逐个线程启动
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
综上,SharedPreferencesImpl定义了ArrayMap集合,其对象初始化时,加载XML文件内容到ArrayMap集合中。SharedPreferencesImpl对外提供get()接口,返回其ArrayMap集合中的数据。Editor也定义了ArrayMap集合,对外提供put()接口,接受调用者传入的值并存入其ArrayMap中,在commit()或者apply()时,同步Editor和SharedPreferencesImpl中的ArrayMap值,把ArrayMap中的数据保存到XML文件中。为考虑多线程情况下的使用,SharedPreferencesImpl和Editor中的多数操作均设置了相应的对象锁和同步代码块synchronized。由于SharedPreferencesImpl的初始化是一次性加载XML,为了性能上考虑,ContextImpl设置了缓冲区(ArrayMap)保存SharedPreferencesImpl对象,以便反复使用已有对象。SharedPreferences不支持跨进程使用,虽然有MODE_MULTI_PROCESS跨进程模式,但是它不能很好的工作,官方已经遗弃这个用法。
注意事项:
1.使用MODE_MULTI_PROCESS时,不要保存SharedPreference变量
2.xml不宜放太多数据,commit()操作或者加载xml过程中有可能会阻塞线程。
3.尽量使用apply()方法替代commit()
疑问:
1. ContextImpl的缓冲区使用Map键值对保存了SharedPreferencesImpl对象,其健值是packageName,在多线程的情况下同时访问相同name的SharedPreferences,那肯定是使用到同一个SharedPreferencesImpl对象,这会产生线程安全的问题,因此SharedPreferencesImpl和Editor中的操作均加了同步代码块synchronized 。多线程的问题就解决了。但是,为啥多进程下,google为啥不建议使用MODE_MULTI_PROCESS? 多线程和多进程需要考虑的点不一样吗?
2. 如果一个应用要访问另一个应用中的SharedPreferences,为啥要createPackageContext()?
Context configContext = null;
try {
configContext = createPackageContext("com.example.administrator.android_example", Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE);
}catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
SharedPreferences sf = configContext.getSharedPreferences("demo", Context.MODE_WORLD_READABLE);
线程安全是指多个线程同时操作同一个变量的时候产生的冲突和不同步问题。由于每个进程都有自己的内存空间,且进程间的内存空间是互相隔离的,每个进程 都有自己的Context上下文,因此要获取相应的Context上下文,才能获取到相应的SharedPreferences 对象,否则跨进程获取不到想要的SharedPreferences数据。synchronized可以尽量保证线程安全,但是不能保证进程安全。
参考:
https://stackoverflow.com/questions/27827678/use-sharedpreferences-on-multi-process-mode
https://stackoverflow.com/questions/4693387/sharedpreferences-and-thread-safety
https://www.jianshu.com/p/875d13458538