Android平台MMKV的原理及实现

2021-08-25  本文已影响0人  Peakmain

前言

Protobuf协议

什么是Protobuf

数据结构

image.png

PortoBuf写入方式

上述步骤结束之后拿到数据

1、1010 0010
2、1011 1011
3、1011 1101
4、0000 0010

代码实现
上面写入方式了解之后,看起来还是挺简单,但是代码怎么写呢?

    1111 1111 1111 1111 1111 1111 1000 0000            (0xffffffff<<7)
&   0000 0000 0000 0000 0000 0000 0110 1110            (110)
=   0000 0000 0000 0000 0000 0000 0000 0000            0

假设我们的value现在是150,因为值已经大于0x7F(也就是上述不成立),这时候我们需要将value&(0xFFFFFFFF<<14),如果等于0则表示需要2个字节

    1111 1111 1111 1111 1100 0000 0000 0000            (0xffffffff<<14)
&   0000 0000 0000 0000 0000 0000 1001 0110            (150)
=   0000 0000 0000 0000 0000 0000 1000 0000            0

以此推论,最终我们可以写出如下代码

int32_t ProtoBuf::computeInt32Size(int32_t value) {
    //0xffffffff 表示 uint 最大值
    //<< 7 则低7位变成0 与上value
    //如果value只要7位就够了则=0,编码只需要一个字节,否则进入其他判断
    if ((value & (0xffffffff << 7)) == 0) {
        return 1;
    } else if ((value & (0xffffffff << 14)) == 0) {
        return 2;
    } else if ((value & (0xffffffff << 21)) == 0) {
        return 3;
    } else if ((value & (0xffffffff << 28)) == 0) {
        return 4;
    }
    return 5;
}

首先key的长度其实也就是

int32_t keyLength = key.length();

然后保存key的长度+key内容的长度:

 int32_t size = keyLength + ProtoBuf::computeInt32Size(keyLength);

value的长度+value内容的长度

 size += value->length() + ProtoBuf::computeInt32Size(value->length());

所以获取key+value大小的完整代码

int32_t ProtoBuf::computeItemSize(std::string key, ProtoBuf *value) {
    int32_t keyLength = key.length();
    // 保存key的长度与key数据需要的字节
    int32_t size = keyLength + ProtoBuf::computeInt32Size(keyLength);
    // 加上保存value的长度与value数据需要的字节
    size += value->length() + ProtoBuf::computeInt32Size(value->length());
    return size;
}
 if (value <= 0x7f) {
            writeByte(value);
            return;
 }
void ProtoBuf::writeByte(int8_t value) {
    if (m_position == m_size) {
        //满啦,出错啦
        return;
    }
    //将byte放入数组
    m_buf[m_position++] = value;
}

如果key数据的长度是字符串150,因为此时大于0x7f,将150转成字符串 1001 1000 ,首先记录低七位,

(value & 0x7F)

将第一位的数据变成1,再移除低7位

            writeByte((value & 0x7F) | 0x80);
            //7位已经写完了,处理更高位的数据
            value >>= 7;

原理如下

         0111 1111            (0x7F)
&        1001 1000             (150)
=        0001 1000           
|        1000 0000             (0X80)
=        1001 1000

此时key的长度已经全部写完,那key的内容怎么写呢,其实也很简单,直接将key的内容拷贝到数组就可以了

  memcpy(m_buf + m_position, data->getBuf(), numberOfBytes);

因此写入string数据的完整代码可以写成如下

void ProtoBuf::writeByte(int8_t value) {
    if (m_position == m_size) {
        //满啦,出错啦
        return;
    }
    //将byte放入数组
    m_buf[m_position++] = value;
}

void ProtoBuf::writeRawInt(int32_t value) {
    while (true) {
        //每次处理7位数据,如果写入的数据 <= 0x7f(7位都是1)那么使用7位就可以表示了
        if (value <= 0x7f) {
            writeByte(value);
            return;
        } else {
            //大于7位,则先记录低7位,并且将最高位置为1
            //1、& 0x7F 获得低7位数据
            //2、| 0x80 让最高位变成1,表示超过1个字节记录整个数据
            writeByte((value & 0x7F) | 0x80);
            //7位已经写完了,处理更高位的数据
            value >>= 7;
        }
    }
}
void ProtoBuf::writeString(std::string value) {
    size_t numberOfBytes = value.size();
    writeRawInt(numberOfBytes);
    memcpy(m_buf + m_position, value.data(), numberOfBytes);
    m_position += numberOfBytes;
}
    if ((tmp >> 7) == 0) {
        return tmp;
    }

如果最高位1代表还有数据,我们首先读取低7位的数据

 int32_t result = tmp & 0x7f;

再读取一个字节,将后面的读取到字节左移7位拼接到上一个数据的低7位

int32_t ProtoBuf::readInt() {
    uint8_t tmp = readByte();
    //最高1位为0  这个字节是一个有效int。
    if ((tmp >> 7) == 0) {
        return tmp;
    }
    //获得低7位数据
    int32_t result = tmp & 0x7f;
    int32_t i = 1;
    do {
        //再读一个字节
        tmp = readByte();
        if (tmp < 0x80) {
            //读取后一个字节左移7位再拼上前一个数据的低7位
            result |= tmp << (7 * i);
        } else {
            result |= (tmp & 0x7f) << (7 * i);
        }
        i++;
    } while (tmp >= 0x80);
    return result;
}
int8_t ProtoBuf::readByte() {
    if (m_position == m_size) {
        return 0;
    }
    return m_buf[m_position++];
}

