SharedPreference面试必会知识点及ANR
[TOC]
概要
SharedPreferences(简称SP)是Android中很常用的数据存储方式,SP采用key-value(键值对)形式, 主要用于轻量级的数据存储, 尤其适合保存应用的配置参数
使用
SharedPreferences sharedPreferences = getSharedPreferences("setting", Context.MODE_PRIVATE);
Editor editor = sharedPreferences.edit();
editor.putString("current_city", "北京");
editor.commit();
/data/data/<package_name>/shared_prefs/setting.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="current_city">北京</string>
</map>
整体结构
关键点
- 获取SP,没有文件会创建
- 获取SP实际会创建SharedPreferencesImpl,会将文件内容加载到内存
获取SP
getSharedPreferences
关键点:
- 没有xml的时候会创建文件
- 返回的SP实际是SharedPreferencesImpl
- SP文件打开后缓存位置
- ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache
- key是package_name
class ContextImpl extends Context {
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
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) {
//创建目录
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;
}
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
加载和初始化
- SharedPreference加载文件使用异步方式,可利用此预加载
- 将XML的KV读到一个map里,所以如果XML很大,会占用比较多的内存
- 每次初始化会把所有kv全部加载到内存,提交时会把全部的数据更新到文件,所以整个文件不应该过大,否则影响性能。
- 拆分:将使用频繁和非频繁、读频繁和写频繁的kv分开存储
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(); //唤醒处于等待状态的线程
}
}
读
如果没有加载完,会等待一个锁,调用线程被阻塞
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) {
// 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);
}
}
写
可以看到实际上创建了mModified的map,用于记录要修改的内容
public Editor edit() {
synchronized (this) {
awaitLoadedLocked();
}
return new 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;
}
}
}
落盘 commit apply
commit
关键点:
-
将内存的KV的map和mModified进行merge
-
commit执行是isFromSyncCommit,直接将落盘的runnable run了起来。是同步的过程。
-
commit有返回值
-
不要连续多次提交,应该获取一次获取edit(),然后多次执行putxxx()后批量提交,封装时尤其要注意
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);
}
//这里就是要写到磁盘的map
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;
}
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);
}
apply
实际使用HandlerThread去执行的
public void apply() {
final long startTime = System.currentTimeMillis();
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);
}
};
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);
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
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);
}
}
}
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;
}
}
该用哪一个
- apply() 没有返回值而 commit() 返回 boolean 表明修改是否提交成功
- commit() 是把内容同步提交到硬盘的,而 apply() 先立即把修改提交到内存,然后开启一个异步的线程提交到硬盘,并且如果提交失败,你不会收到任何通知。
- get 操作都是线程安全的, 并且 get 仅仅是从内存中 (mMap) 获取数据
问题
SharedPreferences 是线程安全的吗
- SharePreferences是线程安全的里面的方法有大量的synchronized来保障(效率低,很多synchronized),但不是进程安全
ANR
-
第一次读取完毕之前 所有的get/set请求都会被卡住 等待读取完毕后再执行,所以第一次读取会有ANR风险。
-
即使是apply,在没有落盘结束,也可能会ANR
总结
- SharedPreference加载文件使用异步方式,可利用此预加载
- 每次初始化会把所有kv全部加载到内存,提交时会把全部的数据更新到文件,所以整个文件不应该过大,否则影响性能。拆分原则:将使用频繁和非频繁的kv分开存储,将读频繁和写频繁的kv分开存储
- 不要连续多次提交,应该获取一次获取edit(),然后多次执行putxxx()后批量提交,封装时尤其要注意
- apply虽是异步提交,但会引起卡顿/ANR
SP的替代方案:MMKV
内存准备
通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
mmap回写磁盘时机
- 内存不足
- 进程退出
- 调用msync和munmap
- 一定延迟时间后
数据组织
数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。考虑到我们要提供的是通用 kv 组件,key 可以限定是 string 字符串类型,value 则多种多样(int/bool/double 等)。要做到通用的话,考虑将 value 通过 protobuf 协议序列化成统一的内存块(buffer),然后就可以将这些 KV 对象序列化到内存中。
转载自:https://juejin.cn/post/6982742727528022052