cocos2d-x中的autorelease pool

引用计数的由来

cocos2d-x 的世界是基于 CCObject 类构建的,其中的每个元素:层、场景、精灵等都是一个个 CCObject 的对象。所以 内存管理的本质就是管理一个个 CCObject。作为一个 cocos2d 的 C++ 移植版本,在它之前有很多其它语言的 实现,从架构层次来说,这与语言的实现无关(比如 CCNode 的节点树形关系,其它语言也可以实现,如果是内存方便,C# 等更是无需考虑),但就从内存管理方面来说,参考了 OC (Objective-C) 的内存管理实现。

一个简单的自动管理原则CCObject 内部维护着一个引用计数,引用计数为 0 就自动释放 ~(如果么有直接做如 delete 之类的操作)。那么此时可以预见,管理内存的实质就是管理这些 “引用计数” 了!使用 retain 和 release 方法对引用计数进行操作!


为什么要有自动释放池 及其作用

我们知道 cocos2d-x 使用了自动释放池,自动管理对象,知其然!其所以然呢?为什么需要自动释放池,它在整个框架之中又起着什么样的作用!在了解这一点之前,我们需要 知道 CCObject 从创建之初,到最终销毁,经历了哪些过程。在此,一叶总结以下几点:

  • 刚创建的对象,而 为了保证在使用之前不会释放(至少让它存活一帧),所以自引用(也就是初始为1)
  • 为了确定是否 实际使用,所以需要在一个合适的时机,解除自身引用。
  • 而这个何时的时机正是在帧过度之时。
  • 帧过度之后的对象,用则用矣,不用则弃!
  • 由于已经解除了自身引用,所以它的引用被使用者管理(一般而言,内部组成树形结构的链式反应,如 CCNode)。
  • 链式反应,也就是,如果释放一个对象,也会释放它所引用的对象。

上面是一个对象的大致流程,我们将对象分为两个时期,一个是刚创建时期,自引用为 1(如果为 0 就会释放对象,这是基本原则,所以要大于 0) 的时期,另一个是使用时期。上面说到,为了保证创建时期的对象不被销毁,所以自引用(并没有实际的使用)初始化为 1,这就意味着我们需要一个合适的时机,来解除这样的自引用。

何时?在帧过度之时!(这样可保证当前帧能正确使用对象而没有被销毁。)怎么样释放?由于是自引用,我们并不能通过其它方式访问到它,所以就有了自动释放池,我们 变相的将“自引用”转化“自动释放池引用”,来标记一个 “创建时期的对象”。然后在帧过度之时,通过自动释放池管理,统一释放 “释放池引用”,也就意味着,去除了“自身引用”。帧过度之后的对象,才是真正的被使用者所管理 下面我们用代码来解释上述过程。

通常我们使用 create(); 方法来创建一个自动管理的对象,而其内部实际操作如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
// 初始化一个对象
static CCObject* create()
{
     // new CCObject 对象
     CCObject *pRet = new CCObject();
     if (pRet && pRet->init())
     {
         // 添加到自动释放池
         pRet->autorelease();
         return pRet;
     }
     else
     {
         delete pRet;
         pRet = 0;
         return 0;
     }
}
// 我们看到初始化的对象 自引用 m_uReference = 1
CCObject::CCObject( void )
:m_uAutoReleaseCount(0)
,m_uReference(1) // when the object is created, the reference count of it is 1
,m_nLuaID(0)
{
     static unsigned int uObjectCount = 0;
 
     m_uID = ++uObjectCount;
}
 
// 标记为自动释放对象
CCObject* CCObject::autorelease( void )
{
     // 添加到自动释放池
     CCPoolManager::sharedPoolManager()->addObject( this );
     return this ;
}
 
// 继续跟踪
void CCPoolManager::addObject(CCObject* pObject)
{
     getCurReleasePool()->addObject(pObject);
}
 
// 添加到自动释放池的实际操作
void CCAutoreleasePool::addObject(CCObject* pObject)
{
     // 内部是由一个 CCArray 维护自动释放对象,并且此操作 会使引用 + 1
     m_pManagedObjectArray->addObject(pObject);
 
     // 由于初始化 引用为 1,上面又有操作,所以引用至少为 2 (可能还被其它所引用)
     CCAssert(pObject->m_uReference > 1, "reference count should be greater than 1" );
     ++(pObject->m_uAutoReleaseCount);
     // 变相的将自身引用转化为释放池引用,所以减 1
     pObject->release(); // no ref count, in this case autorelease pool added.
}

上面便是通过 create() 方法创建对象的过程。文中说到,一个合适的时机,解除自身引用(也就是释放池引用),那这又是在何时进行的呢?程序的运行有一个主循环,控制着每一帧的操作,在每一帧画面画完之时会自动调用CCPoolManager::sharedPoolManager()->pop(); 方法 ( 具体可参见文章Cocos2d-x 程序是如何开始运行与结束的 ,这里我们只要知道每一帧结束都会调用 pop() 方法),来自动清理 创建时期 的引用。现在我们就来看看 pop() 的方法实现:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
void CCPoolManager::pop()
{
     if (! m_pCurReleasePool)
     {
         return ;
     }
 
     // 当前释放池个数,pop 使用栈结构
      int nCount = m_pReleasePoolStack->count();
     // 释放池当中存放的都是 创建时期 对象,此时解除释放池引用
     m_pCurReleasePool->clear();
 
     // 当前释放池,出栈,在这里可以看到判断 nCount 是否大于 1,文后将会对此做具体说明
       if (nCount > 1)
       {
         m_pReleasePoolStack->removeObjectAtIndex(nCount-1);
 
//         if(nCount > 1)
//         {
//             m_pCurReleasePool = m_pReleasePoolStack->objectAtIndex(nCount - 2);
//             return;
//         }
         m_pCurReleasePool = (CCAutoreleasePool*)m_pReleasePoolStack->objectAtIndex(nCount - 2);
     }
 
     /*m_pCurReleasePool = NULL;*/
}
 
// 释放池引用清理工作
void CCAutoreleasePool::clear()
{
     // 如果释放池存在 创建时期 的对象
     if (m_pManagedObjectArray->count() > 0)
     {
         //CCAutoreleasePool* pReleasePool;
#ifdef _DEBUG
         int nIndex = m_pManagedObjectArray->count() - 1;
#endif
 
         CCObject* pObj = NULL;
         CCARRAY_FOREACH_REVERSE(m_pManagedObjectArray, pObj)
         {
             if (!pObj)
                 break ;
 
             --(pObj->m_uAutoReleaseCount);
             //(*it)->release();
             //delete (*it);
#ifdef _DEBUG
             nIndex--;
#endif
         }
         // 移除释放池对创建时期对象的引用,从而使对象交由使用者全权管理
         m_pManagedObjectArray->removeAllObjects();
     }
}

到这里,自动释放池的作用也就完成了! 可以说创建的对象在一帧 (但有特殊情况,下一段说明) 之后就完全脱离了 自动释放池的控制,自动释放池,对对象的管理也就在 创建时期起着作用!之后变交由使用者管理,释放。


对”释放池”的管理说明

我们知道了释放池管理着 创建时期 的对象,那么对于释放池本身是如何管理的?我们知道对于释放池,只需要有一个就已经能够满足我们的需求了,而在 cocos2d-x 的设计中,使用了集合管理 一堆 释放池。而在实际,它们又发挥了多大的用处?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
// 释放池管理接口
class CC_DLL CCPoolManager
{
     // 释放池对象集合
     CCArray*    m_pReleasePoolStack;
     // 当前操作释放池
     CCAutoreleasePool*                    m_pCurReleasePool;
 
     // 获取当前释放池
     CCAutoreleasePool* getCurReleasePool();
public :
     CCPoolManager();
     ~CCPoolManager();
     void finalize();
     void push();
     void pop();
 
     void removeObject(CCObject* pObject);
     // 添加一个 创建时期 对象
     void addObject(CCObject* pObject);
 
     static CCPoolManager* sharedPoolManager();
     static void purgePoolManager();
 
     friend class CCAutoreleasePool;
};
 
// 我们从 addObject 开始看起,由上文可以 addObject 是由 CCObject 的 autorelease 自动调用的
void CCPoolManager::addObject(CCObject* pObject)
{
     getCurReleasePool()->addObject(pObject);
}
 
CCAutoreleasePool* CCPoolManager::getCurReleasePool()
{
     // 如果当前释放池为空
     if (!m_pCurReleasePool)
     {
         // 添加一个
         push();
     }
 
     CCAssert(m_pCurReleasePool, "current auto release pool should not be null" );
 
     return m_pCurReleasePool;
}
 
void CCPoolManager::push()
{
     CCAutoreleasePool* pPool = new CCAutoreleasePool();       //ref = 1
     m_pCurReleasePool = pPool;
     // 像集合添加一个新的释放池
     m_pReleasePoolStack->addObject(pPool);                   //ref = 2
 
     pPool->release();                                       //ref = 1
}

从 addObject 开始分析,我们知道在 addObject 之前,会首先判断是否有当前的释放池,如果没有则创建,如果有,则直接使用,可想而知,在任何使用,任何情况,通过 addObject 只需要创建一个释放池便已经足够使用了。事实上也是如此。再来看 pop 方法。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void CCPoolManager::pop()
{
     if (! m_pCurReleasePool)
     {
         return ;
     }
 
      int nCount = m_pReleasePoolStack->count();
     // 清楚对 创建对象 的引用
     m_pCurReleasePool->clear();
 
     // 如果大于 1,这也保证着,在任何时候,总有一个释放池是可以使用的
       if (nCount > 1)
       {
           // 移除当前的释放池
         m_pReleasePoolStack->removeObjectAtIndex(nCount-1);
 
//         if(nCount > 1)
//         {
//             m_pCurReleasePool = m_pReleasePoolStack->objectAtIndex(nCount - 2);
//             return;
//         }
         // 将当前释放池设定为前一个释放池,也就是 “出栈”的操作
         m_pCurReleasePool = (CCAutoreleasePool*)m_pReleasePoolStack->objectAtIndex(nCount - 2);
     }
 
     /*m_pCurReleasePool = NULL;*/
}

看到这里 我就不解了!什么情况下才能用到多个释放池?按照设计的逻辑根本用不到。带着这个疑问,我在CCPoolManager::push() 方法之内添加了一句话打印(修改源代码) CCLog("这里要长长长的 **********"); ,然后重新编译源文件,运行程序,发现实际的使用中,push 只被调用了两次!我们知道,通过 addObject 可能会自动调用 push() 一次,但也仅有一次,所以一定是哪里手动调用了 push() 方法,才会出现这种情况,所以我继续翻看源代码,定位到了 bool CCDirector::init(void) 方法,在这里进行了游戏的全局初始化相关工作:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
bool CCDirector::init( void )
{
     CCLOG( "cocos2d: %s" , cocos2dVersion());
 
     ...
     ...
     m_dOldAnimationInterval = m_dAnimationInterval = 1.0 / kDefaultFPS;   
     m_pobScenesStack = new CCArray();
     m_pobScenesStack->init();
 
     ...
     ...
     m_fContentScaleFactor = 1.0f;
 
     ...
     ...
     // touchDispatcher
     m_pTouchDispatcher = new CCTouchDispatcher();
     m_pTouchDispatcher->init();
 
     // KeypadDispatcher
     m_pKeypadDispatcher = new CCKeypadDispatcher();
 
     // Accelerometer
     m_pAccelerometer = new CCAccelerometer();
 
     // 这里手动调用了 push 方法,而在这之前的初始化过程中,间接的使用了 CCObject 的 autorelease,已经触发过一次 push 方法
     CCPoolManager::sharedPoolManager()->push();
 
     return true ;
}

所以我们便能够看到 push 方法被调用了两次,但其实如果我们把这里的手动调用放在方法的开始处,或者干脆就不使用CCPoolManager::sharedPoolManager()->push(); ,对程序也没任何影响,这样从头到尾,只创建了一个自动释放池,而这里多创建的一个并没有多大的用处。 或者用处不甚明显,因为多创建一个释放池是有其效果的,效果具体体现在哪里,那就是 可以使调用 push() 方法之前的对象,多存活一帧。,因为 pop 方法只对当前释放池做了 clear 释放。为了方便起见,我们使用 Cocos2d-x 内存管理浅说 里面的方法观察每一帧的情况,看下面测试代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 关键代码如下
CCLog( "update index: %d" , updateCount);
 
// 在不同的帧做相关操作,以便观察
if (updateCount == 1) {
     // 创建一个自动管理对象
     layer = LSLayer::create();
     // 创建一个新的自动释放池
     CCPoolManager::sharedPoolManager()->push();
     // 再创建一个自动管理对象
     sprite = LSSprite::create();
} else if (updateCount == 2) {
 
} else if (updateCount == 3) {
 
}
 
CCLog( "update index: %d end" , updateCount);
 
/// 打印代码如下
cocos2d-x debug info [update index: 1]
// 第一帧创建了两个自动管理对象
cocos2d-x debug info [LSLayer().()]
cocos2d-x debug info [LSSprite().()]
cocos2d-x debug info [update index: 1 end]
// 第一个过度帧只释放了 sprite 对象
cocos2d-x debug info [LSSprite().~()]
cocos2d-x debug info [update index: 2]
cocos2d-x debug info [update index: 2 end]
// 第二个过度帧释放了 layer 对象
cocos2d-x debug info [LSLayer().~()]
cocos2d-x debug info [update index: 3]
cocos2d-x debug info [update index: 3 end]

可以对比 sprite 和 layer 对象,两个对象被放在了不同的自动释放池之中。这就是 手动调用 push() 方法所能达到的效果,至于怎么利用这个特性,帮助我们完成特殊的功能?我想还是不用了,这会增加我们程序设计的 复杂度,在我看来,甚至想把,cocos2d-x 2.0.4 中那唯一一次调用的 push() 给删了,以保持简单(程序的第一次初始化“可能”会用到这个特性,不过目测是没有多大关系的了 : P),在这里只系统通过这个例子理解 自动释放池是怎样被管理的即可!

从自动释放池管理 创建时期 对象,再到对释放池的管理,我们已经大概了解了一个对象的生命周期经历了哪些! 下面简单说说 使用时期 的对象管理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值