Box2D目录下主要包括四个文件夹,
Collision-碰撞相关代码
Common-通用代码,包含块内存分配,栈内存分配,计时器等。
Dynamics-Box2D世界,物体,形状等定义
Rope-绳连接的定义
还是一个一个攻破这些代码吧,先从通用代码common入手,其中负责内容分配的源码有以下四个文件。
b2BlockAllocator.cpp
b2BlockAllocator.h
b2StackAllocator.cpp
b2StackAllocator.h
本节分析块内存的分配。
虽然C++有自己的内存分配new,或者c的malloc,但是由于物理引擎中需要分配大量的小型对象。并且生命周期很短,甚至只有几个时间步,使用系统自带的内存分配效率比较低。所以Box2D自己写了内存分配的代码。
Box2D使用的是被称为小型对象分配器(SOA:Small-Object Allocator)的东西来分配这些对象的内存。后面会分析SOA的源码。
有关SOA的介绍可以看这本书的第四章http://download.csdn.net/detail/chen52671/6391805。不过文字为繁体,看着不顺畅,有了SOA,在代码使用过程中就可以更灵活的为大量的小空间分配内存。
为了便于理解,简要解释一下其工作原理。SOA内存分配时,以分配30字节为例,是首先查看之前分配的内存还有没有空闲的,有的话,直接返回空闲内存地址以供使用,如果没有空闲内容,就要重新分配一块内存,分配内存时,会分配一大块内容(chunk),然后按照分配内存的大小,如本例中的30字节,分配一个够其使用的大小的block。大小为32字节。于是就将chunk这大块内存,分配为n个大小为32字节的block。并将第一个block地址返回,第二个block地址标记为free(空闲的)。以供以后再需要调用,以后如果再分配的内存是在16-32字节之间,就会来这里找free的内存并直接返回。
当不需要这块内存的时候,则调用Free()函数释放这小块内存,但是不是真正的释放,而是将其再次标记为空闲的,加入到空闲内存链表中,以供下次需要分配内存时,不需要真正的分配内存,而是直接将标记为空闲内存返回使用。
当真正需要释放掉这块内存是,调用Clear()来释放所有的chunks大区块的内存。
b2BlockAllocator.cpp和b2BlockAllocator.h主要定义了块内存的快速分配和回收机制。b2BlockAllocator.h中定义了class b2BlockAllocator类,除了构造函数外,只有Allocate(),Free(),Clear()分别用来分配内存,释放内存和真正的释放。当然头文件中还包括一些全局变量和一些私有数据,这里罗列一下,为下面的阅读提供方便,或者在阅读遇到问题时,回来翻阅。当把整篇文字读完后,应该就会知道这些数据的目的了。
全局常量:
const int32 b2_chunkSize = 16 * 1024;//chunk区块大小,chunk是一大块内存区域,内部可以分配若干小的block。Chunk大小为16K。
const int32 b2_maxBlockSize = 640;//Block最大640字节。
const int32 b2_blockSizes = 14;//block一共有14种大小,从16字节到640字节
const int32 b2_chunkArrayIncrement = 128;//初始分配128个chunk,当不够用的话,再增加128个重新分配,以此类推。
b2BlockAllocator类的私有成员:
b2Chunk* m_chunks;//这些chunks的首地址,
int32 m_chunkCount;//使用的chunk计数。
int32 m_chunkSpace;//目前分配的chunk的个数初始为128个。
b2Block* m_freeLists[b2_blockSizes];//标记为空闲的block的链表,一个14个链表,给不同大小block使用。
static int32 s_blockSizes[b2_blockSizes];//保存区块大小种类的数组,一共14种大小,分别为16,32,64...最大为640
static uint8 s_blockSizeLookup[b2_maxBlockSize + 1];//会在构造函数里初始化,分配0-16字节时,blockSizeLookup值为0,
//16-32时,该值为1.以此类推。是为了查找分配不同大小内存是,其位于第几个空闲链表中。
static bool s_blockSizeLookupInitialized;//标记上面那个数组是否已经被初始化了。
这部分具体代码参考源码吧。
首先,初始化这几个值,值得大小上面有说过。
m_chunkSpace
m_chunkCount
m_chunks
其次,空闲区块列表m_freeLists初始化为空指针。
最后,保存区块大小种类的数组s_blockSizeLookup初始化。
这部分没代码还说不清楚。那
Allocate函数主要作用是,根据参数size,分配内存,并返回内存地址。
1,如果size比预设的最大值640还大,那直接进行分配
if (size > b2_maxBlockSize)
{
return b2Alloc(size);//该函数调用malloc(size);直接分配。
}
2,根据要分配的内存的大小,确定其出在空闲链表数组的第几个,index可以理解为目录,一共14个目录,分别代表不同大小的blocks。
index = s_blockSizeLookup[size];
3,如果空闲链表中有对应大小的区块,直接返回该空闲区块,并把空闲链表指针后移,指向下一块空闲区块
if (m_freeLists[index])
{
b2Block* block = m_freeLists[index];
m_freeLists[index] = block->next;
return block;
}
4,空闲链表没空闲的了,那就分配呗。分配的时候要先判断还有没有地方分配大内容块chunk?如果没有地方了,就增加m_chunks大小,原来是能容纳128个chunks的话,现在可以容纳256个了。这个分配不是分配chunks的内存,不用担心浪费时间和空间,这个只是一堆指针。
if (m_chunkCount == m_chunkSpace)
{
b2Chunk* oldChunks = m_chunks;
m_chunkSpace += b2_chunkArrayIncrement;
m_chunks = (b2Chunk*)b2Alloc(m_chunkSpace * sizeof(b2Chunk));
memcpy(m_chunks, oldChunks, m_chunkCount * sizeof(b2Chunk));
memset(m_chunks + m_chunkCount, 0, b2_chunkArrayIncrement * sizeof(b2Chunk));
b2Free(oldChunks);
}
5,现在m_chunks有地方了,在下一个chunk,分配一个16k的空间以供使用。
b2Chunk* chunk = m_chunks + m_chunkCount;
chunk->blocks = (b2Block*)b2Alloc(b2_chunkSize);
6,在这16k的空间中分配n多个blocks。这里不是真的分配内存,因为内存上一步就分配了,这里是把内存分割,并把分割好的blocks首地址保存到链表。
int32 blockSize = s_blockSizes[index];
chunk->blockSize = blockSize;
int32 blockCount = b2_chunkSize / blockSize;
b2Assert(blockCount * blockSize <= b2_chunkSize);
for (int32 i = 0; i < blockCount - 1; ++i)
{
b2Block* block = (b2Block*)((int8*)chunk->blocks + blockSize * i);
b2Block* next = (b2Block*)((int8*)chunk->blocks + blockSize * (i + 1));
block->next = next;
}
b2Block* last = (b2Block*)((int8*)chunk->blocks + blockSize * (blockCount - 1));
last->next = NULL;
7,最后呢,就是返回切割好的blocks的指针,也就是首地址-第一块blocks啦。并把后面的block加入到对应目录的空闲链表里,比如分配30字节的空间,则会分成32字节的block,index为1,也就是空闲链表数组m_freeLists的第二个链表。不同大小区块不冲突。
m_freeLists[index] = chunk->blocks->next;
++m_chunkCount;
return chunk->blocks;
其实free的过程和分配的过程是对应的。其会释放指针p指向的大小为size 的内存。
1,如果空间大于最大640字节,那直接调用b2Free来释放
if (size > b2_maxBlockSize)
{
b2Free(p);//会调用free(mem);来释放指针
return;
}
2,查找size这么大的内存,是放在哪个目录了
int32 index = s_blockSizeLookup[size];
b2Assert(0 <= index && index < b2_blockSizes);
3根据目录得到区块大小,如size为30,则blockSize 为32。然后需要验证p指针指向的这块地址是不是真的就是32字节大小。
int32 blockSize = s_blockSizes[index];
bool found = false;
for (int32 i = 0; i < m_chunkCount; ++i)
{
b2Chunk* chunk = m_chunks + i;
if (chunk->blockSize != blockSize)
{
b2Assert((int8*)p + blockSize <= (int8*)chunk->blocks ||
(int8*)chunk->blocks + b2_chunkSize <= (int8*)p);
}
else
{
if ((int8*)chunk->blocks <= (int8*)p && (int8*)p + blockSize <= (int8*)chunk->blocks + b2_chunkSize)
{
found = true;
}
}
}
b2Assert(found);
4,找到的话,把这个区块的内存都写为0xfd,(如果单就内存释放来说,这个没啥用,至于是否在其他地方有用,还没看),下一步就是把这块区块增加到第2个空闲列表数组m_freeLists[2]中,以供以后分配。
memset(p, 0xfd, blockSize);
b2Block* block = (b2Block*)p;
block->next = m_freeLists[index];
m_freeLists[index] = block;
Clear()会彻底把那些内存释放掉,比如一共128个chunk,那就分别那这些chunk都调用b2Free释放掉。并把指针指向0.
for (int32 i = 0; i < m_chunkCount; ++i)
{
b2Free(m_chunks[i].blocks);//调用free(mem);
}
m_chunkCount = 0;
memset(m_chunks, 0, m_chunkSpace * sizeof(b2Chunk));
memset(m_freeLists, 0, sizeof(m_freeLists));
析构函数和Clear函数差不多
b2BlockAllocator::~b2BlockAllocator()
{
for (int32 i = 0; i < m_chunkCount; ++i)
{
b2Free(m_chunks[i].blocks);
}
b2Free(m_chunks);
}
不需要指针指向0是因为这些指针变量都不存在了,自然不会有野指针产生了。
在上一篇文章的helloworld实例里,会调用CreateBody来创建物体。
b2Body* groundBody = world.CreateBody(&groundBodyDef);
CreateBody实际就是用到了b2BlockAllocator。看看CreateBody的主要函数。
void* mem = m_blockAllocator.Allocate(sizeof(b2Body));
b2Body* b = new (mem) b2Body(def, this);
首先会调用b2BlockAllocator的Allocate方法,分配b2Body那么大的内存给它。
然后会使用new运算符来为创建一个b2Body从堆中创建一个对象,不过这个对象是在指定位置创建的。使用new (mem) b2Body(def, this);可以指定在mem的内存位置来创建对象。这种方式也被称为定位构造(placement new)。b2Body的构造函数中并没有从堆中分配内存。
通过这种方法,可以方便的分配小块内存,在对象不需要使用的时候,
b->~b2Body();
m_blockAllocator.Free(b, sizeof(b2Body));
调用对象的析构函数后(析构函数没有从堆中释放内存的过程),调用b2BlockAllocator的free函数即可。轻松快捷。