COCOS2D-X中的智能指针浅析

2015年03月22日 16:28 0 点赞 0 评论 更新于 2025-11-21 17:41

为了正确释放对象的内存,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(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_cast在编译时进行类型检查。

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 T>
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<typename std::remove_const<T>::type*>(other);
}
return *this;
}
};

这样在对T*进行转换时不会直接调用转换方法,从而可以释放旧资源。此外,也可以使用nullptr让RefPtr成为空的智能指针。

3.2.6.3 弱引用赋值

无论是复制构造函数还是赋值操作符,RefPtr都会对任何非空的左值资源保持强引用关系。但有时对于左值资源,我们可能希望保持弱引用关系。例如:

RefPtr<Image> image;
image = new cocos2d::Image();
image->release();

如果能基于弱引用来构造智能指针,语法会更简洁且不易出错。RefPtr通过提供weakAssign方法来实现弱引用:

template <class T>
class RefPtr
{
public:
inline void weakAssign(const RefPtr & other)
{
CC_REF_PTR_SAFE_RELEASE(_ptr);
_ptr = other._ptr;
}
};

所以前面的例子可以转换为如下简洁且不易出错的写法:

RefPtr<Image> image;
image.weakAssign(new cocos2d::Image());

细心的读者会发现,直接使用new Image()作为参数会调用转换函数,而转换函数会增加其引用计数。但实际执行过程可转换为如下语句:

RefPtr<Image> image;
RefPtr<Image> 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);

RefPtr还包含一些对比较操作符的重载和类型的转换,这里不再详述,读者可自行查看源代码。

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_ptr中不会出现,因为开发者无法在外部修改引用计数。

其次,虽然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中,我们需要小心确保智能指针的合法性,这在一定程度上给开发者带来了困惑。

作者信息

feifeila

feifeila

共发布了 3994 篇文章