赞
踩
众所周知,SharedPreferences是一种轻型的Android数据存储方式,它的本质是基于XML文件存储key-value键值对数据,通常用来存储一些简单的配置信息。它的存储位置是在/data/data/<包名>/shared_prefs目录下。SharedPreferences对象本身只能获取数据而不支持存储和修改,存储修改是通过Editor对象实现。比较经典的使用方式例如用户输入框对过往登录账户的存储。
实现SharedPreferences存储的步骤如下:
1、根据Context获取SharedPreferences对象
2、利用edit()方法获取Editor对象。
3、通过Editor对象存储key-value键值对数据。
4、通过commit()或apply()方法提交数据。
1、apply没有返回值而commit返回boolean表明修改是否提交成功
2、apply是将修改数据原子提交到内存,而后异步真正提交到硬件磁盘;而commit是同步的提交到硬件磁盘,因此,在多个并发的提交commit的时候,他们会等待正在处理的commit保存到磁盘后在操作,从而降低了效率。而apply只是原子的提交到内存,后面有调用apply的函数的将会直接覆盖前面的内存数据,这样从一定程度上提高了很多效率。
3、apply方法不会提示任何失败的提示。
一般的话,建议使用apply,当然,如果是需要确保数据提交成功,且有后续操作的话,则需要用commit方法。
优点:
缺点:
建议:
/**
* 错误示例
* */
private void errorExample() {
SharedPreferences sharedPreferences = getSharedPreferences("MyID", MODE_PRIVATE);
sharedPreferences.edit().putInt("intId", 1).apply();
sharedPreferences.edit().putString("stringId", "stringId").apply();
Set<String> stringSet = new HashSet<>();
stringSet.add("stringSetTest");
sharedPreferences.edit().putStringSet("stringSetId", stringSet).apply();
sharedPreferences.edit().putBoolean("booleanId", true).apply();
sharedPreferences.edit().putLong("longId", 1).apply();
sharedPreferences.edit().putFloat("floatId", 1).apply();
}
/**
* 正确示例
* */
private void rightExample() {
SharedPreferences sharedPreferences = getSharedPreferences("MyID", MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putInt("intId", 1);
editor.putString("stringId", "stringId");
Set<String> stringSet = new HashSet<>();
stringSet.add("stringSetTest");
editor.putStringSet("stringSetId", stringSet);
editor.putBoolean("booleanId", true);
editor.putLong("longId", 1);
editor.putFloat("floatId", 1);
editor.commit();
}
诞生背景
它的最早的诞生,主要是因为在微信iOS端有一个重大的bug,一个特殊的文本可以导致微信的iOS端闪退,而且还出现了不止一次。为了统计这种闪退的字符出现频率以及过滤,但是由于出现的次数,发现原来的键值对存储组件NSUserDefaults根本达不到要求,会导致cell的滑动卡顿。因此iOS端就开始创造一个高新性能的键值对存储组件。Android端因为SharePreference的跨进程读写问题及性能问题,也开始复用iOS的MMKV,并进行了改进。
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。
性能对比
官方性能比较图:
1、Android端1000次的读写性能比较:
iOS端10000次的读写性能比较:
能看到mmkv比起我们开发常用的组件要快上数百倍。
GitHub地址:https://github.com/grandcentrix/tray
一个外国Android开发开源的一款Android SharePreference替代者,最早的版本是14年9月17号,最后一个版本是17年2月7号,GitHub2.3kstar,目前已经不再维护,网上资料也相对较少,不做进一步研究。
GitHub地址:https://github.com/mengdd/PreferencesProvider
Tray的使用者,APP被一个偶现的未解决的issue:https://github.com/grandcentrix/tray/issues/50影响,作者迟迟不发版解决,自己团队根据Tray进一步优化而诞生,只有一个版本,发布于四年前,GitHub46star,基本也是处于不维护的状态,网上资料也相对很少,不做进一步研究。
SharedPreferences是Android提供的数据持久化的一种手段,适合单进程、小批量的数据存储与访问。为什么这么说呢?因为SharedPreferences的实现是基于单个xml文件实现的,并且所有持久化数据都是一次性加载到内存,如果数据过大,是不合适采用SharedPreferences存放的。而适用的场景是单进程的原因同样如此,由于Android原生的文件访问并不支持多进程互斥,所以SharePreferences也不支持,如果多个进程更新同一个xml文件,就可能存在不互斥问题。
首先,从基本使用简单看下SharedPreferences的实现原理:
mSharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putString(key, value);
editor.apply();
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.
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();
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;
}
上述代码有几个重要的点,下面单独分析一下:
1、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更省内存,后续看看哪位大佬比较擅长这个,给大家普及分享一下各种array及map的原理源码及使用场景。
2、通过file拿到对应的sp的实现类实例
3、检查操作模式,看一下实现:
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");
}
}
}
Android N及以上版本跨进程的读写模式直接抛出安全异常,可见Google粑粑对应用安全方面的限制越来越严格了。
4、创建sp的实现类的实例,并加入到缓存中,以便下次能够快速的拿到。
5、当操作模式设置为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时会去检查文件状态是否改变,改变就重新加载数据到内存。
上面了解到getSharedPreferences返回的是SharedPreferencesImpl的实例,现在重点看一下SharedPreferencesImpl的实现:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
都是一些常规操作,初始化一些值,创建备份文件,重点看一下startLoadFromDisk
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
将变量mLoaded置为false,表示数据还没有加载成功,然后开启一个线程调用loadFromDisk
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}
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);
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) {
// An errno exception means the stat failed. Treat as empty/non-existing by
// ignoring.
} catch (Throwable t) {
thrown = t;
}
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
// It's important that we always signal waiters, even if we'll make
// them fail with an exception. The try-finally is pretty wide, but
// better safe than sorry.
try {
if (thrown == null) {
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
// In case of a thrown exception, we retain the old map. That allows
// any open editors to commit and store updates.
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll();
}
}
}
1、先判断备份文件是否存在,如果存在就删除当前文件,将备份文件重命名为正式文件。
2、然后创建文件输出流读取文件内存并转化为Map,注意这里创建带缓存的输出流时,指定的buffer大小为16k。
3、将读取到的Map赋值给mMap成员变量,如果map为空就创建一个空的HashMap,这里又是用到HashMap了,因为这里设计频繁查找或插入操作,而hashMap的查询和插入操作的效率是优于ArrayMap的
4、通知唤醒线程,有唤醒就有阻塞,看一下哪里阻塞了,全局搜索一下mLock.wait
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);
}
}
该方法在mLoaded为false的时候一直阻塞,而之前的notifyAll唤醒的就是此处的阻塞。再看一下awaitLoadedLocked在哪里被调用了。
@Override
public Map<String, ?> getAll() {
synchronized (mLock) {
awaitLoadedLocked();
//noinspection unchecked
return new HashMap<String, Object>(mMap);
}
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
@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;
}
}
@Override
public int getInt(String key, int defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public long getLong(String key, long defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Long v = (Long)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public float getFloat(String key, float defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Float v = (Float)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public boolean getBoolean(String key, boolean defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public boolean contains(String key) {
synchronized (mLock) {
awaitLoadedLocked();
return mMap.containsKey(key);
}
}
所有的get相关方法都被阻塞,直到完成数据从文件加载到内存的过程。因此当第一次调用sp的get相关
函数时是比较慢的,需要等待数据从文件被读取到内存,之后会比较快,因为是直接在内存中读取。
接下来看看put相关方法
@Override
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的实例。
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
EditorImpl是SharedPreferencesImpl的内部类,内部有一个HashMap保存被更改的键值对。
@Override
public Editor putBoolean(String key, boolean value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
@Override
public Editor remove(String key) {
synchronized (mEditorLock) {
mModified.put(key, this);
return this;
}
}
从以上两个方法可以知道,put方法就是向mModified添加一个键值对,remove方法添加的value为当前editor实例。它们都是被mEditorLock加锁保护的,有两个原因:
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
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;
}
1、commitToMemory实现
// Returns true if any changes were made
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 (mEditorLock) {
boolean changesMade = false;
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.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 (!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, keysModified, listeners,
mapToWriteToDisk);
}
就是把更改的键值对提交到内存中,即把mModified中的键值对更新到mapToWriteToDisk中,顺便获取被更新的键的集合以及外部设置监听器列表
2、enqueueDiskWrite
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
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);
}
主要工作就是把最新的数据写入到文件
@Override
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);
}
关键代码enqueueDiskWrite上面已经分析过,核心代码如下:
final boolean isFromSyncCommit = (postWriteRunnable == null);
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
此处可知isFromSyncCommit为false,queue参数取非为true,
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,这是一个内部维护消息机制的线程,任务是按照时间顺序依次执行的。
接下来看一下QueuedWorkHandler里面的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);
}
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方法既可。
注:1、SharedPreferences有大约万分之一的损坏率(网络数据)
2、一个 100KB 的 SharedPreferences 文件读取等待时间大约需要 50~100ms(网络数据),建议提前用异步线程预加载启动过程中用到的 SharedPreferences 文件。
mmkv其实和SharePrefences一样,有增删查改四种操作。MMKV作为一个键值对存储组件,也对了存储对象的序列化方式进行了优化。常用的方式比如有json,Twitter的Serial。而MMKV使用的是Google开源的序列化方案:Protocol Buffers。
Protocol Buffers & json对比:
下面进行比较几个对象序列化之间的要素比较:
要素 | Serial | JSON | Protocol Buffers |
正确性 | 优 | 优 | 优 |
时间开销 | 良(Json>性能>Serializable) | 良(性能<Protocol Buffers) | 优 |
空间开销 | 良(对象序列化,空间较大) | 良 (数据序列化,保留可读性牺牲空间) | 优(二进制压缩) |
开发成本 | 良(比起Serializable麻烦需要额外接入) | 良(对引用,继承支持有限) | 差(不支持对象之间引用和继承) |
兼容性 | 良(和平台相关) | 优(跨平台跨语言支持) | 优(跨平台跨语言支持) |
MMKV就是看重了Protocol Buffers的时间开销小,选择Protocol Buffers进行对象缓存的核心。
在 App 模块的 build.gradle 文件里添加:
dependencies {
implementation 'com.tencent:mmkv:1.0.22'
// replace "1.0.22" with any available version
}
3.2.2 初始化
MMKV 的使用非常简单,所有变更立马生效,无需调用 sync 、apply 。 在 App 启动时初始化 MMKV,设定 MMKV 的根目录(files/mmkv/),也可以直接使用默认目录:
MMKV.initialize(this);
public static String initialize(Context context) {
//默认根目录
String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
return initialize(root, (MMKV.LibLoader)null);
}
String dir = getFilesDir().getAbsolutePath() + "/mmkv_2";
MMKV.initialize(dir);
3.2.3 获取实例
// 获取默认的全局实例
MMKV mmkv = MMKV.defaultMMKV();
// 根据业务区别存储, 附带一个自己的 ID
MMKV mmkv = MMKV.mmkvWithID("MyID");
// 多进程同步支持
MMKV mmkv = MMKV.mmkvWithID("MyID", MMKV.MULTI_PROCESS_MODE);
3.2.4 CURD
// 添加/更新数据
mmkv.encode(key, value);
// 获取数据
int tmp = mmkv.decodeInt(key);
// 删除数据
mmkv.removeValueForKey(key);
3.2.5 SharedPreferences迁移到MMKV
MMKV mmkv = MMKV.mmkvWithID("MyID");
SharedPreferences sharedPreferences = getSharedPreferences("MyID", MODE_PRIVATE);
// 迁移旧数据
mmkv.importFromSharedPreferences(sharedPreferences);
// 清空旧数据
sharedPreferences.edit().clear().commit();
3.3 美菜商城实际应用
美菜商城跟美菜大客户目前使用的自己开发的storage SDK,内部使用的存储组件就是MMKV,下面我们来简单看一下使用情况。
/**
* Desc: 用来标注接口内的方法,定义sp存储字段的key
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SpKey {
String spKey();
}
/**
* Desc: 用来标注sharedPreference接口, tableName 为表名
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SpTable {
String tableName() default "";
}
3.3.2 SharedPreferences接口的返回操作
public interface Option <SpType>{
SpType get(SpType defValue);
SpType get();
void set(SpType value);
}
3.3.3 获取 SpTable interface 的实例
public static <T> T provideInstance(Context context, Class<T> spInterface) {
if (!spInterface.isInterface()) {
throw new IllegalStateException("spInterface must be an interface");
}
String spTableName = getTableNameFromInterface(spInterface);
final SpUtil spUtil = new SpUtil(context, spTableName);
@SuppressWarnings("unchecked")
T instance = (T) Proxy.newProxyInstance(spInterface.getClassLoader(), new Class<?>[]{spInterface}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getReturnType() != Option.class) {
throw new IllegalStateException("spInterface method return type must be Option.class but now it is " + method.getReturnType().getName());
}
final String spKey;
SpKey keyAnn = method.getAnnotation(SpKey.class);
if (keyAnn != null && !keyAnn.spKey().isEmpty()) {
spKey = keyAnn.spKey();
} else {
spKey = method.getName();
}
ParameterizedType optionType = ((ParameterizedType) method.getGenericReturnType());
Type optionGenericType = optionType.getActualTypeArguments()[0];
if (!(optionGenericType instanceof Class)) {
if (optionGenericType.toString().equals("java.util.Set<java.lang.String>")) { //Set<String>类型
return new Option<Set<String>>() {
@Override
public Set<String> get(Set<String> defValue) {
return spUtil.getStringSet(spKey, defValue);
}
@Override
public Set<String> get() {
return spUtil.getStringSet(spKey, Collections.<String>emptySet());
}
@Override
public void set(Set<String> value) {
spUtil.putStringSet(spKey, value);
}
};
}
throw new IllegalStateException("the saving type " + optionGenericType + " is not Shared preferences supported");
}
Class optionGenericClass = (Class)optionGenericType;
if (optionGenericClass == String.class) {
return new Option<String>() {
@Override
public String get(String defValue) {
return spUtil.getString(spKey, defValue);
}
@Override
public String get() {
return spUtil.getString(spKey, "");
}
@Override
public void set(String value) {
spUtil.putString(spKey, value);
}
};
} else if (optionGenericClass == Integer.class) {
return new Option<Integer>() {
@Override
public Integer get(Integer defValue) {
return spUtil.getInt(spKey, defValue);
}
@Override
public Integer get() {
return spUtil.getInt(spKey, 0);
}
@Override
public void set(Integer value) {
spUtil.putInt(spKey, value);
}
};
} else if (optionGenericClass == Long.class) {
return new Option<Long>() {
@Override
public Long get(Long defValue) {
return spUtil.getLong(spKey, defValue);
}
@Override
public Long get() {
return spUtil.getLong(spKey, 0);
}
@Override
public void set(Long value) {
spUtil.putLong(spKey, value);
}
};
} else if (optionGenericClass == Float.class) {
return new Option<Float>() {
@Override
public Float get(Float defValue) {
return spUtil.getFloat(spKey, defValue);
}
@Override
public Float get() {
return spUtil.getFloat(spKey, 0);
}
@Override
public void set(Float value) {
spUtil.putFloat(spKey, value);
}
};
} else if (optionGenericClass == Boolean.class) {
return new Option<Boolean>() {
@Override
public Boolean get(Boolean defValue) {
return spUtil.getBoolean(spKey, defValue);
}
@Override
public Boolean get() {
return spUtil.getBoolean(spKey, false);
}
@Override
public void set(Boolean value) {
spUtil.putBoolean(spKey, value);
}
};
} else {
throw new IllegalStateException("the saving type " + optionGenericClass + " is not Shared preferences supported");
}
}
});
return instance;
}
3.3.4 获取 SpTable interface的table名称
private static <T> String getTableNameFromInterface(Class<T> spInterface) {
SpTable tableAnn = spInterface.getAnnotation(SpTable.class);
if (tableAnn == null) {
throw new IllegalStateException("spInterface must have a SpTable annotation");
}
String spTableName = tableAnn.tableName();
if (spTableName.isEmpty()) {
spTableName = spInterface.getSimpleName();
}
return spTableName;
}
3.3.5 清空SharedPreferences&清除某一个key
public static void clearSpTable(Context context, String tableName) {
new SpUtil(context, tableName).clear();
}
public static <T> void clearSpTable(Context context, Class<T> spInterface) {
clearSpTable(context, getTableNameFromInterface(spInterface));
}
public static void remove(Context context, String tableName, String key) {
new SpUtil(context, tableName).remove(key);
}
public static <T> void remove(Context context, Class<T> spInterface, String key) {
remove(context, getTableNameFromInterface(spInterface), key);
}
3.3.6 保存跟清除的核心代码
public class SpUtil {
private SharedPreferences mSp;
private MMKV mmkv;
public SpUtil(Context context) {
this(context, context.getPackageName() + "_sp");
}
public SpUtil(Context context, String spTableName) {
Log.d("MyTag","--SpUtil spTableName : "+spTableName);
mSp = context.getSharedPreferences(spTableName, Context.MODE_PRIVATE);
mmkv = MMKV.mmkvWithID(spTableName);
mmkv.importFromSharedPreferences(mSp);
mSp.edit().clear().apply();
}
public void putString(String key, String value) {
// mmkv.edit().putString(key, value).apply();
mmkv.encode(key,value);
}
public String getString(String key, String defValue) {
// return mmkv.getString(key, defValue);
return mmkv.decodeString(key,defValue);
}
public void putInt(String key, int value) {
mSp.edit().putInt(key, value).apply();
mmkv.encode(key,value);
}
public int getInt(String key, int defValue) {
// return mmkv.getInt(key, defValue);
return mmkv.decodeInt(key,defValue);
}
public void putFloat(String key, float value) {
// mmkv.edit().putFloat(key, value).apply();
mmkv.encode(key,value);
}
public float getFloat(String key, float defValue) {
// return mmkv.getFloat(key, defValue);
return mmkv.decodeFloat(key,defValue);
}
public void putLong(String key, long value) {
// mmkv.edit().putLong(key, value).apply();
mmkv.encode(key,value);
}
public long getLong(String key, long defValue) {
// return mmkv.getLong(key, defValue);
return mmkv.decodeLong(key,defValue);
}
public void putBoolean(String key, boolean value) {
// mmkv.edit().putBoolean(key, value).apply();
mmkv.encode(key,value);
}
public boolean getBoolean(String key, boolean defValue) {
// return mmkv.getBoolean(key, defValue);
return mmkv.decodeBool(key,defValue);
}
public void putStringSet(String key, Set<String> value) {
// mmkv.edit().putStringSet(key, value).apply();
mmkv.encode(key,value);
}
public Set<String> getStringSet(String key, Set<String> defValue) {
// return mmkv.getStringSet(key, defValue);
return mmkv.decodeStringSet(key,defValue);
}
public void clear() {
// mmkv.edit().clear().apply();
mmkv.clearAll();
}
public void remove(String key) {
// mmkv.edit().remove(key).apply();
mmkv.remove(key).apply();
}
}
3.3.7 商城代码的实际使用
"mc_storage" : 'com.meicai.android.sdk:storage:0.0.5'
api rootProject.ext.dependencies["mc_storage"]
MMKV.initialize(this);
@Keep
@SpTable(tableName = "PurchaseSearchHistory")
public interface PurchaseSearchHistorySp {
Option<String> purchaseSearchHistory();
}
private PurchaseSearchHistorySp mPurchaseSearchHistorySp = SpManager.provideInstance(MainApp.getInstance(), PurchaseSearchHistorySp.class);
mPurchaseSearchHistorySp.purchaseSearchHistory().get("[]")
mPurchaseSearchHistorySp.purchaseSearchHistory().set(history);
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。