Boost是一个可移植、免费开源的C++库,提供了大量实用的开发组件,而且由于对跨平台和C++标准的强调,其实现的功能几乎不依赖于操作系统和标准库外的其他组件,因此可以在任何支持C++的平台上运作良好。
Boost提供了一个对象池object_pool,它位于boost库的"boost/pool/object/_pool.hpp"中。这是一个泛型的对象池,能够针对指定类型的对象进行分配。一个对象池的声明和使用规范为如下结构:
object_pool的一大特色是可以针对不同的参数调用被分配对象的构造函数。可惜在Cocos2d-x对象生命周期管理中,对象的创建和初始化是分离的,大部分类的初始化都不在构造函数中完成,构造函数中仅仅作引用计数的初始化。这里也引入了一个新的问题,Cocos2d-x对象在引用计数为零的时候会自动触发delete。对于从对象池分配的对象来说,不能通过delete而必须通过destroy来删除。因此,在不修改引擎源码的前提下,我们需要在object_pool的基础上作一点小小的包装使其可以配合引擎的内存管理使用,相关代码如下:
由于做成了模板类的形式,类的实现就全部存在于头文件中了。在这个包装类中,我们仅仅做了一件事情--在分配对象的时候,同时将对象添加到一个数组中,数组会增加对象的一次引用计数,因此可以保证在正常使用的情况下,不会有对象会被触发delete操作。由此引出的便是,需要在合适的时候回收对象,否则对象池将持续增长直到耗尽内存。
在提供的内存释放函数freeObjects中,我们检查当前缓冲数组中每个元素的引用计数,对于引用计数为1的对象,表示已经没有其他对象在引用这个对象,将其回收归还到对象池中。值得注意的是,在释放对象的循环中,我们将一个待回收的对象retain后并没有release,这是对引用计数内存管理的一个小小破例,保证了该对象在从数组清理之后仍然不会触发delete操作。
另外,这里设计了一个回收的扫描步长,每次回收仅在数组中扫描一定数量的对象就返回。这样做的好处在于,我们可以将整个对象池的回收扫描分散到每一帧中,隐性地完成并发。这个步长可以根据工程规模和所需的清理频率进行调整,对于游戏中对象生成和销毁并不频繁的情况,可以设置一个较长的清理周期,在每次清理时设置一个较大的扫描步长以回收更多的对象,同时减轻计算压力。
模板化之后,实际上每个类对应了一个对象池,以硬编码的形式清理这些对象池是十分费劲的,因此我们再在此基础上扩展一个管理器,管理这些对象池的清理。
15.5 对象池实现(2)
首先,需要做的是将回收操作分离抽象。我们定义一个接口并让MTPoolFromBoost继承,这样就能够在运行时用统一接口调用内存池回收对象:
这样抽象的目的是将所有用到的对象池添加到数组内,以便统一管理。首先,为管理器封装一个获取对象指针的函数:
在管理器中我们设计了一个获取对象的接口函数getObject,可以根据传入的指针类型调用相应类型的对象池获得对象。这里我们设置一个静态变量,使用这个变量的构造函数添加当前对象池到类的对象池数组中。由于这个函数是模板化的,最终将把每种调用到的对象池添加到管理器的对象池数组中。这样设计的另一个好处是,管理器调用某一类型的对象池之前,不会在管理器的清理函数中触发该对象池的清理。
而在管理器的清理函数中,可以获取每一个曾经使用过的管理器,调用其清理接口清理对象。 最后,我们只需要在程序初始化完毕后添加该管理器到引擎的定时触发器中:
然后介绍安装!
目录(?)[+]
http://sourceforge.net/projects/boost/files/boost/1.45.0/
下载.7z或.zip都可以。
以上是我用的命令,意思是完全编译所有的二进制库,但不重新安装头文件。bjam的其他参数如下:
stage/install: stage表示只生成库(dll和lib),install还会生成包含头文件的include目录。本人推荐使用stage,因为install生成的这个include目录实际就是boost安装包解压缩后的boost目录(E:/SDK/boost_1_39_0/boost,只比include目录多几个非hpp文件,都很小),所以可以直接使用,而且不同的IDE都可以使用同一套头文件,这样既节省编译时间,也节省硬盘空间。
toolset: 指定编译器,可选的如borland、gcc、msvc(VC6)、msvc-9.0(VS2008)等。
without/with: 选择不编译/编译哪些库。因为python、mpi等库我都用不着,所以排除之。另外,wave、graph、math、regex、test、program_options、serialization、signals这几个库编出的静态lib都非常大,所以不需要的也可以without掉。这可以根据各人需要选择,默认是全部编译。但是需要注意,如果选择编译python的话,是需要python语言支持的,应该到python官方主页http://www.python.org/下载安装。
stagedir/prefix: stage时使用stagedir,install时使用prefix,表示编译生成文件的路径。
build-dir: 编译生成的中间文件的路径。这个本人这里没用到,默认就在根目录(E:/SDK/boost_1_39_0)下,目录名为bin.v2,等编译完成后可将这个目录全部删除(没用了),所以不需要去设置。
link: 生成动态链接库/静态链接库。生成动态链接库需使用shared方式,生成静态链接库需使用static方式。一般boost库可能都是以static方式编译,因为最终发布程序带着boost的dll感觉会比较累赘。
runtime-link: 动态/静态链接C/C++运行时库。同样有shared和static两种方式,这样runtime-link和link一共可以产生4种组合方式,各人可以根据自己的需要选择编译。一般link只选static的话,只需要编译2种组合即可,即link=static runtime-link=shared和link=static runtime-link=static。
threading: 单/多线程编译。一般都写多线程程序,当然要指定multi方式了;如果需要编写单线程程序,那么还需要编译单线程库,可以使用single方式。
debug/release: 编译debug/release版本。一般都是程序的debug版本对应库的debug版本,所以两个都编译。
下面介绍如何使用!
Boost库的pool提供了一个内存池分配器,用于管理在一个独立的、大的分配空间里的动态内存分配。Boost库的pool主要适用于快速分配同样大小的内存块,尤其是反复分配和释放同样大小的内存块的情况。使用pool内存池主要有以下两个优点:
1. 能够有效地管理许多小型对象的分配和释放工作,避免了自己去管理内存而产生的内存碎片和效率低下问题。
2. 告别程序内存泄漏的烦恼,pool库会在内部对内存自动进行管理,避免了程序员一不小心而造成的内存泄漏问题。
pool库主要提供了四种内存池接口,分别是pool、object_pool、singleton_pool和pool_allocator(fast_pool_allocator)。
1. pool
pool是最简单也最容易使用的内存池类,可以返回一个简单数据类型(POD) 的内存指针。它
pool很容易使用,可以像C中的malloc()一样分配内存,然后随意使用。除非有特殊要求,否则不必对分配的内存调用free()释放,pool会很好地管理内存。例如:
int main()
{
pool<> pl(sizeof(int)); //一个可分配int的内存池
int *p = (int *)pl.malloc(); //必须把void*转换成需要的类型
assert(pl.is_from(p));
pl.free(p); //释放内存池分配的内存块
for (int i = 0;i < 100; ++i) //连续分配大量的内存
{
pl.ordered_malloc(10);
}
}
2. object_pool
object_pool是用于类实例(对象)的内存池,它的功能与pool类似,但会在析构时对所有已经分配的内存块调用析构函数,从而正确地释放资源。
malloc()和free()函数分别分配和释放一块类型为ElementType*的内存块,同样,可以用is_from()来测试内存块的归属,只有是本内存池分配的内存才能被free()释放。但它们被调用时并不调用类的构造函数和析构函数,也就是说操作的是一块原始内存块,里面的值是未定义的,因此我们应当尽量少使用malloc()和free()。
object_pool的特殊之处是construct()和destroy()函数,这两个函数是object_ pool的真正价值所在。construct()实际上是一组函数,有多个参数的重载形式(目前最多支持3个参数,但可以扩展),它先调用malloc()分配内存,然后再在内存块上使用传入的参数调用类的构造函数,返回的是一个已经初始化的对象指针。destory()则先调用对象的析构函数,然后再用free()释放内存块。
这些函数都不会抛出异常,如果内存分配失败,将返回0。
object_pool的用法也是很简单,我们既可以像pool那样分配原始内存块,也可以使用construct()来直接在内存池中创建对象。当然,后一种使用方法是最方便的,也是本书所推荐的。
下面的代码示范了object_pool的用法:
#include
using namespace boost;
struct demo_class //一个示范用的类
{
public:
int a,b,c;
demo_class(int x = 1, int y = 2, int z = 3):a(x),b(y),c(z){}
};
int main()
{
object_pool
demo_class *p = pl.malloc(); //分配一个原始内存块
assert(pl.is_from(p)); //p指向的内存未经过初始化
assert(p->a!=1 || p->b != 2 || p->c !=3);
p = pl.construct(7, 8, 9); //构造一个对象,可以传递参数
assert(p->a == 7);
object_pool
for (int i = 0; i < 10 ; ++i) //连续分配大量string对象
{
string *ps = pls.construct("hello object_pool");
cout << *ps << endl;
}
} //所有创建的对象在这里都被正确析构、释放内存
3. singleton_pool
singleton_pool与pool的接口完全一致,可以分配简单数据类型(POD)的内存指针,但它是一个单件,并提供线程安全。
singleton_pool主要有两个模板类型参数(其余的可以使用缺省值)。第一个Tag仅仅是用于标记不同的单件,可以是空类,甚至是声明(这个用法还被用于boost.exception,参见4.9小节,136页)。第二个参数RequestedSize等同于pool构造函数中的整数requested_ size,指示pool分配内存块的大小。
singleton_pool的接口与pool完全一致,但成员函数均是静态的,因此不需要声明singleton_pool的实例 ,直接用域操作符::来调用静态成员函数。因为singleton_pool是单件,所以它的生命周期与整个程序同样长,除非手动调用release_memory()或purge_memory(),否则singleton_pool不会自动释放所占用的内存。除了这两点,singleton_pool的用法与pool完全相同。
下面的代码示范了singleton_pool的用法:
#include
using namespace boost;
struct pool_tag{}; //仅仅用于标记的空类
typedef singleton_pool
int main()
{
int *p = (int *)spl::malloc(); //分配一个整数内存块
assert(spl::is_from(p));
spl::release_memory(); //释放所有未被分配的内存
} //spl的内存直到程序结束才完
singleton_pool在使用时最好使用typedef来简化名称,否则会使得类型名过于冗长而难以使用。如代码中所示:
typedef singleton_pool
用于标记的类pool_tag可以再进行简化,直接在模板参数列表中声明tag类,这样可以在一条语句中完成对singleton_pool的类型定义,例如:
typedef singleton_pool