SharedPreference与MMKV

2021-09-17  本文已影响0人  Archer_J

SharedPreference

XML格式保存,使用Pull解析

创建SharedPreferencesImpl时解析数据,子线程使用Java IO读取整个文件,进行XML解析,并将所有数据存入内存Map集合,其他操作都需要等待初始化完成

commit同步提交,阻塞调用线程

apply异步提交,通过HandlerThread创建子线程

把Map中的数据,全部序列化为XML,覆盖文件保存

可能出现ANR

调用apply方法异步提交数据

// SharedPreferencesImpl#EditorImpl#apply
public void apply() {
    ...
  final Runnable awaitCommit = new Runnable(){
    ...
  }
  // 将runnable添加进队列中
    QueuedWork.addFinisher(awaitCommit);
  ...
  // 通过HandlerThread执行IO操作
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    ...
}
@Override
public void handleStopActivity(IBinder token, int configChanges,
      PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
   ...
   // 回调onStop
   performStopActivityInner(...);         
   ...
   // 阻塞等待队列执行完毕
   QueuedWork.waitToFinish();
}

优化方向(MMKV)

  1. 更高效的文件操作(mmap)

  2. 比XML更精简的数据格式(二进制、protobuf)

  3. 更优的数据更新方式(增量更新)

MMKV

MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。

Github

I/O

工作原理

image1.png

写文件流程

  1. 调用write向内核发起系统调用,上下文从用户态切换为内核态

  2. CPU将用户缓冲区的数据拷贝到内核空间的缓冲区(CPU拷贝)

  3. CPU利用 DMA 控制器将数据从内核缓冲区拷贝到磁盘缓冲区进行数据传输(DMA拷贝)

  4. 上下文从内核态切换回用户态,write系统调用执行返回

写文件经历了两次拷贝:

注:SP是基于I/O的存储方式

mmap(memory mapping 内存映射)

原理

Linux通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射。

image
  1. 对文件进行mmap,会在进程的虚拟内存分配地址空间,创建映射关系
  2. 实现这样的映射关系后,就可以采用指针的方式读写操作这一段内存,而系统会自动回写到对应的文件磁盘上

注:mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不同的繁琐过程

优势

注:MMAP是零拷贝的(不需要CPU参与的拷贝),也可理解为一次拷贝(/DMA拷贝)

Binder

Binder是基于mmap实现的跨进程通讯机制

Activity启动过程中,ZygoteInit.nativeZygoteInit() 调用c++代码创建Binder对象

具体过程为:

-> zygote fork 一个应用进程

-> RuntimeInit

-> ZyzoteInit

-> ProcessState(进程状态对象),一个进程会有一个ProcessState对象

// ZygoteInit#zygoteInit
public static final Runnable zygoteInit(int targetSdkVersion, String[] argv,
            ClassLoader classLoader) {
        // 为当前的VM设置未捕获异常器
    RuntimeInit.commonInit();
        // Binder驱动初始化,该方法完成后,可通过Binder进行进程通信
    ZygoteInit.nativeZygoteInit();
        // 主要调用SystemServer的main方法
    return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
}

ZygoteInit.nativeZygoteInit() 方法调用native创建ProcessState并创建了内存映射关系

image3.png

注:

DEFAULT_BINDER_VM_SIZE 为Binder传输数据的大小限制

_SC_PAGE_SIZE为一页,一般为4096个字节(4K)

所以:Binder默认能传输的大小为:1M - 8k,这里指的是同步方式

异步(aidl 指定 oneway):(1M-8K) / 2 = 500+K

同步:1M-8K

image2.png

通过mmap方法映射了一个虚拟文件:/dev/binder

mDriverFD位文件句柄

应用

微信Mars

美团Logan

网易七鱼

爱奇艺xCrash

Protobuf(变长编码)

protobuf 是google开源的一个序列化框架,类似xml,json,最大的特点是基于二进制,比传统的XML表示同样一段内容要短小得多。

MMKV正式基于protobuf协议进行数据存储,存储方式为增量更新,也就是不需要每次修改数据都要重新将所有数据写入文件了。

为什么使用二进制

一个字节 = 8位

整数1 = 4个字节32位,转化为二进制:0000 0000 0000 0000 0000 0000 0000 0001

如果使用二进制格式,即可用一个字节表示:0000 0001

数据更紧凑、精简了

场景:

http1:使用字符串文本格式传输

http2:使用二进制格式传输

数据结构

protobuf是二进制存储格式,第一位代表的是key和value的总长度,后面是key长度->key, value长度->value。。。。。 依次排列,可以用二进制查看工具来看一下:

image

写入方式

1个字节8位,低7位是数据位,第1位为标志位(0表示读取截止,1表示需要继续读取)

编解码过程看这里

扩容

Linux采用了分页来管理内存,存入数据先要创建一个文件,并要给这个文件分配一个固定的大小。如果存入了一个很小的数据,那么这个文件其余的内存就会被浪费。相反如果存入的数据比文件大,就需要动态扩容。

增量更新

将增量key-value对象序列化后,append 到内存文件

由于数据读取出来后,会放入一个map集合,这样后面的数据就会覆盖前面的数据,所以总能拿到最新的值

多进程

具体过程参考这里

flock文件锁

处理进程间的同步时使用了flock文件锁

文件锁的使用:

//通过open方法打开一个文件
string m_path;
int m_fd = open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU);
//通过文件句柄对文件上锁
int flock(m_fd, operation);

其中的参数operation是上锁的类型

LOCK_SH, 共享锁,多个进程可以同时使用,可以作为读锁

LOCK_EX, 排他锁,同时只允许一个进程使用,可以作为写锁

LOCK_UN, 解锁

LOCK_BN, 非阻塞请求, 与读写锁配合使用

使用flock对一个文件上读锁,或者写锁,都是会阻塞的,比如A进程持有一个文件的写锁,B进程想要对这个文件上写锁,就会阻塞住,如果不想被阻塞,可以配合LOCK_BN属性使用,即LOCK_BN | LOCK_EX.

flock有几个特点:

为了解决上面的问题,mmkv对文件锁进行了封装,增加了读写锁计数器,支持递归

文件校验

利用文件锁可以实现同一时间只有一个进程对file进行操作了,但是A进程修改了文件后,B进程怎么知道这个修改呢?

mmkv并没有去对保存key-value数据的那个文件枷锁,而是锁了个.crc校验文件.这个校验文件就是来解决上面的问题的.

struct MMKVMetaInfo {
    uint32_t m_crcDigest = 0;
    uint32_t m_version = 1;
    uint32_t m_sequence = 0; // full write-back count
    unsigned char m_vector[AES_KEY_LEN] = {0};
}

如果序列号不一致,说明发生了内存重整,重新读取整个文件

通过校验文件,在读取数据时,来做校验,就实现了多个进程的数据同步.

总结

上一篇下一篇

猜你喜欢

热点阅读