今天看到一个有趣的现象,比较好奇,由于这个是好几亿用户的大项目,所以我相信这里面代码有一定权威性,由于很好奇为什么不用原生的SharedPreferences,而是用ContentProvider的方式代替。究竟是为什么呢?要知道这个原因,那就必须要看看原生SharedPreferences有什么相对于ContentProvider的缺点了。
好神奇,不知道谷歌什么时候有一个看源码的链接哈哈。
使用
SharedPreferences sharedPreferences = getSharedPreferences("xxx", Context.MODE_PRIVATE);
Editor editor = sharedPreferences.edit();
editor.putString("aaa", "xxx");
editor.putInt("bbb", 10);
editor.commit();
生成的文件如下:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="aaa">xxx</string>
<int name="bbb" value="10" />
</map>
源码解析
会有两种方式使用SharedPreferences 但是最终原理一样。
Activity.java
public SharedPreferences getPreferences(int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
PreferenceManager.java
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
第二种方式以包名加上_preferences作为文件名, 以MODE_PRIVATE模式创建SP文件. 即packgeName_preferences.xml.
最终都会调到:
ContextImpl.java
class ContextImpl extends Context {
private ArrayMap<String, File> mSharedPrefsPaths;
public SharedPreferences getSharedPreferences(String name, int mode) {
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
//先从mSharedPrefsPaths查询是否存在相应文件
file = mSharedPrefsPaths.get(name);
if (file == null) {
//如果文件不存在, 则创建新的文件,文件的路径是/data/data/package name/shared_prefs/
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
}
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
//创建目录/data/data/package name/shared_prefs/
mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
return ensurePrivateDirExists(mPreferencesDir);
}
}
最后会调用到重载函数中
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
//创建SharedPreferencesImpl
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) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
关于checkMode
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");
}
}
}
可以看出来N包括N以上的版本都不支持MODE_WORLD_READABLE
,MODE_WORLD_WRITEABLE
这两种模式了。
上面getSharedPreferences是把文件缓存起来,这次又要缓存啥?我们看看
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;
}
ArrayMap<packageName,<ArrayMap<File,SharedPreferencesImpl>>>
形成了上面这种结构
第一次肯定是空,所以我们要创建SharedPreferencesImpl。
SharedPreferencesImpl(File file, int mode) {
mFile = file;
//创建为.bak为后缀的备份文件
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
synchronized (SharedPreferencesImpl.this) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
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);
} catch (XmlPullParserException | IOException e) {
...
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
...
}
synchronized (SharedPreferencesImpl.this) {
mLoaded = true;
if (map != null) {
mMap = map; //从文件读取的信息保存到mMap
mStatTimestamp = stat.st_mtime; //更新修改时间
mStatSize = stat.st_size; //更新文件大小
} else {
mMap = new HashMap<>();
}
notifyAll(); //唤醒处于等待状态的线程
}
}
我们看整体流程就是,首次会创建xml文件,然后会把整个文件在子线程中加载到内存中,但是要注意的是此时异步是加锁的就是没加载完成要是读取文件内容的话,会一直等待,所以我们就可以想象,如果一个文件很大,是不是我们就得等很长时间才可以读内容。而且我们看到缓存是根据文件缓存,当每个文件都比较大的时候就悲剧了。这也就是不能用SharedPreferences存储大量信息的原因。
继续说查询
SharedPreferencesImpl.java
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
//检查是否加载完成
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
wait(); //当没有加载完成,则进入等待状态
} catch (InterruptedException unused) {
}
}
}
当xml文件没有加载到内存中时就等待。
根据我们的用法我们要存储就得先有Editor
public Editor edit() {
synchronized (this) {
awaitLoadedLocked();
}
return new EditorImpl(); //创建EditorImpl
}
我去还要等待,其实也对等待加载完成才能写入
public final class EditorImpl implements Editor {
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;
//插入数据
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
//插入数据, 先暂存到mModified对象
mModified.put(key, value);
return this;
}
}
//移除数据
public Editor remove(String key) {
synchronized (this) {
mModified.put(key, this);
return this;
}
}
//清空全部数据
public Editor clear() {
synchronized (this) {
mClear = true;
return this;
}
}
}
我们添加数据移除数据清空数据,仅仅只是操作mModified 这个在内存中的Map而已。
最后一步提交
EditorImpl.java
public boolean commit() {
//将数据更新到内存
MemoryCommitResult mcr = commitToMemory();
//将内存数据同步到文件
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
try {
//进入等待状态, 直到写入文件的操作完成
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
//通知监听则, 并在主线程回调onSharedPreferenceChanged()方法
notifyListeners(mcr);
// 返回文件操作的结果数据
return mcr.writeToDiskResult;
}
提交就比较有水平了我们一个个分析对应的方法:
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {
if (mDiskWritesInFlight > 0) {
mMap = new HashMap<String, Object>(mMap);
}
mcr.mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
//是否有监听key改变的监听者
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
mcr.keysModified = new ArrayList<String>();
mcr.listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (this) {
//当mClear为true, 则直接清空mMap
if (mClear) {
if (!mMap.isEmpty()) {
mcr.changesMade = true;
mMap.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
//注意此处的this是个特殊值, 用于移除相应的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);
}
mcr.changesMade = true; // changesMade代表数据是否有改变
if (hasListeners) {
mcr.keysModified.add(k); //记录发生改变的key
}
}
mModified.clear(); //清空EditorImpl中的mModified数据
}
}
return mcr;
}
我们上面如果设置清空,则真正清空的地方是在这里。我们把mModified对应的数据都拿出来,这个是添加数据,移除数据等操作的内存数据映射,然后在mMap这个是读入文件的内存数据映射。然后过滤包含的等操作,如果发生变化,则记录这个发生变化的key,因为已经将数据转移到了mMap中,所以此时清空mModified在内存中的数据。
哦对了,这里还有mDiskWritesInFlight++操作
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
//执行文件写入操作
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
//此时postWriteRunnable为null不执行该方法
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
final boolean isFromSyncCommit = (postWriteRunnable == null);
if (isFromSyncCommit) { //commit方法会进入该分支
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
//commitToMemory过程会加1,则wasEmpty=true
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//跳转到上面
writeToDiskRunnable.run();
return;
}
}
//不执行该方法
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
首先映入眼帘的是我们的中国梦之队跳水运动员...额跑的有点偏,mDiskWritesInFlight--操作,在run里面。说明写成功了这个值就成0了。
这里wasEmpty = mDiskWritesInFlight == 1;
由于上面操作此时为true,所以执行writeToDiskRunnable.run();
private void writeToFile(MemoryCommitResult mcr) {
if (mFile.exists()) {
if (!mcr.changesMade) { //没有key发生改变, 则直接返回
mcr.setDiskWriteResult(true);
return;
}
if (!mBackupFile.exists()) {
//当备份文件不存在, 则把mFile重命名为备份文件
if (!mFile.renameTo(mBackupFile)) {
mcr.setDiskWriteResult(false);
return;
}
} else {
mFile.delete(); //否则,直接删除mFile
}
}
try {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false);
return;
}
//将mMap全部信息写入文件
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (this) {
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
...
}
//写入成功, 则删除备份文件
mBackupFile.delete();
//返回写入成功, 唤醒等待线程
mcr.setDiskWriteResult(true);
return;
} catch (XmlPullParserException e) {
...
} catch (IOException e) {
...
}
//如果写入文件的操作失败, 则删除未成功写入的文件
if (mFile.exists()) {
if (!mFile.delete()) {
...
}
}
//返回写入失败, 唤醒等待线程
mcr.setDiskWriteResult(false);
}
mcr.changesMade这个只有在数据更新有变化的时候才会为true。
其次当备份文件不存在了,把mFile重命名成备份文件,所以写之前一定有一份备份文件,这也类似于我们写一句代码按下Ctrl+S哈哈,下来才是写的过程,将mMap的全部信息写入到文件按照XML格式。写成功了,删除备份,并且唤醒等待线程。
所以我们每一次commit都是将全部文件进行写入操作。所以耗时耗时耗时啊。
我们还有一种提交方式
那我们就看看这个操作:
public void apply() {
//把数据更新到内存
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
//进入等待状态
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
//将awaitCommit添加到QueuedWork
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
//从QueuedWork移除
QueuedWork.remove(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
//执行文件写入操作
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
final boolean isFromSyncCommit = (postWriteRunnable == null);
if (isFromSyncCommit) {
... //postWriteRunnable不为空
}
//将任务放入单线程的线程池来执行
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
首先我们的postWriteRunnable参数不为null了,其次我们看到线程池了。
public class QueuedWork {
private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =
new ConcurrentLinkedQueue<Runnable>();
public static void add(Runnable finisher) {
sPendingWorkFinishers.add(finisher);
}
public static void remove(Runnable finisher) {
sPendingWorkFinishers.remove(finisher);
}
public static void waitToFinish() {
Runnable toFinish;
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
toFinish.run();
}
}
public static boolean hasPendingWork() {
return !sPendingWorkFinishers.isEmpty();
}
}
所以区别就是,我们第二种方式用线程池来管理写的过程,这个过程首先会将awaitCommit放入QueuedWork队列当写执行完之后就会移除。
这个QueuedWork存在的意义是什么呢?具体这里我也不清楚,只是看网上的大虾,说用于在Stop Service, finish BroadcastReceiver过程用于 判定是否处理完所有的异步SP操作.
对比一下提交的两种方式:
apply | commit |
---|---|
没有返回值 | 有返回值可判断是否写入成功 |
apply是将修改提交到内存再异步提交到磁盘文件 | 同步的提交到磁盘文件 |
只是原子更新到内存,后调用apply函数会直接覆盖前面内存数据 | 多并发的提交commit时,需等待正在处理的commit数据更新到磁盘文件后才会继续往下执行 |
从对比中我们可以看出来一些合理的方式:
- 尽量不要在sp里面存储特别大的key/value, 有助于减少卡顿/anr
- 尽量批量apply与commit,不要频繁
- 不要使用MODE_MULTI_PROCESS模式
- 因为key是文件,所以可以拆分成多个文件,减少同步锁的竞争
- 不要连续多次edit(), 应该获取一次获取edit(),然后多次执行putxxx(), 减少内存波动; 经常看到大家喜欢封装方法, 结果就导致anr这种情况的出现.
- 每次commit时会把全部的数据更新的文件, 所以整个文件是不应该过大的, 影响整体性能;
- 不要一上来就执行getSharedPreferences().edit(), 应该分成两大步骤来做, 中间可以执行其他代码.
参考:辉辉大神的建议。