至此ProtoBuf的读取和写入都已经基本差不多了,我们来看mmap

内存映射MMAP

SharedPreferences的弊端
页、页框、页表
mmap
 void * mmap(void *addr, size_t len, int prot, int flags, int fd, off_t of    fset); 
核心代码的实现

具体代码大家可以看我的github:https://github.com/Peakmain/Video_Audio/tree/master/app/src/main/cpp/src/mmkv/MMKV.cpp

int32_t DEFAULT_MMAP_SIZE =getpagesize();
void MMKV::initializeMMKV(const char *path) {
    g_rootDir = path;
    //创建文件夹
    mkdir(g_rootDir.c_str(), 0777);
}

MMKV::MMKV(const char *mmapID) {
    m_path = g_rootDir + "/" + mmapID;
    loadFromFile();
}

MMKV *MMKV::defaultMMKV() {
    MMKV *kv = new MMKV(DEFAULT_MMAP_ID);
    return kv;
}
    m_fd = open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU);
    //获取文件的具体大小
    struct stat st = {0};
    if (fstat(m_fd, &st) != -1) {
        m_size = st.st_size;
    }
    if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {
        //调整为4k整数倍
        int32_t oldSize = m_size;
        //新的4k整数倍
        m_size = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
        if (ftruncate(m_fd, m_size) != 0) {
            m_size = st.st_size;
        }
        //如果文件大小被增加了, 让增加这些大小的内容变成空
        zeroFillFile(m_fd, oldSize, m_size - oldSize);
    }
    m_ptr = static_cast<int8_t *>(mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0));
  memcpy(&m_actualSize, m_ptr, 4);
    if (m_actualSize > 0) {
        ProtoBuf inputBuffer(m_ptr + 4, m_actualSize);
        //清空
        map.clear();
        //已有的数据添加到Map
        while (!inputBuffer.isAtEnd()) {
            std::string key = inputBuffer.readString();
            LOGE("key=%s ", key.c_str());
            if (key.length() > 0) {
                ProtoBuf *value = inputBuffer.readData();
                if (value && value->length() > 0) {
                     //相当于java的Hashmap的add
                    map.emplace(key, value);
                }
            }
        }
    }
    m_output = new ProtoBuf(m_ptr + 4 + m_actualSize,
                            m_size - 4 - m_actualSize);

这里有人可能不懂为什么这里是m_ptr + 4 + m_actualSize,这里的目的是将我们的buf指向没有被填写的位置

void MMKV::putInt(const std::string &key, int32_t value) {
    //value需要几个字节
    int32_t size = ProtoBuf::computeInt32Size(value);
    ProtoBuf *buf = new ProtoBuf(size);
    buf->writeRawInt(value);
    map.emplace(key, buf);
    appendDataWithKey(key, buf);
}

2、计算待写入数据的大小(也就是key+key的长度+value+value的长度),如果当前待写入数据的大小大于剩余的空间大小,就需要扩大内存。如果内存足够则直接放入数据即可

void MMKV::appendDataWithKey(std::string key, ProtoBuf *value) {
    //待写入数据的大小
    int32_t itemSize = ProtoBuf::computeItemSize(key, value);
    if (itemSize > m_output->spaceLeft()) {
        //内存不够
        //计算map的大小
        int32_t needSize = ProtoBuf::computeMapSize(map);
        //加上总长度
        needSize += 4;
        //扩容的大小
        //计算每个item的平均长度
        int32_t avgItemSize = needSize / std::max<int32_t>(1, map.size());
        int32_t futureUsage = avgItemSize * std::max<int32_t>(8, (map.size() + 1) / 2);
        if (needSize + futureUsage >= m_size) {
            int32_t oldSize = m_size;
            //如果在需要的与将来可能增加的加起来比扩容后还要大,继续扩容
            do {
                //扩充一倍
                m_size *= 2;

            } while (needSize + futureUsage >= m_size);
            //重新设定文件大小
            ftruncate(m_fd, m_size);
            zeroFillFile(m_fd, oldSize, m_size - oldSize);
            //解除映射
            munmap(m_ptr, oldSize);
            //重新映射
            m_ptr = (int8_t *) mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
        }
        m_actualSize = needSize - 4;
        memcpy(m_ptr, &m_actualSize, 4);
        LOGE("extending  full write");
        delete m_output;
        m_output = new ProtoBuf(m_ptr + 4,
                                m_size - 4);
        auto iter = map.begin();
        for (; iter != map.end(); iter++) {
            auto k = iter->first;
            auto v = iter->second;
            m_output->writeString(k);
            m_output->writeData(v);
        }
    } else {
        //内存够
        m_actualSize += itemSize;
        memcpy(m_ptr, &m_actualSize, 4);
        m_output->writeString(key);
        m_output->writeData(value);
    }
int32_t MMKV::getInt(std::string key, int32_t defaultValue) {
    auto itr = map.find(key);
    if (itr != map.end()) {
        ProtoBuf *buf = itr->second;
        int32_t returnValue = buf->readInt();
        //多次读取,将position还原为0
        buf->restore();
        return returnValue;
    }
    return defaultValue;
}
上一篇 下一篇

猜你喜欢

热点阅读