一、简介
在Android中,我们通常会需要存储一些数据,有一些大型的数据诸如图片、JSON数据等,可以通过读写File的方式实现;有一些大量级的关系型数据,可以通过数据库SQLite实现;还有一些简单的、无安全风险的键值对数据,可以通过Android提供的SharedPreferences实现。
SharedPreferences
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);
}
SharedPreferences是一个轻量级的xml键值对文件,使用起来也是很方便。
//根据文件名,获取SharedPreferences对象;mode一般都使用MODE_PRIVATE,只能由该App访问
SharedPreferences sp = getSharedPreferences("setting", Context.MODE_PRIVATE);
//根据key,获取指定值
boolean needInitChannels = sp.getBoolean("isDebug", false);
//获取Editor编辑对象,用于编辑SharedPreferences
SharedPreferences.Editor editor = sp.edit();
editor.putBoolean("isDebug", true);
//同步提交到SharedPreferences文件,获取是否同步成功的结果
editor.commit();
//异步提交到SharedPreferences文件
editor.apply();
当我们第一次访问一个名为"setting"的SharedPreferences文件,系统会在应用数据目录下(/data/data/packageName/)的shared_prefs文件夹下,创建一个同名的xml文件。
二、实现原理
1.创建
我们是通过Context的getSharedPreferences()方法来获取一个SharedPreferences对象,而Context的实际逻辑载体,是在ContextImpl里,所以我们来看ContextImpl的该方法。
public SharedPreferences getSharedPreferences(String name, int mode) {
...
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
//1.获取缓存File对象
file = mSharedPrefsPaths.get(name);
if (file == null) {
//生成File对象
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
//2.获取File对象对应的SharedPreferences对象
return getSharedPreferences(file, mode);
}
@Override
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) {
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;
}
}
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;
}
这里第一步我们可以看到,有一个ArrayMap会缓存每一个SharedPreferences文件对象,如果第一次加载的话,会调用getSharedPreferencesPath()生成File对象。
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
///data/data/packageName/目录
mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
return ensurePrivateDirExists(mPreferencesDir);
}
}
private static File ensurePrivateDirExists(File file, int mode, int gid, String xattr) {
if (!file.exists()) {
final String path = file.getAbsolutePath();
try {
Os.mkdir(path, mode);
...
}...
}
return file;
}
我们会看到,如果应用目录下还没有shared_prefs文件夹,则创建一个该文件夹。
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File makeFilename(File base, String name) {
if (name.indexOf(File.separatorChar) < 0) {
return new File(base, name);
}
}
然后,会在shared_prefs文件夹下,创建一个我们指定名字的xml文件,用来存储键值对数据。
至此,会生成一个相应SharedPreferences的File文件,并进行缓存。
第二步,通过File获取SharedPreferences对象。
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
...
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
...
return sp;
}
这里我们看到,返回的SharedPreferences实际对象,是一个SharedPreferencesImpl对象实例,并且也通过一个ArrayMap做了一个缓存,也就是说,一个name会对应一个SharedPreferences的File实例,而一个File会对应一个SharedPreferencesImpl实例。
sharedPreferences是一个接口,在Content类中,声明了getSharedPreferences()空实现的一个方法,先看源码
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
}
final String packageName = getPackageName();
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
sSharedPrefs.put(packageName, packagePrefs);
}
// 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 (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
sp = packagePrefs.get(name);
if (sp == null) {
File prefsFile = getSharedPrefsFile(name);
sp = new SharedPreferencesImpl(prefsFile, mode);
packagePrefs.put(name, 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();
}
return sp;
}
我们可以很明显的看到
创建ArrayMap集合,集合的key:string,value:ArrayMap<String,SharedPreferenceImpl>
getPackageName(),拿到项目包名;
根据包名,在第一步创建的ArrayMap集合中找,有没有包名为key的键值对,if逻辑判断
如果没有,创建一个ArrayMap集合,作为value,包名作为key,存储在第一步创建的集合ArrayMap中
根据方法传入的SharedPreferences名称,也就是name的String类型参数,在第四步创建的ArrayMap中get查找有没有创建对应名称的SharedPreferenceImpl对象,if逻辑判断
如果sp为空,创建对应的file文件,将包名称对应的SharedPreferenceImpl对象存储到第四步的ArrayMap中,name作为key
然后返回第四步以包名称为key的ArrayMap。
2.取值
通过上述内容,可以知道我们实际操作的对象,其实都是SharedPreferencesImpl对象。这里我们以getBoolean()为例,看一下如何获取值。
public boolean getBoolean(String key, boolean defValue) {
synchronized (mLock) {
//等待从磁盘加载数据(需要的话)
awaitLoadedLocked();
//直接从map取值返回
Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
那么从磁盘读取文件内容,并生成Map对象的初始化过程,是在何时进行的呢?是在SharedPreferencesImpl的构造函数中进行。
SharedPreferencesImpl(File file, int mode) {
...
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
//工作线程进行
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
...
}
...
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);
//解析XML生成Map
map = (Map<String, Object>) XmlUtils.readMapXml(str);
}...
}
}
...
mMap = map;
...
mLock.notifyAll();
}
可以看到,SharedPreferencesImpl创建的时候,简单粗暴的开启了一个工作线程,进行File的读取和解析,并生成了Map对象,赋值给mMap变量。
而在我们的getBoolean()等操作Map的方法调用时,会通过awaitLoadedLocked()方法判断是否map已经生成,如果没有则等待。
private void awaitLoadedLocked() {
...
while (!mLoaded) {
try {
mLock.wait();
}...
}
}
3.更新
再来看更新,上面说到过,更新是通过SharedPreferences的edit()方法,获取一个Editor对象进行。
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
可见每次都是创建一个新的EditorImpl对象实例进行操作。通过源码可知多个数据编辑的时候 尽可能创建一个edit 对象
public Editor putBoolean(String key, boolean value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
所有的更新操作,都会维护在EditorImpl对象的一个HashMap中,等待后续应用。
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
4.应用
更新后,我们需要写入到磁盘中,EditorImpl为我们提供了两个方法,一个是commit()方法,一个是apply()方法,他们的区别,主要是同步和异步的差别。
(1)commit()
public boolean commit() {
...
//1.将改动先同步到内存
MemoryCommitResult mcr = commitToMemory();
//2.同步写入磁盘
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
}...
return mcr.writeToDiskResult;
}
第一步是调用commitToMemory()方法,将EditorImpl的modify改动,应用到内存中,即应用到mMap中。
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
mMap = new HashMap<String, Object>(mMap);
}
mapToWriteToDisk = mMap;
//当前更新操作的线程数+1
mDiskWritesInFlight++;
...
synchronized (mEditorLock) {
...
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
if (v == this || v == null) {//remove
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {//update or insert
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
}...
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
第二步,将内存中最新的数据,写到文件中。
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//commit()方法时为true
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
//将Map写入File
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
//当前更新操作的线程数-1
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
//只有当前线程调用时,因为同步进行,所以一直为1;有多个线程同时调用时,会大于1
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//当前线程直接调用
writeToDiskRunnable.run();
return;
}
}
//异步调用
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
当只有一个线程操作SharedPreferences的话,mDiskWritesInFlight计数器始终为1,因为是同步写入File,写入后计数器会-1。
而写入文件就是很简单的IO操作,只不过需要把Map转换为xml的格式。
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
...
try {
//OutputStream
FileOutputStream str = createFileOutputStream(mFile);
...
//将map数据写成xml格式到file中
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
...
//设置成功写入的结果
mcr.setDiskWriteResult(true, true);
}
(2)apply()
再来看apply()方法。
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
...
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
//postWriteRunnable不为null
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
...
}
这里可以看到,调用的方法和commit()都一样,唯独调用enqueueDiskWrite()方法时,第二个Runnable对象不为null,按照上面说,不为null时,走的方法就是异步方法QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
//将任务加入到队列中等待执行
sWork.add(work);
//通过handler发送消息
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
private static class QueuedWorkHandler extends Handler {
...
public void handleMessage(Message msg) {
if (msg.what == MSG_RUN) {
processPendingWork();
}
}
}
private static void processPendingWork() {
...
for (Runnable w : work) {
//执行任务
w.run();
}
}
可以看到,内部就是通过handler发送消息执行任务的,任务还是一样的writeFile()方法,那么为何是异步的呢?就得来看看handler是啥handler了。
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;
}
}
现在就明白了吧,是一个全局的HandlerThread对象,也就是一个工作线程,所以apply()方法,会通过一个全局唯一的异步线程进行写文件的操作。
三、总结
- SharedPreferences的File创建和内容解析,在内存中是有缓存的。
- SharedPreferences的提交,commit()方法是在当前线程完成,而apply()方法在全局唯一的一个工作线程中完成。
- 所有的文件和内存读写操作,都通过锁对象进行加锁,保证了多线程同步。
- sp的数据保存写入是全量的。
- sp在保存大数据量的时候会有可能导致ANR。
四、QA
(1)每次调用getSharedPreferences时都会创建一个SharedPreferences对象吗?这个对象具体是哪个类对象?
答:不是,只要name相同,就会返回同一个,SharedPreferencesImpl对象,packagePrefs存放文件name与SharedPreferencesImpl键值对,sSharedPrefs存放包名与ArrayMap键值对。注意sSharedPrefs是static变量,也就是一个类只有一个实例,因此你每次getSharedPreferences其实拿到的都是同一个SharedPreferences对象。
(2)在UI线程中调用getXXX有可能导致ANR吗?
答:有可能的,getXXX之前,会给当前线程加锁,如果sp文件特别大,查询非常耗时的时候,有可能ANR。
(3)为什么SharedPreferences只适合用来存放少量数据,为什么不能把SharedPreferences对应的xml文件当成普通文件一样存放大量数据?
答:其实这和第二个问题没有区别,因为SharedPreference是整个文件都加载到内存中,文件太大了会对内存造成压力。
(4)commit和apply有什么区别?
(5)SharedPreferences每次写入时是增量写入吗?
答:不是,每次都是重新写入,说一下那个mBackupFile,SharedPreferences在写入时会先把之前的xml文件改成名成一个备份文件,然后再将要写入的数据写到一个新的文件中,如果这个过程执行成功的话,就会把备份文件删除。由此可见每次即使只是添加一个键值对,也会重新写入整个文件的数据,这也说明SharedPreferences只适合保存少量数据,文件太大会有性能问题。SharedPreferences每次写入都是整个文件重新写入,不是增量写入。
网友评论