加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

Cocos2d-x V3.x内存管理分析

发布时间:2020-12-14 17:08:02 所属栏目:百科 来源:网络整理
导读:原文:http://galoisplusplus.coding.me/blog/2014/07/30/memory-management-in-cocos2d-x-v3/ cocos2d-x移植自Objective C的cocos2d,其内存管理其实也来自于OC。因而对于写过OC程序的朋友来讲,cocos2d-x的内存管理应该是一目了然的,但对于本渣这枚没接触

原文:http://galoisplusplus.coding.me/blog/2014/07/30/memory-management-in-cocos2d-x-v3/


cocos2d-x移植自Objective C的cocos2d,其内存管理其实也来自于OC。因而对于写过OC程序的朋友来讲,cocos2d-x的内存管理应该是一目了然的,但对于本渣这枚没接触过OC的C++码农来说,或许直接看cocos2d-x源代码才是最直接快捷的方式。

Node类

我们首先来看Node类的代码,Node是cocos2d-x中极重要的基类,许多常用的SceneLayerMenuItem等都继承自Node。

Node的创建是通过以下的接口,该函数返回一个Node的静态对象指针:

1
2
3
4
5
6
7
8
9
/**  * Allocates and initializes a node.  * @return A initialized node which is marked as "autorelease".  */  * 分配空间并初始化Node  * 返回一个被初始化过且是autorelease的Node对象  */ static Node * create(); 

下面让我们来看这个函数的实现。该函数采用二段式创建的方式——首先用new operator在heap中开辟空间并进行简单的初始化,假如new返回一个合法地址(cocos2d-x没有采用c++的异常处理机制),则接着init函数用于实际初始化Node的成员。只有在这二者都成功后,才把创建的指针设为autorelease(关于autorelease后面会继续解释)并返回。

9 10 11 12 13
Node::create() {  ret = new Node();  if (ret && ret->init())  {  autorelease();  }  else  {  CC_SAFE_DELETE(ret);  }  return ret; } 

对于创建失败的情况,cocos2d-x使用了下面的宏保证该指针被delete且被设为nullptr:

1
#define CC_SAFE_DELETE(p) do { delete (p); (p) = nullptr; } while(0) 

这个二段式的create函数在cocos2d-x中非常常用,因而cocos2d-x用了以下一个叫CREATE_FUNC来表示这个函数以便给继承Node的子类使用:

13 14 15 16 17 18 19 20
 * define a create function for a specific type,such as Layer  * @param __TYPE__ class type to add create(),117)!important"> */ #define CREATE_FUNC(__TYPE__)  static __TYPE__* create()  {   __TYPE__ *pRet = new __TYPE__();   if (pRet && pRet->init())   {   pRet->autorelease();   return pRet;   }   else   delete pRet;   pRet = NULL;   return NULL;  } 

这样,继承Node的子类(例如ExampleLayer)只需要在类声明(class declaration)中加入CREATE_FUNC(类名)(例如CREATE_FUNC(ExampleLayer)),再override下init函数即可。

Ref类

在cocos2d-x中,Node类的父类是Ref类,之前我们所看到的autorelease方法实际上就来自于这个父类。

下面我们先来看Ref类的声明,这里为了突出重点,我们忽略script binding的情况:

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
class CC_DLL Ref { public:   * Retains the ownership.  *  * This increases the Ref's reference count.  * @see release,autorelease  * @js NA  */   * 拿到所有权  * 这会增加引用计数  */  void retain();    * Releases the ownership immediately.  * This decrements the Ref's reference count.  * If the reference count reaches 0 after the descrement,this Ref is  * destructed.  * @see retain,117)!important"> * 立即释放所有权  * 这会减少引用计数  * 如果更新后的引用计数为0,该Ref对象会被销毁 release();    * Releases the ownership sometime soon automatically.  * This descrements the Ref's reference count at the end of current  * autorelease pool block.  * @returns The Ref itself.  * @see AutoreleasePool,retain,release  * @lua NA  * 自动释放所有权  */  Ref* autorelease();    * Returns the Ref's current reference count.  * @returns The Ref's reference count.  * 返回该Ref对象的引用计数 unsigned int getReferenceCount() const;  protected:   * Constructor  * The Ref's reference count is 1 after construction.  * 构造函数  * 初始引用计数为1 Ref();   */  virtual ~ * 采用引用计数(reference counting)  * _referenceCount就是计数值  */  // count of references  _referenceCount;   friend AutoreleasePool;   // Memory leak diagnostic data (only included when CC_USE_MEM_LEAK_DETECTION is defined and its value isn't zero)  // 以下函数用于开启内存泄露检测时打印出泄露信息 #if CC_USE_MEM_LEAK_DETECTION public:  static printLeaks(); #endif }; 

