深度剖析Android SharePreferences
前言
提到Sp(后面都用这个简称),相信Android开发者都不会陌生,基本上写项目都会用到,但是可能没有深究实现细节,因此当面试时
被面试官问到相关问题,往往不知所措.
先提几个问题:
- q1:Sp可以跨进程么?为什么?
- q2:有什么方法可以让Sp实现跨进程?
- q3:commit和apply有什么区别?使用场景?
- q4:为什么Sp不适合存放占用内存较大的内容?如bitmap
使用
Sp的简单使用如下
SharedPreferences preferences = this.getSharedPreferences("sp_name", Context.MODE_PRIVATE);
String key = preferences.getString("key", "");
preferences.edit().putString("my_key", "hell").apply();
调用Context.getSharedPreferences
,传递sp的名称和操作模式既可获取Sp的实例
默认的操作模式是Context.MODE_PRIVATE
,也是官方推荐的,其他几种模式基本都被弃用了,即官方不推荐使用.
源码分析
getSharedPreferences
我们知道Context的唯一实现是ContextImpl
,不管是Activity的context还是Application的context,context.getSharedPreferences
最终调用都是ContextImpl.getSharedPreferences
@Override
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.
//目标sdk小于4.4时,支持sp的名字为null
if (mPackageInfo.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);
}
ContextImpl
有一个成员变量mSharedPrefsPaths
,保存sp的名字与对应的文件的映射,这个很好理解,当我们通过context拿sp的实例
的时候,肯定先要找到sp对应文件,然后再对该文件进行读写操作.
值得注意的是这里对于mSharedPrefsPaths
的操作时加锁了,锁的对象是ContextImpl.class
,所以不论是从哪个Context的子类来获取sp,都能保证
mSharedPrefsPaths
的线程安全.
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();//1
sp = cache.get(file);//2
if (sp == null) {
checkMode(mode);//3
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);//4
cache.put(file, sp);
return sp;
}
}
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();
}//5
return sp;
}
这个方法大概涉及5个比较重要的点,上面都有标注,接下来一一分析
- 首先看
getSharedPreferencesCacheLocked()
方法的实现
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;
}
这里主要涉及两个映射关系,一个是应用包名与sp之间的映射,因为一个应用可能创建多个sp文件来存储不同的业务配置项
第二个是sp文件与sp实现类SharedPreferencesImpl
之间的映射关系,这个之前有提到.
值得注意的是它们使用的都是ArrayMap
而不是HashMap
,主要是因为ArrayMap
比HashMap
更省内存,这个以后单独写一篇.
-
通过file拿到对应的sp的实现类实例.
-
检查操作模式,看一下实现
private void checkMode(int mode) {
if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
if ((mode & MODE_WORLD_READABLE) != 0) {
throw new SecurityException("MODE_WORLD_READABLE no longer supported");
}
if ((mode & MODE_WORLD_WRITEABLE) != 0) {
throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
}
}
}
当目标sdk版本大于N的时候,如果操作模式设置为MODE_WORLD_READABLE
或MODE_WORLD_WRITEABLE
话,即允许其他应用读写sp的话,就会抛出
安全异常,可见Google对应用安全方面的限制越来越严格了.
-
创建sp的实现类的实例,并加入到缓存中,以便下次能够快速的拿到.
-
当操作模式设置为
Context.MODE_MULTI_PROCESS
或者目标sdk版本小于3.2时,调用sp.startReloadIfChangedUnexpectedly()
void startReloadIfChangedUnexpectedly() {
synchronized (mLock) {
// TODO: wait for any pending writes to disk?
if (!hasFileChangedUnexpectedly()) {
return;
}
startLoadFromDisk();
}
}
该方法先去检查文件状态是否改变,如果有的话就重新读取文件数据到内存.这里我们知道MODE_MULTI_PROCESS
是不靠谱的,它并不能支持数据
跨进程共享,只是getSharePreference
时回去检查文件状态是否改变,改变就重新加载数据到内存.
SharedPreferencesImpl
上面了解到getSharePreference
返回的其实是SharedPreferencesImpl
的实例,现在重点看一下SharedPreferencesImpl
的实现.
构造函数
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
都是一些常规操作,初始化一些值,创建备份文件,重点看一下startLoadFromDisk
startLoadFromDisk
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
将mLoaded
变量置为false,表示数据还没有加载成功,然后开启了一个线程,并调用loadFromDisk
loadFromDisk
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {//1
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}
Map map = null;
StructStat stat = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);//2
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
/* ignore */
}
synchronized (mLock) {
mLoaded = true;
if (map != null) {
mMap = map;//3
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
mLock.notifyAll();//4
}
}
-
先判断备份文件是否存在,如果存在就删除当前文件,将备份文件重命名为正式文件.
-
然后创建文件输出流读取文件内存并转化为Map,注意这里创建带缓存的输出流时,指定的buffer大小为16k.可以借鉴.
-
将读取到的Map赋值给mMap成员变量,如果map为空就创建一个空的
HashMap
,这里又是用到HashMap了,因为这里
设计频繁查找或插入操作,而hashMap的查询和插入操作的效率是优于ArrayMap的.
- 通知唤醒线程,有唤醒就有阻塞,看一下哪里阻塞了,全局搜索一下
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) {
}
}
}
该方法在mLoaded为false的时候一直阻塞,而之前的notifyAll唤醒的就是此处的阻塞.再看一下awaitLoadedLocked
在哪里被调用了.
public Map<String, ?> getAll() {
synchronized (mLock) {
awaitLoadedLocked();
//noinspection unchecked
return new HashMap<String, Object>(mMap);
}
}
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
@Nullable
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
synchronized (mLock) {
awaitLoadedLocked();
Set<String> v = (Set<String>) mMap.get(key);
return v != null ? v : defValues;
}
}
public int getInt(String key, int defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}
public long getLong(String key, long defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Long v = (Long)mMap.get(key);
return v != null ? v : defValue;
}
}
public float getFloat(String key, float defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Float v = (Float)mMap.get(key);
return v != null ? v : defValue;
}
}
public boolean getBoolean(String key, boolean defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
public boolean contains(String key) {
synchronized (mLock) {
awaitLoadedLocked();
return mMap.containsKey(key);
}
}
这里可以知道,所有的get相关方法都被阻塞,直到完成数据从文件加载到内存的过程.因此当第一次调用sp的get相关
函数时是比较慢的,需要等待数据从文件被读取到内存,之后会比较快,因为是直接在内存中读取.
至此,get相关方法已经分析完毕,原理也比较容易理解,接下来看看put相关方法.
edit
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.
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
调用put相关方法之前需要调用edit方法,此处也是需要等待的,返回的是EditorImpl
的实例.
EditorImpl
@GuardedBy("mLock")
private final Map<String, Object> mModified = Maps.newHashMap();
EditorImpl是SharePreferenceImpl的内部类,内部有一个HashMap保存被更改的键值对.
public Editor putBoolean(String key, boolean value) {
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
public Editor remove(String key) {
synchronized (mLock) {
mModified.put(key, this);
return this;
}
}
从以上两个方法可以知道,put方法就是向mModified添加一个键值对,remove方法添加的value为当前editor实例.
它们都是被mLock加锁保护的,有两个原因
- HashMap不是线程安全的
- 需要和其他的get方法互斥
commit
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
MemoryCommitResult mcr = commitToMemory(); //1
SharedPreferencesImpl.this.enqueueDiskWrite( //2
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;
}
- commitToMemory实现
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
// We optimistically don't make a deep copy until
// a memory commit comes in when we're already
// writing to disk.
if (mDiskWritesInFlight > 0) {
// We can't modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
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 (mLock) {
boolean changesMade = false;
if (mClear) {
if (!mMap.isEmpty()) {
changesMade = true;
mMap.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// "this" is the magic value for a removal mutation. In addition,
// setting a value to "null" for a given key is specified to be
// equivalent to calling remove on that key.
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
顾名思义,就是把更改的键值对提交到内存中,即把mModified中的键值对更新到mMap中,顺便获取被更新
的键的集合以及外部设置监听器列表(基于深拷贝)
- enqueueDiskWrite
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//判断是同步任务还是异步任务
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
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);
}
commit方法分析完毕,其实就是将最新的mMap写入到文件中.
apply
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
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() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
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);
}
enqueueDiskWrite之前看过了,关键代码如下
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
首先获取一个Handler
的实例,然后再通过Handler
发送一个消息,先看一下getHandler
private static Handler getHandler() {
synchronized (sLock) {
if (sHandler == null) {
HandlerThread handlerThread = new HandlerThread("queued-work-looper",
Process.THREAD_PRIORITY_FOREGROUND);
handlerThread.start();
sHandler = new QueuedWorkHandler(handlerThread.getLooper());
}
return sHandler;
}
}
这是一个典型的单例模式写法,Handler
构造方法的Looper来自HandlerThread
,这是一个内部维护消息机制
的线程,任务是按照时间顺序依次执行的,不了解的可以去看一下源码.
接下来看一下handleMessage
方法实现
public void handleMessage(Message msg) {
if (msg.what == MSG_RUN) {
processPendingWork();
}
}
private static void processPendingWork() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {
work = (LinkedList<Runnable>) sWork.clone();
sWork.clear();
// Remove all msg-s as all work will be processed now
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
//直接循环调用runnable的run方法执行任务
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
if (DEBUG) {
Log.d(LOG_TAG, "processing " + work.size() + " items took " +
+(System.currentTimeMillis() - startTime) + " ms");
}
}
}
}
其实到这里apply方法也基本上分析完毕,该方法是在子线程被调用的,为了线程安全考虑,使用的是HandlerThread
来依次执行写文件任务.
但我们需要依次提交更改多个键值对时,只需要保留最后一个commit或apply方法既可.
跨进程的Sp
结合ContentProvider
并重写call方法
总结
-
sp不适合存储过大的数据,因为它一直保存在内存中,数据过大容易造成内存溢出.
-
sp并不支持跨进程,因为它不能保证更新本地数据后被另一个进程所知道,而且跨进程的操作标记已经被弃用.
-
sp的commit方法是直接在当前线程执行文件写入操作,而apply方法是在工作线程执行文件写入,尽可能使用apply,因为不会阻塞当前线程.
-
sp批量更改数据时,只需要保留最后一个apply即可,避免添加多余的写文件任务.
-
每个sp存储的键值对不宜过多,否则在加载文件数据到内存时会耗时过长,而阻塞sp的相关
get
或put
方法,造成ui卡顿. -
频繁更改的配置项和不常更改的配置项应该分开为不同的sp存放,避免不必要的io操作.
感觉写的有些乱,不过一些重要的点基本都有,如果有理解错误的地方还请大佬指正.