微信基于内存映射的KV组件MMKV源码分析

NS_SWIFT_NAME的用法

用于在OC中重新命名在Swift中的方法名
如下

+ (nullable instancetype)mmkvWithID:(NSString *)mmapID NS_SWIFT_NAME(init(mmapID:));

即在swift中,要以mmkv.init(mmapID:xxx)的方式来调用

类似@objc在swift中的用法

NSRecursiveLock递归锁的使用

这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中

NSLock *lock = [[NSLock alloc] init];
 
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 
    static void (^RecursiveMethod)(int);
 
    RecursiveMethod = ^(int value) {
 
        [lock lock];
        if (value > 0) {
 
            NSLog(@"value = %d", value);
            sleep(2);
            RecursiveMethod(value - 1);
        }
        [lock unlock];
    };
 
    RecursiveMethod(5);
});

以上代码会造成死锁,在第二次进入时,因为上次的锁没有解开,会等待造成死锁,将NSLock换成NSRecursiveLock即可正常使用, 递归锁会自动跟踪被lock的次数,允许同一线程多次加锁

使用C++的类来管理NSRecursiveLock

以下方法为构造方法,接受一个NSRecursiveLock对象,并调用其lock方法

CScopedLock(NSRecursiveLock *oLock) : m_oLock(oLock) { [m_oLock lock]; }

使用方法为初始化一个该类对象即可加锁

CScopedLock lock(g_instanceLock);

这里有个技巧,因为lock是临时变量,在函数结束时会释放调用其析构函数,解锁的操作就放在析构函数里,所以很巧妙啊

~CScopedLock() {
        [m_oLock unlock];
        m_oLock = nil;
    }

其中g_instanceLock为在+ (void)initialize中初始化的递归锁

+ (void)initialize
苹果官方对这个方法有这样的一段描述:这个方法会在 第一次初始化这个类之前 被调用,我们用它来初始化静态变量

相关的C函数

  • ftruncate调整文件大小为指定大小
  • fstat配合open用于获取打开的文件的状态,见如下,MMKV中用于取打开的映射文件的size
m_fd = open(m_path.UTF8String, O_RDWR, S_IRWXU);
m_size = 0;
struct stat st = {};
if (fstat(m_fd, &st) != -1) {
    m_size = (size_t) st.st_size;
}
  • mmap创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连,注意看下面这段代码
m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
    MMKVError(@"fail to mmap [%@], %s", m_mmapID, strerror(errno));
}

因为mmap的返回值是void *,所以MAP_FAILED代表((void *)-1)
表示执行失败.以32位为例,-1即0xFFFFFFFF,代表内存最高位.一般用来表示失败.

typedef预定义的一些数据类型

一般有后缀_t代表typedef重定义,常见以下

typedef    __signed char        int8_t;
typedef unsigned char uint8_t;
typedef    int            int32_t;
typedef unsigned int uint32_t;
typedef    long long        int64_t;
typedef unsigned long long uint64_t;

利用union模板实现数据类型转换

template <typename T, typename P>
union Converter {
    static_assert(sizeof(T) == sizeof(P), "size not match");
    T first;
    P second;
};

static inline int64_t Float64ToInt64(double v) {
    Converter<double, int64_t> converter;
    converter.first = v;
    return converter.second;
}

因为union的特性,second的值天生就是first的值.前提是需要保证在内存中的size相同.直接return就是模板中的类型,使用很简单如下

Float64ToInt64(value)

补充,Union还经常用来做大端和小端(Big endian ,Little endian)的判断,如下

int checkCPU()
{
    union w
    {
        int a;//在ios中,4 Byte
        char b;//在ios中,1 Byte
    } c;
    c.a = 1;
    return(c.b ==1);//如果c.b == 1,表示第一位是0x01,那就是小端,如果返回0,就是大端
}

iOS目前为小端序,一些网络传输要求为大端序时可以使用iOS自带方法来进行转换

UInt16  Byte = 0x1234;
HTONS(Byte);//转换
NSLog(@"Byte == %x",Byte);

取前4个byte转为Int长度

readRawLittleEndian32方法为取前4个byte转为int值表示长度,需要注意的是,byte转int时因为长度的问题不能直接转,要和0xFF(即二进制的11111111)做&运算,使高位为0,来保证int和byte的一致性.例如将0000 0001 转为 0000 0000 0000 0000 ... 0001
换句话说

当byte要转化为int的时候,高的24位必然会补1,这样,其二进制补码其实已经不一致了,&0xff可以将高的24位置为0,低8位保持原样。这样做的目的就是为了保证二进制数据的一致性

确保对应的数据文件属于NSFileProtectionCompleteUntilFirstUserAuthentication模式

在初始化调用loadFromFile方法的最后,检查对应数据文件的保护模式属于NSFileProtectionCompleteUntilFirstUserAuthentication,苹果官方的解释为

The file is stored in an encrypted format on disk and cannot be accessed until after the device has booted

这样,只要设备解锁即可进行修改,确保系统能将内存映射更新到物理存储.

正确使用Autorelease Pool来提高内存效率

关于Autorelease Pool,苹果官方有如下解释

At the end of the autorelease pool block, objects that received an autorelease message within the block are sent a release message

即在block结束时,block中的临时变量会收到一个release消息,这样可以即时地清除不需要的临时变量,降低内存峰值.苹果建议有如下几种情况可能需要用到Autorelease Pool

  • If you are writing a program that is not based on a UI framework, such as a command-line tool.
  • If you write a loop that creates many temporary objects.
  • You may use an autorelease pool block inside the loop to dispose of those objects before the next iteration. Using an autorelease pool block in the loop helps to reduce the maximum memory footprint of the application.
  • If you spawn a secondary thread.
    You must create your own autorelease pool block as soon as the thread begins executing; otherwise, your application will leak objects. (See Autorelease Pool Blocks and Threads for details.)

内存映射的核心方法mmap

void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
//实例如下
m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);

第一个参数addr如果为NULL,kernel会选择地址对齐来创建映射,大部分移植的mmap方法均使用该参数,如果addr不为空,则使用该地址来创建.

  • length为大小
  • prot 指文件操作权限,PROT_EXEC, PROT_READ, PROT_WRITE, PROT_NONE,示例中使用PROT_READ | PROT_WRITE代表读和写权限
  • flags代表其他进程是否可以映射同样的区域.示例中为允许
  • fd(file descriptor)
  • offset为偏移量

参考官方说明

利用哈希表unordered_map实现了基于LRU的淘汰算法

STL中,map 对应的数据结构是 红黑树。红黑树是一种近似于平衡的二叉查找树,里面的数据是有序的。在红黑树上做查找操作的时间复杂度为 O(logN)。而 unordered_map 对应 哈希表,哈希表的特点就是查找效率高,时间复杂度为常数级别 O(1), 而额外空间复杂度则要高出许多。所以对于需要高效率查询的情况,使用 unordered_map 容器。而如果对内存大小比较敏感或者数据存储要求有序的话,则可以用 map 容器

标签: iOS, mmap, mmkv, 微信源码, 开源, 源码解析

添加新评论