1 SharedPreferences 的使用
1.1 SharedPreferences 存储的特点:
- SharedPreferences 是一种 Android 自带的轻量级数据 K-V 存储库,它使用了 .xml 的方式来存储数据,比如说我们经常用它来保存一些用户登录信息等轻量级的数据。
- 只支持 Java 基本数据类型,不支持自定义的数据类型
- 应用内数据可以共享
- 使用简单,方便
1.2 SharedPreferences 的操作模式:
- Context.MODE_PRIVATE:为默认操作模式,表示该文件是私有的,只能被应用本身访问,在该模式下,写入的内通会覆盖原文件的内容——目前只用这一种
- Context.MODE_APPEND:模式会检查文件是否存储,存在就往文件追加内容,否则就创建新文件(没有用过)
- Context.MODE_WORLD_REASABLE:表示当前文件可以被其他应用读取(Android 4.2 中弃用)
- Context.MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入(Android 4.2 中弃用)
1.3 获取 SharedPreferences 的3种方式:
-
Context 对象的 getSharedPreferences(String name, int mode) 方法获取
-
Activity 对象的 getPreferences(int mode) 方法获取
-
PreferenceManager 对象的 getDefaultSharedPreferences(Context context) 方法获取
1.4 SharedPerferences 存数据:
SharedPreferences sp = getSharedPreferences("test", Context.MODE_PRIVATE);
// 1. 第一种写法
sp.edit().putString("name", "Eileen").putInt("age", 30).commit();
// 2. 第二种写法
SharedPreferences.Editor editor = sp.edit();
editor.putString("name", "Eileen");
editor.putInt("age", 30);
editor.commit();
// 3. 错误写法
sp.edit().putString("name", "Eileen");
sp.edit().putInt("age", 30);
sp.edit().commit();
因为 sp.edit() 每次都会返回一个新的 Editor 对象,Editor 的实现类 EditorImpl 里面会有一个缓存的 Map,最后 commit() 的时候先将缓存里面的 Map 写入到内存中的 Map,然后将内存中的 Map 写入到 .xml 文件中。使用上面的方式 commit,由于 sp.edit()又重新返回了一个新的 Editor 对象,缓存中的 Map 是空的,所以导致数据无法被存储。
1.5 SharedPreferences 取数据:
SharedPreferences sp = getSharedPreferences("test", Context.MODE_PRIVATE);
String name = sp.getString("name", null);
int age = sp.getInt("age", 0);
Log.e(TAG, "name: " + name); // name: Eileen
Log.e(TAG, "age: " + age); // age: 30
1.6 SharedPreference 查看
2 SharedPreferences 源码
2.1 获取 SharedPreferences
以下是 Activity.getPreference(int mode) 的源码:
public class Activity extends ContextThemeWrapper
implements Window.Callback, KeyEvent.Callback, Window.OnWindowDismissedCallback {
public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
// 最终也是调用ContextWrapper的getSharedPreferences方法
return getSharedPreferences(getLocalClassName(), mode);
}
}
public class ContextWrapper extends Context {
Context mBase;
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
}
public abstract class Context {
public abstract SharedPreferences getSharedPreferences(
String name, @PreferencesMode int mode);
}
以下是 PreferenceManager.getDefaultSharedPreferences(Context context) 的源码:
public class PreferenceManager {
private Context mContext;
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
}
最终调用的都是 Context.getSharedPreferences ,而 Context 的最终实现类是 ContextImpl,因此,最终调用的是 ContextImpl.getSharedPreferences 方法。
以下是 ContextImpl.getSharedPreferences 的源码:
class ContextImpl extends Context {
// 1. 定义了一个key为String类型,value为File类型的HashMap
private ArrayMap<String, File> mSharedPrefsPaths;
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
File file;
synchronized (ContextImpl.class) { // 2. 加锁操作
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
// 3. 构建出和name对应的File,并将其放入map中
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
// 5. 这里有一个缓存机制,以加快之后获取SP的速度
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
// 6. 获取SharedPreferences的实现类SharedPreferencesImpl(简称SPI)
sp = cache.get(file);
if (sp == null) {
checkMode(mode);
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
return sp;
}
@Override
public File getSharedPreferencesPath(String name) {
// 4. 创建一个名为name的.xml文件
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File makeFilename(File base, String name) {
if (name.indexOf(File.separatorChar) < 0) {
final File res = new File(base, name);
BlockGuard.getVmPolicy().onPathAccess(res.getPath());
return res;
}
throw new IllegalArgumentException(
"File " + name + " contains a path separator");
}
}
由此可以看出,SharedPreferences 确实是用 .xml 来保存其中的 K-V 数据的。
缓存机制
以下是 getSharedPreferencesCacheLocked 方法的源码:
class ContextImpl extends Context {
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
@GuardedBy("ContextImpl.class")
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
// 1. 根据PackageName来保存不同的SP缓存,通过这样的方式,就保证了不同的PackageName,即使有相同name的SP从缓存中拿到的数据也是各自的
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
}
SharedPreferencesImpl
SharedPreferences 其实只是一个接口,SharedPreferencesImpl 才是具体的实现类:
public interface SharedPreferences {
public interface OnSharedPreferenceChangeListener {
void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
}
public interface Editor {
Editor putString(String key, @Nullable String value);
Editor putStringSet(String key, @Nullable Set<String> values);
Editor putInt(String key, int value);
Editor putLong(String key, long value);
Editor putFloat(String key, float value);
Editor putBoolean(String key, boolean value);
Editor remove(String key);
Editor clear();
boolean commit();
void apply();
}
Map<String, ?> getAll();
@Nullable
String getString(String key, @Nullable String defValue);
@Nullable
Set<String> getStringSet(String key, @Nullable Set<String> defValues);
int getInt(String key, int defValue);
long getLong(String key, long defValue);
float getFloat(String key, float defValue);
boolean getBoolean(String key, boolean defValue);
boolean contains(String key);
Editor edit();
void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
}
final class SharedPreferencesImpl implements SharedPreferences {
private Map<String, Object> mMap;
SharedPreferencesImpl(File file, int mode) {
mFile = file;
// 1. 进行备份文件创建
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
// 2. 从Disk载入信息
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
// 3. 开启新线程来加载数据
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);
// 通过XmlUtil将xml的数据读取为一个Map,这是一个非常耗时的操作
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();
}
}
}
}
2.2 编辑 SharedPreferences
2.2.1 获取 Editor
以下是 SharedPreferencesImpl.edit() 的源码
final class SharedPreferencesImpl implements SharedPreferences {
@Override
public Editor edit() {
synchronized (mLock) {
// 1. 等待读取完成
awaitLoadedLocked();
}
// 2. 读取完成之后才会真正的创建并返回EditorImpl对象。因为每次都创建一个新的对象,所以要做到数据更新的操作一次性完成
return new EditorImpl();
}
public final class EditorImpl implements Editor {
}
}
2.2.2 EditorImpl
真正操作 SharedPreferences 的是 Editor。Editor 是一个接口,具体的实现类为 EditorImpl。 EditorImpl 是 SharedPreferencesImpl 的一个内部类。
final class SharedPreferencesImpl implements SharedPreferences {
private Map<String, Object> mMap;
public final class EditorImpl implements Editor {
@GuardedBy("mEditorLock")
// 1. 通过mModified对SharedPreferences进行操作
private final Map<String, Object> mModified = new HashMap<>();
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
// 2. 此时还不会提交到SharedPreferencesImpl的mMap,操作改变的都是mModified
mModified.put(key, value);
return this;
}
}
}
}
2.3 提交 SharedPreferences
SharedPreferences 的提交有两种方式:apply() 和 commit()
- apply():线程不安全,性能高,异步处理 I/O 操作,一定会把这个写文件操作放入一个SingleThreadExecutor 线程池中处理
- commit():线程安全,性能慢,一般来说在当前线程完成写文件操作
2.3.1 apply()
final class SharedPreferencesImpl implements SharedPreferences {
public final class EditorImpl implements Editor {
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
// 1. 将原先读取mMap与刚刚修改过的mModified进行合并,并存储于返回的MemoryCommitResult中
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);
}
};
// 2. 传入postWriteRunnable,进行一个异步写操作
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);
}
}
}
也就是说,apply() 方法会先将数据提交到内存,再开启一个线程将数据写入到硬盘,是个异步过程。
2.3.2 commit()
final class SharedPreferencesImpl implements SharedPreferences {
public final class EditorImpl implements Editor {
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
// 1. 将原先读取mMap与刚刚修改过的mModified进行合并,并存储于返回的MemoryCommitResult中
MemoryCommitResult mcr = commitToMemory();
// 2. enqueueDiskWrite传入的第二个参数是null,会在当前线程执行写操作
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
}
}
也就是说,commit 方法会先将数据提交到内存,然后在当前线程写入硬盘,是个同步过程。
2.3.3 同步数据到内存
以下是 EditorImpl.commitToMemory 的源码:
private MemoryCommitResult commitToMemory() {
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();
}
keysCleared = true;
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, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}
将 mMap 的数据与 mModified 的数据进行了整合,之后将 mModified 重新清空。最后将合并的数据放入到 MemoryCommitResult 中。
2.3.4 写入数据至硬盘
以下是 SharedPreferencesImpl.enqueueDiskWrite 的源码:
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
// 1. 若第二个Runnable为null的话,则会将isFromSyncCommit设置为true,也就是一个同步过程
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
// 4. XmlUtils来进行XML的写入
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) {
// 2. 同步写入,否则会构造一个Runnable来提供给QueueWork进行异步写入
writeToDiskRunnable.run();
return;
}
}
// 3.QueueWork类内部维护了一个线程池,这样可以达到一步写入的目的
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
2.4 总结
SharedPreferences 其实是一个用 .xml 进行保存的 K-V 存储库。有一个两级缓存系统,包括了内存缓存和磁盘缓存。
在获取 SharedPreferences 时会进行数据加载,将 name 对应的 .xml 文件以 Map (SharedPreferences.mMap)的形式读入到内存中。
SharedPreferences 的编辑操作其实都是在其内部类 Editor 内实现。在 Editor 的内部维护了一个新的 Map(Editor.mModified),而所有的编辑其实都是在操作这个 Map,只有提交时才会和之前读取的数据进行合并。
apply
- 先将数据提交到内存,再开启一个异步过程来将数据写入硬盘
- 返回值时可能写入操作还没有结束,写入失败不会有任何提示
commit
- 先将数据提交到内存,之后是一个同步过程将数据写入硬盘
- 写入操作结束后才会返回值,写入失败会有提示
apply 的写磁盘是异步行为,commit 的写磁盘是同步行为。两者在写磁盘前都会先同步的写到内存缓存中。
当我们对写入的结果不那么关心的情况下,可以使用 apply 进行异步写入,当我们对写入的结果十分关心且有后续操作的话,最好使用 commit 的方法进行同步写入的。
3 SharedPreferences 使用封装
由于 SharedPreferences 的 key 和 value 其实最终都是以 String 类型存在,所以可以写这样一个 SharedPreferences 工具类:
class PreferenceManager {
private static final String PERF_NAME = "com.cah.androidtest.my_pref";
private static final int CURRENT_VERSION_CODE = 1;
private volatile static PreferenceManager instance;
private final SharedPreferences preferences;
private PreferenceManager(Context context) {
preferences = context.getSharedPreferences(PERF_NAME, Context.MODE_PRIVATE);
checkPreVersion();
}
public static PreferenceManager getInstance(Context context) {
if (instance == null) {
synchronized (PreferenceManager.class) {
if (instance == null) {
instance = new PreferenceManager(context);
}
}
}
return instance;
}
public final void putValue(String key, String value) {
preferences.edit().putString(key, value).apply();
}
public final String getValue(String key) {
checksLegal(key);
return preferences.getString("key", "");
}
public final void deleteValue(String key) {
checksLegal(key);
preferences.edit().remove(key).apply();
}
public final void clear() {
preferences.edit().clear().apply();
}
private void checksLegal(String key) {
if (TextUtils.isEmpty(key))
throw new IllegalArgumentException("This parameter is illegal, key: " + key);
}
private void checkPreVersion() {
final int oldVersion = preferences.getInt(PERF_NAME, 0);
if (oldVersion < CURRENT_VERSION_CODE) {
preferences.edit().clear().putInt(PERF_NAME, CURRENT_VERSION_CODE).apply();
}
}
}
由于版本升级时不会删除 SharedPreferences 文件,所以可以根据版本判断,来进行一些数据更新,从上面看来,由于每一次调用 getSharedPreferences() 都会有 I/O 操作,当内容比较多时,就不适宜在 Application 的 onCreate 中进行 SharedPreferences 文件初始化了,最好的办法就是开个子线程去完成它的创建和数据的预加载。
参考
https://blog.csdn.net/u013441613/article/details/80361817
SharedPreference 的commit和apply