从上面的代码,我们可以初步了解到:Ref采用引用计数(reference counting)的方法来管理某个指针所指向的某个对象,初始创建时计数是1,当计数变为0时该对象被析构;retain方法会增加计数并拿到所有权,而与之对应的,release方法会减少计数;autorelease是把所有权交给友类(friend class)AutoreleasePool,让它来决定何时减少计数,这个类我们后面会继续谈到。

下面我们来看Ref类的实现(definition):

110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
trackRef(ref); untrackRef(ref); #endif  // 在初始化列表中将计数设为1 Ref::Ref() : _referenceCount(1) // when the Ref is created,the reference count of it is 1 { // 假如开启内存泄露检测,则追踪该对象指针,将该对象指针放入一个列表(list)中 // 后面的代码我们很快就会看到这个list #if CC_USE_MEM_LEAK_DETECTION  trackRef(this); #endif }  Ref::~Ref() { // 假如开启内存泄露检测且引用计数非0,则在追踪列表中找到该对象指针并删除 #if CC_USE_MEM_LEAK_DETECTION  _referenceCount != 0)  untrackRef(#endif }  // retain只是单纯将计数递增 retain() {  // CCASSERT是cocos2d-x对C++的assert所封装的宏  CCASSERT(_referenceCount > 0, "reference count should greater than 0");  ++_referenceCount; }  release() {  // 首先计数递减  "reference count should greater than 0");  --_referenceCount;   // 计数为0,应当析构对象  _referenceCount == 0)  { #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)  // 得到一个PoolManager单例的对象  // PoolManager类后面会解释  auto poolManager = PoolManager::getInstance();  // 后面会详细解释这段代码  if (!poolManager->getCurrentPool()->isClearing() && isObjectInPools(this))  {  // 以下的注释很重要,很快会解释到  // Trigger an assert if the reference count is 0 but the Ref is still in autorelease pool.  // This happens when 'autorelease/release' were not used in pairs with 'new/retain'.  //  // Wrong usage (1):  // auto obj = Node::create(); // Ref = 1,but it's an autorelease Ref which means it was in the autorelease pool.  // obj->autorelease(); // Wrong: If you wish to invoke autorelease several times,you should retain `obj` first.  // Wrong usage (2):  // auto obj = Node::create();  // obj->release(); // Wrong: obj is an autorelease Ref,it will be released when clearing current pool.  // Correct usage (1):  // |- new Node(); // `new` is the pair of the `autorelease` of next line  // |- autorelease(); // The pair of `new Node`.  // obj->retain();  // obj->autorelease(); // This `autorelease` is the pair of `retain` of previous line.  // Correct usage (2):  // obj->release(); // This `release` is the pair of `retain` of previous line.  CCASSERT(false,152)!important">"The reference shouldn't be 0 because it is still in autorelease pool.");  } // 假如开启内存泄露检测,则在追踪列表中找到该对象指针并删除 #endif  // 调用析构函数并释放空间  delete this;  } }  // 把该对象指针交给友类AutoreleasePool(具体来说,是PoolManager单例对象所得到的当前的AutoreleasePool)来管理 autorelease() {  getInstance()->addObject(this);  return this; }  const {  _referenceCount; }  #if CC_USE_MEM_LEAK_DETECTION  // 这里便是存放所追踪的对象指针的列表 std::list<Ref*> __refAllocationList;  printLeaks() {  // Dump Ref object memory leaks  __refAllocationList.empty())  {  log("[memory] All Ref objects successfully cleaned up (no leaks detected).n");  }  "[memory] WARNING: %d Ref objects still active in memory.", (int)size());   // C++的range-for语法  // 打印出每个泄露内存的对象指针的类型和引用计数  for (const auto& ref : __refAllocationList)  {  CC_ASSERT(ref);  const char* type = typeid(*ref).name();  "[memory] LEAK: Ref object '%s' still active with reference count %d.(type ? type : ""), ref->getReferenceCount());  }  } }  // 将对象指针放入列表中 ref) {  ref,152)!important">"Invalid parameter,ref should not be null!");   // Create memory allocation record.  push_back(ref); }  // 在列表中找到该对象指针并删除 ref) {  iter = find(begin(),210)!important">end(),210)!important">iter == end())  {  "[memory] CORRUPTION: Attempting to free (%s) with invalid ref tracking record.name());  return;  }   erase(iter); }  #endif // #if CC_USE_MEM_LEAK_DETECTION 

