原文:http://hielvis.com/2014/05/24/cocos2d-x-refptr/
至此,我们学习了Cocos2d-x内存管理的一些机制,它包含了很多概念,为了理清其中的关系,我们再将所有相关知识点进行梳理一下。
Cocos2d-x中所有内存管理方式的基础是引用计数,动态分配一个Ref对象后其引用计数为1,并通过retain和release来增持和减少其引用计数。引用计数本身并不能帮助我们进行内存管理。
为了正确地释放对象的内存,Cocos2d-x使用Objective-C里面的自动回收池的机制来管理对象内存的释放。Autorelease有点类似于一个共享的”智能指针”,该”智能指针”的作用域为一帧,该帧结束后,它将释放自己的引用计数,此时,如果该对象没有被其他”共享指针”引用,则对象被释放。如果对象被引用,则保留。
Vector和Map<K,V>通常用来和autorelease一起工作,我们通常应该将一个autorelease对象加入到Vector或者Map中,例如Node将所有的子元素存储在一个Vector<Node*>中。Vector和Map对新加入的元素执行retain操作,并对从中移除的元素执行release操作,这样元素在从Vector或者Map中移除的时候就会被自动释放。
对于单个的非集合元素对象,我们往往不会通过autorelease来进行管理,除非它是一个临时对象。这个时候我们只能手动使用retain和release来进行管理,这其实等价于通过new和delete来进行内存管理,这样的情况就容易导致内存管理问题。
因此,Cocos2d-x 在3.1中引入了智能指针RefPtr,RefPtr是基于RAII[引用6]实现的,RAII的全称为Resource Acquisition Is Initialization,是由C++之父Bjarne Stroustrup提出的管理动态内存的方法。在RAII中,动态资源的持有发生在一个对象的生命周期之内,即是说在对象的构造函数中分配内存,在对象的析构函数中释放内存。这就是我们前面讲述的将动态分配的内存映射到一个自动变量上,通过自动变量的构造函数和析构函数来分配和释放内存。这可以保证资源始终会被释放,即使出现异常,也能被正常释放。这也是各种智能指针如std::shared_ptr实现的基本原理。
RefPtr实际上是模仿C++11中的std::shared_ptr实现的,它保持着一个Ref*对象的强引用,并使用Cocos2d-x自身的引用计数来管理多个智能指针对内存的共享。与shared_ptr相比,它更轻量级,并且能够结合Cocos2d-x的内存管理模型,但是它不保证线程安全,因此比shared_ptr更高效。但是,Cocos2d-x并没有提供与std::unique_ptr和std::weak_ptr类似功能的智能指针。
3.2.6.1 构造函数
RefPtr需要依赖于Ref的引用计数来管理内存,所有类型T必须是一个Ref类型,Cocos2d-x通过静态转换static_const来在编译时进行类型检查。
RefPtr提供了几个重载的构造函数,由于RefPtr变量和Ref指针是一种强引用关系,所以这些构造函数会对任何不为nullptr的Ref指针增持其引用计数,除非它是一个右值。例如:
// 转换函数
RefPtr<__String> ref2(cocos2d::String::create(“Hello”));
CC_ASSERT(strcmp(“Hello”,ref2->getCString()) == 0);
CC_ASSERT(2 == ref2->getReferenceCount());
// 复制构造函数
RefPtr<__String> ref4(ref2);
CC_ASSERT(strcmp(“Hello”,ref4->getCString()) == 0);
CC_ASSERT(3 == ref2->getReferenceCount());
CC_ASSERT(3 == ref4->getReferenceCount());
在C++中,只有一个参数的构造函数可以看做一个转换函数,在上面的例子中,类型T*的转换函数对T*引用计数执行了+1,而对于左值的ref2使用的复制构造函数也会对引用的内存执行引用计数+1。通过复制构造函数和转换函数,多个RefPtr可以共享一个Ref对象,并且它们各自均保持对Ref的强引用关系。
而对于右值的复制构造函数则不会增加其引用计数,因为通常对于返回右值的方法,该方法通常不再负责对该对象的内存进行管理,这个时候接受者不应该是共享的一方,而应该是将其对内存的占用转移过来,例如:
RefPtr<__String> getRefPtr()
{
RefPtr<__String> ref2(cocos2d::String::create(“Hello”));
CC_ASSERT(strcmp(“Hello”,ref2->getCString()) == 0);
CC_ASSERT(2 == ref2->getReferenceCount());
return ref2;
}
// 移动复制构造函数
RefPtr<__String> ref4(getRefPtr());
CC_ASSERT(strcmp(“Hello”,ref4->getCString()) == 0);
CC_ASSERT(2 == ref4->getReferenceCount());
方法getRefPtr()返回一个右值的RefPtr<__String>智能指针,移动复制构造函数被调用,对返回对象的内存的管理被转移而不是被共享,其不会增加右值的引用计数。
此外,我们可以使用三种特殊的方式来构造一个空的智能指针:
// 默认构造函数
RefPtr ref1;
CC_ASSERT(nullptr == ref1.get());
// 使用空指针参数构造
RefPtr<__String> ref3(nullptr);
CC_ASSERT((__String*) nullptr == ref3.get());
// 使用空引用的智能指针复制构造
RefPtr ref5(ref1);
CC_ASSERT((Ref*) nullptr == ref5.get());
3.2.6.2 赋值操作符
与构造函数类似,任何左值变量的赋值,RefPtr都应该与该左值共享资源而增持其引用计数,而对于右值,仍然应该使用转移而不是共享。与构造函数不同的是,赋值操作符除了会增持其资源的引用计数,还会释放对之前旧的资源的引用计数。
前面RefPtr定义了一个对类型T*的转换函数,在C++中,该转换函数会被用来执行强制转换或者赋值的隐士转换,例如:
RefPtr<__String> ptr=cocos2d::String::create(“Hello”);
实际上会调用T*到RefPtr的转换构造函数,这却不是我们想要的,因为ptr变量可能正持有其他的资源。因此RefPtr提供了对T*的赋值操作符重载:
template class RefPtr
{
public:
inline RefPtr & operator = (T * other)
{
if (other != _ptr)
{
CC_REF_PTR_SAFE_RETAIN(other);
CC_REF_PTR_SAFE_RELEASE(_ptr);
_ptr = const_cast::type*>(other);
}
return *this;
}
};
使得在对T*进行转换的时候不会直接调用转换方法,这样就可以对旧的资源进行释放。此外,也可以使用nullptr来让RefPtr成为一个空的智能指针。
3.2.6.3 弱引用赋值
不管是复制构造函数,还是赋值操作符,RefPtr会对任何非空的左值的资源保持一种强引用的关系。而有时候对于左值的资源我们仍然可能希望保持一种弱引用关系,例如:
RefPtr image;
image = new cocos2d::Image();
image->release();
如果对于左值的image对象,可以基于弱引用来构造智能指针,则会使得语法大大简化同时不容易出错。RefPtr通过提供一个weakAssign方法来实现弱引用:
template class RefPtr
{
public:
inline void weakAssign(const RefPtr & other)
{
CC_REF_PTR_SAFE_RELEASE(_ptr);
_ptr = other._ptr;
}
};
所以前面的例子就可以转换成下面简洁且不容易出错的写法:
RefPtr image;
image.weakAssign(new cocos2d::Image());
细心的朋友马上会发现,直接使用new Image()作为参数会导致转换函数的调用,而转换函数会增持其引用计数,那么这里的new Image()的引用计数仍然会被增加才对。然而实际这里的执行过程可以转换为如下的语句:
RefPtr image;
RefPtr temp(new Image()); //转换构造函数,引用计数为2
image.weakAssign(temp); //引用计数为2
其中,temp为weakAssign方法作用域内的自动变量,当weakAssign方法执行完毕后,temp临时变量将被销毁,从而执行析构函数,释放其对资源的占用,使其引用计数变为1.
3.2.6.4 其他操作
RefPtr其他一些操作包括析构函数中释放资源,这也是遵循RAII原则在对象的生命周期结束时释放资源。也可以通过调用reset方法来释放对其资源的占用,使其变为一个空的智能指针。
此外,RefPtr重载了*操作符,使其能够直接访问资源的地址,另外也可以通过get方法来访问资源的地址。
对于智能指针,比较常用的方法还包括对资源有效性的判断,我们可以通过将get方法得到的结果和nullptr进行比较来判断智能指针的有效性,另外RefPtr也重载了bool()操作符,使得我们可以直接判断其有效性,例如:
RefPtr<__String> ref1 = __String::create(“Hello”);
CC_ASSERT(true == (bool) ref1);
ref1 = nullptr;
CC_ASSERT(false == (bool) ref1);
RedPtr还包含一些对比较操作符的重载,类型的转换,这里不在详述,读者可以自行查看源代码。
3.2.6.5 RefPtr与容器
如果将一个元素加入到容器中,它还需要结合容器对内存的使用进行内存管理,那么RefPtr能否直接加入Vector和Map容器,答案是肯定的。
前面我们讲述了RefPtr提供了一个转换构造函数,用于将一个T*转换为RefPtr,实际上RefPtr还提供了一个到T*的转换操作符:
inline operator T * () const { return reinterpret_cast<T*>(_ptr); }
而Vector的pushBack方法接收一个T的指针,这样operator T*将会被自动调用加入到Vector,加入到Vector的元素的内存也受Vector进行共享管理,如下的代码:
auto str=new __String(“Hello”);
RefPtr<__String> ref1=str;
Vector<__String*> v;
v.pushBack(ref1);
这样RefPtr可以同时结合Cocos2d-x中的容器一起管理内存,使得对内存的管理更加灵活。当然你也可以直接使用*操作符或者get方法直接获取资源的地址传递给Vector,这里只是简化了操作。
3.2.6.6 RefPtr与自动回收池的比较
到此为止,Cocos2d-x提供了两种方式选管理内存的释放:autorelease和RefPtr,那么我们该怎么选择使用这两种内存管理方式呢?
为了比较他们之间的优势和用途,我们反过来尝试用彼此来代替对方。首先我们用autorelease来代替RefPtr,由于它完全依赖于自动回收池的释放,各个共享的变量几乎完全没法控制对资源的使用。
如果用RefPtr来代替autorelease,那么任何一个对Node资源的引用都是强引用,使得当Node从UI树中移除时我们还需要使用reset释放其对Node资源的占用,这显然是不可控制的。
因此,对于UI元素,我们完全需要使用一种弱引用类型的内存管理,只有UI树本身才可以分配和释放内存,其他任何地方都只能是弱引用。虽然RefPtr提供了弱引用赋值,但是RefPtr不能跟Vector有很好的协作。用RefPtr来管理UI元素会变得极其复杂。
所以,对于这两种内存管理方式,笔者的建议是,所有的UI元素都需要使用autorelease来管理,而游戏中的数据则使用智能指针RefPtr。
3.2.6.7 RefPtr的缺陷
Cocos2d-x中的智能指针也存在着一些缺陷,这些缺陷不是明显的,但是对其机制不熟悉的开发者也有可能遇到这些困惑。
首先是引用计数可以被RefPtr外部控制。例如:
auto str=new __String(“Hello”);
RefPtr<__String> ptr;
ptr.weakAssign(str);
str.release();
(*ptr)->getCString(); //访问野指针,将会报错
由于外部可以修改引用计数,将会使得RefPtr中资源的情况变得很复杂,它可能已经被释放,从而其构造函数对其进行释放的时候导致运行时错误,开发者需要谨慎地使用手动内存管理和智能指针的结合。这种情况在std::shared_pre中则不存在,因为开发者没法在外部修改引用计数。
其次,虽然RefPtr提供了一种弱引用,但是这个弱引用的智能指针仍然表现为一个强类型智能指针的行为,它仍然可以对其资源进行修改,从而导致原智能指针的行为变得不可预期。例如:
RefPtr<__String> ptr1(new __String(“Hello”)); //引用计数2
RefPtr<__String> ptr2;
ptr2.weakAssign(ptr1); //引用计数2
ptr2.reset(); //引用计数1
ptr2.reset(); //被释放
(*ptr1)->getCString(); //导致错误
在C++11中,弱引用的std::weak_ptr被限制只能通过其lock成员来访问原std::shared_ptr变量,从而对资源内存进行操作。这样能保证智能指针的有效性。而在Cocos2d-x中,我们则需要小心的保证智能指针的合法性。这在一定程度上给开发者带来困惑。