本文来自刘兆贤的博客_CSDN博客-Java高级,Android旅行,Android基础领域博主 ,引用必须注明出处!
先了解一种Android特有的存储机制,快写机制MMap。
MMap:Memory Mapping,内存映射(升级IPC数据传输)。将一块物理内存(可随时读写的),通过文件操作符,同时映射到用户虚拟内存空间,和内核虚拟地址空间(struct vm_struct *area,属于逻辑地址,区别于物理地址,可通过转换器计算)的一段内存块(两者大小相同)。
多线程:通过加锁(mutex_lock),保证每次只有一个进程被分配内存。
实现原理:App进程负责写数据,由操作系统负责将内存写入文件。用于快速存储,解决日志写入慢的问题。
代表性组件:MMKV,Logan
MMKV存储目录:/data/user/0/applicationId/files/mmkv
在我看来MMKV只有三个优势,
1、增量更新,对比全量更新每次覆盖文件,速度更快,防止数据丢失;
2、存储利用MMAP机制,效率高于文件IO操作;
3、多应用进程共享时,加了进程锁,而SP不具备;
4、如果非加一条就是使用protobuffer协议,数据量较xml更少。
缺点,当存储空间达到一定量级后,需要重新整理(如重复数据删除,key排序等)。
处理三个问题:指针增长、内存增长、内存整理。通过指针增长,其他进程获得数据变化情况。
SharePreference的实现类SharePreferenceImpl,使用CountDownLatch做同步,commit是先写入内存,同时同步写入磁盘,等待内存写入成功,就返回结果。而apply是异步写入磁盘。写内存和文件时都加锁。
https://developer.android.google.cn/guide/topics/manifest/service-element
经过LocalService和RemoteService实验,得知只要是当前APP开启的进程,数据均存储在data/data/applicationId/shared_prefs目录下。
Note: This class does not support use across multiple processes.这句话的意思,应该是非同一APP的进程,不能共享数据。
put/remove ,都是线程级同步操作。
SharePreferenceImpl类中取数据方法:
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
SharePreferenceImpl.EditorImpl
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
https://developer.android.google.cn/reference/android/content/SharedPreferences
MMKV对比SharePreference,前者使用ProtoBuffer协议、增量更新,后者使用XML协议、全量更新
SharePreference使用优化:如果同一时间需要存储很多数据,可以最后再apply,避免多余的任务;常用和不常用的数据,分开保存,避免不必要的IO处理。
MMKV:使用MMap做快速存储的组件,适用于需要日志写入系统。
特点:立马生效,不存在异步的问题。
写数据机制:key/value不断写在文件的后面(即使有相同的key),取值时从后往前查找,使用最新的值。默认映射内存为4kb,不足时扩充1倍。
区别于SharePreference每次都要重写文件,多进程时有几率造成数据丢失,而MMKV通过MMap映射到内核空间,使用文件锁的方式,可以解决此问题。
多进程时,需要解决数据同步的问题,采用文件互斥锁(同时只有一个线程拿到),而非pthead_mutex(Binder死亡通知方案-此通知不能自己处理,A、B进程互相注册对方死亡通知,A一直存在就有一个永久加锁的mutext,无法释放)。在健壮性的条件下,还要考虑锁升降级和递归加锁问题,文件锁不支持,但mutext缺点更明显。
每个进程自己拥有一个写指针,mmap内存中也有一个写指针,对比两者偏移量,来做同步;使用单调递增的序列号,来判断内存是否已经重整(原指针之前的数据全部失效,需要重新加载);重整后内存不足,才需要申请新的内存。
递归锁:属于状态锁,没有计数器,一次解锁全部解掉。
锁升级:共享锁升级到互斥锁,读锁升级到写锁,如果多个进程都要升级,则会出现互相等待的死锁情况。
因此,加写锁时,需要先加写锁,再释放读锁;解写锁时,同样要先加读锁,再解写锁。
https://github.com/Tencent/MMKV/wiki/android_ipc
使用:
默认明文保存,可加密,可换密钥,可转换为明文。
默认单进程模式,可设置多进程模式。
默认存放在FileDir目录下,可更换目录。
可备份和恢复。
问题:
某些设备找不到so库,可以用ReLinker(https://github.com/KeepSafe/ReLinker)来解决。