这段源代码对使用者最重要的在于release函数中的注释:

  • 当Ref的计数变为0时,它一定不能在AutoreleasePool中。

  • Ref的计数为0且同时在AutoreleasePool中的错误是由new/retain和autorelease/release没有对应引起的(有木有想起C++中new和delete没对应所引起的内存泄露?):

    • autorelease缺乏对应的retain。 例如:
2
obj = create(); // 注意create函数会调用autorelease方法,因此obj已经没有该指针的所有权了 obj->autorelease(); // obj没有所有权,因此无法再把所有权转交给AutoreleasePool,若要调用autorelease方法需要先调用retain拿到所有权 
- release缺乏对应的retain。

例如:

2
release(); // obj没有所有权,因此无法再控制计数(所有权在AutoreleasePool),若要调用release方法需要先调用retain拿到所有权 
  • 正确的用法是在create后调用autorelease或release方法前先用retain拿到所有权: 例如:
9
// 前面我们分析过create函数,它会先用new operator得到对象,再调用autorelease方法 // 这里new和autorelease对应 create();  |- Node();  |- autorelease();  // 这里retain和autorelease对应,autorelease一个已经被autorelease过的对象(例如通过create函数构造的对象)必须先retain retain(); autorelease(); 

又如:

4
create(); // 这里retain和release对应,release一个已经被autorelease过的对象(例如通过create函数构造的对象)必须先retain release(); 

AutoreleasePool类

现在我们来看Ref类的友类AutoreleasePool。 首先来看类声明:

110
AutoreleasePool {  * @warn Don't create an auto release pool in heap,create it in stack.  * 警告:不要在heap上构造AutoreleasePool对象,要在stack上构造 AutoreleasePool();    * Create an autorelease pool with specific name. This name is useful for debugging. AutoreleasePool(const string &name);    */  ~ * Add a given object to this pool.  * The same object may be added several times to the same pool; When the  * pool is destructed,the object's Ref::release() method will be called  * for each time it was added.  * @param object The object to add to the pool.  * 把指定的对象指针放到AutoreleasePool对象中  * 注意:  * 同一对象的指针可能会被多次加入到同一AutoreleasePool对象中;  * 当该AutoreleasePool对象被析构时,该对象指针被加入多少次,就得调用多少次该对象的release()函数  * 这是因为AutoreleasePool用vector而非set来存放所管理的对象指针,因此不会去重 addObject(Ref *object);    * Clear the autorelease pool.  * Ref::release() will be called for each time the managed object is  * added to the pool.  * 清空AutoreleasePool  * 每个被管理的对象指针被加入多少次,就会调用多少次release()函数 clear();  #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)   * Whether the pool is doing `clear` operation. bool isClearing() const { _isClearing; }; #endif    * Checks whether the pool contains the specified object.  * 检查AutoreleasePool对象是否管理某个对象指针 contains(object) const;    * Dump the objects that are put into autorelease pool. It is used for debugging.  * The result will look like:  * Object pointer address object id reference count dump();  private:   * The underlying array of object managed by the pool.  * Although Array retains the object once when an object is added,proper  * Ref::release() is called outside the array to make sure that the pool  * does not affect the managed object's reference count. So an object can  * be destructed properly by calling Ref::release() even if the object  * is in the pool.  * AutoreleasePool对象将它所管理的对象指针放到下面的vector中  * 尽管每次有对象指针加到该vector中时,该vector实际上retain拿到了所有权,  * 但是Ref::release()会被调用来保证AutoreleasePool不会改变它所管理的对象指针  * 的引用计数。  * 所以,当某个对象指针被放到AutoreleasePool类中管理时,仍然可以通过调用  * Ref::release()函数来析构它 vector<_managedObjectArray;  string _name;   * The flag for checking whether the pool is doing `clear` operation. _isClearing;  从类声明中能解读出的最重要的信息是AutoreleasePool类用STL vector来存放它所管理的Ref所指向的对象。要搞清楚原理还需要继续看它的实现:

77
AutoreleasePool::AutoreleasePool() : _name("") #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0) ,210)!important">_isClearing(false) #endif {  _managedObjectArray.reserve(150);  // 每个新创建的AutoreleasePool对象都交由PoolManager单例对象统一管理  push(this); }  name) : _name(name) AutoreleasePool::~AutoreleasePool() {  CCLOGINFO("deallocing AutoreleasePool: %p",22)!important">this);  // 清空该AutoreleasePool  clear();   // 要析构的AutoreleasePool对象不再由PoolManager管理  pop(); }  // 只是单纯调用vector::push_back加入所管理的对象 object) {  object); }  // clear函数就是AutoreleasePool调用release来管理对象的引用计数的地方 clear() { #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)  _isClearing = true; // 调用每个在AutoreleasePool的对象指针的release方法  auto &obj : _managedObjectArray)  {  release();  }  // 清空存放管理对象的vector  clear(); false; // 线性搜索所管理的对象指针的vector,查看所指定的Ref指针是否存在 _managedObjectArray)  {  obj == object)  return true;  }  false; }  dump() {  CCLOG("autorelease pool: %s,number of managed object %d_name.c_str(),22)!important">static_cast<int>(size()));  "%20s%20s%20s",152)!important">"Object pointer",152)!important">"Object id",152)!important">"reference count");  CC_UNUSED_PARAM(obj);  "%20p%20uobj,210)!important">getReferenceCount());  } } 

PoolManager类

下面我们来看PoolManager类,在cocos2d-x中,这个类是典型的单例(singleton)工厂类——及有且只有一个PoolManager对象,该PoolManger有一个存放AutoreleasePool对象指针的stack,该stack是由STL::vector实现的。需要注意的是,cocos2d-x的单例类都不是线程安全的,跟内存管理紧密相关的PoolManager类也不例外,因此在多线程中使用cocos2d-x的接口需要特别注意内存管理的问题。

我们先来看类声明:

44
PoolManager { CC_DEPRECATED_ATTRIBUTE PoolManager* sharedPoolManager() { getInstance(); }  getInstance();   purgePoolManager() { destroyInstance(); }  destroyInstance();    * Get current auto release pool,there is at least one auto release pool that created by engine.  * You can create your own auto release pool at demand,which will be put into auto releae pool stack. AutoreleasePool *getCurrentPool() const;   isObjectInPools(obj) AutoreleasePool;  private:  // singleton类把构造函数和析构函数设为private,避免被调用  PoolManager();  ~PoolManager();   push(pool);  pop();   s_singleInstance;   // 同样用vector来存放所管理AutoreleasePool对象指针的列表  AutoreleasePool*> _releasePoolStack; }; 

再来看类实现:

66
s_singleInstance = nullptr;  getInstance() {  s_singleInstance == nullptr)  {  s_singleInstance = PoolManager();  // Add the first auto release pool  AutoreleasePool("cocos2d autorelease pool");  }  s_singleInstance; }  destroyInstance() {  delete s_singleInstance;  nullptr; }  PoolManager() {  _releasePoolStack.10); }  PoolManager::~"deallocing PoolManager: %p",22)!important">this);   // 逐个析构所管理的AutoreleasePool对象  while (!AutoreleasePool* pool = back();   pool;  } }  // 加入AutoreleasePool对象指针时用的是stl::vector的push_back函数, // 于是调用back函数就可以得到最新被加入的AutoreleasePool对象指针 back(); }  // 线性搜索每个被管理的AutoreleasePool, // 每个AutoreleasePool对象再用contains函数线性搜索一遍 pool : _releasePoolStack)  {  pool->obj))  pool) {  pool); }  pop() {  CC_ASSERT(!empty());  pop_back(); } 

最后的疑问

想必各位用惯了c++的看官在看完了以上的代码之后,最有疑问的还是神秘的Ref::autorelease函数。我们从AutoreleasePool的源代码看到,事实上被autorelease的对象最后还是通过release函数来减少其引用计数的,只不过release函数不是由使用者来调用,而是AutoreleasePool来调用,调用的地方在AutoreleasePool::clear()函数。那么AutoreleasePool如何个「auto」自动管理内存法儿?AutoreleasePool::clear()会在哪个地方被调用?

谜底隐藏在cocos/base/CCDirector.cpp中:

15
DisplayLinkDirector::mainLoop() {  _purgeDirectorInNextLoop)  {  _purgeDirectorInNextLoop = false;  purgeDirector();  }  else if (! _invalid)  {  drawScene();   // release the objects  clear();  } } 

这里就不纠缠Director类的实现细节了,上面的代码揭示的事实是:在图像渲染的主循环中,如果当前的图形对象是在当前帧,则调用显示函数,并调用AutoreleasePool::clear()减少这些对象的引用计数。mainLoop是每一帧都会自动调用的,所以下一帧时这些对象都被当前的AutoreleasePool对象release了一次。这也是AutoreleasePool「自动」的来由。

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
    站长推荐
    热点阅读