Cocos2d-X3.0 刨根问底(四)----- 内存管理源码分析
??
Cocos2d-X3.0 刨根问底(四)----- 内存管理源码分析
本系列文章发表以来得到了很多朋友的关注,小鱼在这里谢谢大家对我的支持,我会继续努力的,最近更新慢了一点,因为我老婆流产了抽了很多时间来照顾她希望大家谅解,并在此预祝我老婆早日康复。 上一篇,我们完整的分析了Director这个类,并提到了Director这个继承了Ref这个类,大致看了一下Ref这个类,是一个关于引用计数的类,从而我们可以推断Cocos2d-x用了一种引用计数的方式来管理内存对象,这一章我们刨根问底Cocos2d-x是如何实现内存管理及我们如何在实际项目开发中应用Cocos2d-x的内存管理。 打开CCRef.h文件,还好,这个文件并不大只有167行,整体浏览一遍,这个文件里一共定义了两个类 Clonable类和Ref类。 class CC_DLL Clonable
class CC_DLL Ref
Ref类是引用计数类,那么从命名上可以初步判断Clonable类应该是对象拷贝相关内容的类,不要跑题,我们是冲着Ref类来的先从这里下手,稍后再回过头来看Clonable是个什么球玩意。 Ref类的完整定义 class CC_DLL Ref { public: /** * 增加一次引用计数 */ void retain(); /** * 释放一次计数,具体里面怎么释放的,下面我们跟定义的代码做分析 */ void release(); /** * 自动释放 */ Ref* autorelease(); /** * 得到当前对象的引用计数的值,也是是被引用了多少次 */ unsigned int getReferenceCount() const; protected: /** * Constructor * * The Ref's reference count is 1 after construction. * @js NA */ Ref(); public: /** * @js NA * @lua NA */ virtual ~Ref(); protected: /// 用来记录引用次数的变量值 unsigned int _referenceCount; friend class AutoreleasePool; #if CC_ENABLE_SCRIPT_BINDING public: /// 对象的ID unsigned int _ID; /// 这个变量这里猜测是lua脚本中引用的ID int _luaID; #endif }; 看了这个类的头文件定义,目前还只能了解了大概意思,Ref这个类用一个变量 _referenceCount来记录对象被引用了多少次,是否应该被释放,并且有一个增加引用 的方法 retain 和两个释放的方法 autorelease 和 release 还出现了一个新的类,为Ref的友元类 AutoreleasePool 从命名上断定这是一个 自动释放对象池,看来这个Ref类虽然代码量不大但并不简单。下面我们逐个分析这个类的几个方法, 在看实现源码时先说明一下,里面会有很多宏定义,这些宏定义都在 #include "CCPlatformMacros.h" #include "ccConfig.h" 这两个文件里,碰到陌生的就查看一下这两个文件里的定义就可以了,由于Cocos2d-x的命名很规范,很多宏定义都可以通过命名来得知它的含义,这也就是前辈们的一句老话,最好的注释就是没有注释。 这里和大家分享一下读源码的经验,看别人的类的时候往往不要按着类定义的函数顺序去阅读,要根据逻辑思维的顺序去阅读。 这个Ref类我们从构造函数开始(大多数类都要从构造函数开始读) 再看一下 Ref的构造函数声明 protected: /** * Constructor * * The Ref's reference count is 1 after construction. * @js NA */ Ref(); 恩,没错,Ref的构造函数声明为 protected 的访问权限,那么这个Ref类是不可以被直接实例化的 只能有子类来实例化这个对象。(可能有些新手读者不理解什么叫实例化,可以简单理解实例化就是被 new现来的对象,不能实例化就是不能用new来创建对象) Ref::Ref() : _referenceCount(1) // when the Ref is created,the reference count of it is 1 { #if CC_ENABLE_SCRIPT_BINDING static unsigned int uObjectCount = 0; _luaID = 0; _ID = ++uObjectCount; #endif } _referencecount 被初始化为 1 不难理解,当对象被创建的时候,肯定要用1次,所以这个引用计数必然在创建的时候为1 CC_ENABLE_SCRIPT_BINDING 支持脚本绑定,cocos2d-x 支持 js脚本和lua脚本。在 ccConfig.h中这个值CC_ENABLE_SCRIPT_BINDING 有定义为 1 这里定义了一个静态变量uObjectCount 这里用来记录创建的对象数量的,当Ref的子类创建对象的时候,基类的Ref的构造函数里 uobjectCount就会自增1.这也使所有对象都有唯一的_ID值不会重复.。从这块定义可以看到一个问题,这个函数并不是线程安全的,可以知道Cocos2d-x不适合多线程程序。 小结一下:构造函数里初始化了对象的 _ID 脚本 _luaID=0、并且初始化了引用计数 _referencecount为1。 看过了构造函数现在看析构函数 Ref::~Ref() { #if CC_ENABLE_SCRIPT_BINDING // if the object is referenced by Lua engine,remove it if (_luaID) { ScriptEngineManager::getInstance()->getScriptEngine()->removeScriptObjectByObject(this); } else { ScriptEngineProtocol* pEngine = ScriptEngineManager::getInstance()->getScriptEngine(); if (pEngine != NULL && pEngine->getScriptType() == kScriptTypeJavascript) { pEngine->removeScriptObjectByObject(this); } } #endif } 这个函数没什么太特别的,又新出来几个类ScriptEngineProtocol,ScriptEngineManager 从命名上理解,这两个类应该是脚本管理器 总结一下析构当这个对象被Lua脚本引用时,在销毁的时候通知脚本管理器把这个对象消除掉 当这个对象被其它脚本管理器引用是 这里尤其指 kScriptTypeJavascript 这个类型的脚本 js脚本。在这个对象销毁时通知 js管理器清除这个对象。 看代码切忌看到什么都跟进,那样就跳到深坑出不来了。这块我们就分析到这种程序,具体ScriptEngineProtocol,ScriptEngineManager 是什么玩意,分析脚本的时候我们再去看,这里只要知道在对象销毁的时候,会去通知脚本管理器消除对象就可以了。 到现在为止Ref的对象创建和销毁我们都了解了,下面看下引用 计数的操作。 先看 retain方法 void Ref::retain() { CCASSERT(_referenceCount > 0,"reference count should greater than 0"); ++_referenceCount; } retain的实现和想象中的没什么差别,就是增加了一次引用计数。 再看 release方法 void Ref::release() { CCASSERT(_referenceCount > 0,"reference count should greater than 0"); --_referenceCount; if (_referenceCount == 0) { #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0) auto poolManager = PoolManager::getInstance(); if (!poolManager->getCurrentPool()->isClearing() && poolManager->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): // // auto obj = Node::create(); // |- 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): // // auto obj = Node::create(); // obj->retain(); // obj->release(); // This `release` is the pair of `retain` of previous line. CCASSERT(false,"The reference shouldn't be 0 because it is still in autorelease pool."); } #endif delete this; } } 代码这么一大段子,不要慌张,其实这个函数一点也不复杂,中间一大段的注释是告诉大家怎么样使用Cocos2d-x引用计数机制的。 分析代码,其实这个函数就干了两件事
中间那段含义是 在debug模式下如果引用计数已经为0并且这个对象还在自动释放池里面,报一个警告。 这里我们先不看那段说明引用机制用法的注释,那段注释最后再看。 release方法看过了,我们再看一下autorelease方法 Ref* Ref::autorelease() { PoolManager::getInstance()->getCurrentPool()->addObject(this); return this; } autorelease方法更简单,这里出现了PoolManager这个类,从字面含义上看,自动释放就是将当前对象加到了对象列表管理器的一个对象列表里面,特别注意到这个函数里面并没有减少引用计数的操作 还有一个成员函数比较简单,就是返回当前对象的引用次数的值 unsigned int Ref::getReferenceCount() const { return _referenceCount; } 这个函数没什么可分析的,就是一个get方法。 分析到这里,引用机制有了进一步了解,小结一下有以下几点:
标红的5,6两点到目前我们只知道个大概,并不知道里面的具体机制是什么,所以下面跟小鱼来分析一下这个PoolManager类,看看这家伙到底干了些什么 PoolManager类在CCAutoreleasePool.h文件中定义的 这个文件里面定义了两个类 class CC_DLL AutoreleasePool
class CC_DLL PoolManager
Cocos2d-x的命名真是太好了,很大的程度上可以帮助我们来阅读代码,这里赞一个。 顾名思义 AutoreleasePool 是用来描述一个自动释放对象池结构的定义 PoolManager 是用来管理所有的AutoreleasePool的管理器。结构很像我们经常用到过的 线程定义与线程管理器定义、socket连接的定义与socket连接管理器的定义,新手读者可以学习这样的数据结构用到自己的程序中。 下面我们先看 AutoreleasePool类的定义 先看一下AutoreleasePool的成员变量 std::vector<Ref*> _managedObjectArray; std::string _name; #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0) /** * The flag for checking whether the pool is doing `clear` operation. */ bool _isClearing; #endif _managedObjectArray; 是用来保存这个自动释放池里面加入的所有Ref对象 std::string _name; 可以了解到每个自动释放池可以有一个名字,这个也方便我们查看和管理 bool _isClearing; 在debug模式下,用来标记当前这个对象列表是否已经做了清理操作。 通过这三个成员变量可以得知,这个类主要是操作_managedObjectArray这个vector来管理这个对象列表的所有对象,下面我们一个一个方法来分析,探究对象列表都有哪些管理操作。 从构造函数和析构函数开始 构造函数 AutoreleasePool::AutoreleasePool() : _name("") #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0),_isClearing(false) #endif { _managedObjectArray.reserve(150); PoolManager::getInstance()->push(this);//创建AutoreleasePool对象时构造函数自动将此对象加入PoolManager, // void PoolManager::push(AutoreleasePool *pool) ///{ // _releasePoolStack.push_back(pool); // _curReleasePool = pool; ///} //} AutoreleasePool::AutoreleasePool(const std::string &name) : _name(name) #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0),_isClearing(false) #endif { _managedObjectArray.reserve(150); PoolManager::getInstance()->push(this); } AutoreleasePool有两个构造函数 第一个没有参数的,做了两件事情,1. 将对象列表的容量扩展到了 150个对象。 2. 将当前的对象列表加入到了对象列表管理器的队列中 第二 个是带一个参数的构造函数,可以给要创建的对象列表起一个名字,其它的操作与第一个默认构造函数一样。 再看析构函数 AutoreleasePool::~AutoreleasePool() { CCLOGINFO("deallocing AutoreleasePool: %p",this); clear(); PoolManager::getInstance()->pop(); } 析构函数也很简单,也是干了两件事 1. 执行了一次clear()方法 2. 将这个对象列表从自动释放对象列表管理器里面删除。 (疑惑1,调用 了 PoolManager::getInstance()->pop();方法,那么内存管理器里肯定有很多个对象列表,它怎么知道pop的一定是当前这个列表呢?这里面是不是有错误呢?这里究竟是怎么回事?后面我们看看能不能解决这个疑问) 接下来,我们来看一下,将Ref对象加入到对象列表的方法 void AutoreleasePool::addObject(Ref* object) { _managedObjectArray.push_back(object); } 这个函数很简单,就是将Ref*对象加到_managedObjectArray的最后面。 再看一下clear方法 void AutoreleasePool::clear() { #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0) _isClearing = true; #endif for (const auto &obj : _managedObjectArray) { obj->release(); } _managedObjectArray.clear(); #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0) _isClearing = false; #endif } 这个函数主要还是干了两件事情
这里面我们注意了,在debug模式下,会有一个_isClearing的状态操作,这个有点类似线程同步的数据锁的编程技巧,因为_managedObjectArray可能里面的对象很多,所以用这个_isClearing来将清除时的操作状态锁定,其它线程访问这个对象列表的时候,会根据这个对象列表的状态来决定是否立即引用这个对象列表里面的对象。 AutoreleasePool还提供了两个工具方法 bool AutoreleasePool::contains(Ref* object) const { for (const auto& obj : _managedObjectArray) { if (obj == object) return true; } return false; } 这个方法是用来检查指定的对象 object 是否在当前对象列表里面,返回 bool类型 void AutoreleasePool::dump() { CCLOG("autorelease pool: %s,number of managed object %dn",_name.c_str(),static_cast<int>(_managedObjectArray.size())); CCLOG("%20s%20s%20s","Object pointer","Object id","reference count"); for (const auto &obj : _managedObjectArray) { CC_UNUSED_PARAM(obj); CCLOG("%20p%20un",obj,obj->getReferenceCount()); } } 一看就知道 dump是个调试时用的方法,打印出当前对象列表的信息,包括名称,装载对象数量及每个对象的引用次数。游戏开发做性能分析及优化的时候肯定少不了用到这个函数。 到此我们看完了AutoreleasePool这个类的源码,我给这个类起的名字是,对象自动释放列表,不知道是否专业和恰当,希望众多读者指正。 小结一下
这里面我们有一个疑惑就是在clear方法里面只是调用了 PoolManager的pop方法,而没有具体告诉PoolManager要消除哪个对象列表,这里面会不会出错。带着疑问我们来看一下PoolManager这个类 老方法,先看看PoolManager的成员变量 static PoolManager* s_singleInstance; std::deque<AutoreleasePool*> _releasePoolStack; AutoreleasePool *_curReleasePool; s_singleInstance 用来实现单例的PoolManager对象 releasePoolStack 这是一个双向队列,来保存所有的 AutoreleasePool AutoreleasePool *_curReleasePool; 从命名上看是当前的autoreleasePool,难道这个对象和我们的疑问有关吗?好像有点眉目了,继续看方法。 构造函数 空的,没啥可看的了, 析构函数 PoolManager::~PoolManager() { CCLOGINFO("deallocing PoolManager: %p",this); while (!_releasePoolStack.empty()) { AutoreleasePool* pool = _releasePoolStack.back(); _releasePoolStack.pop_back(); delete pool;//此时会delete引擎自动产生的2个一模一样的默认的自动释放对象列表 } } 析构函数干了一件事 遍历所有对象列表 从 _releasePoolStack里面出栈,并销毁这个对象列表。上面我们分析autoreleaesPool的时候知道 析构的时候会调用 clear方法,对每个对象进行引用计数及销毁操作。这里面我们注意到releasePoolStack是以栈的行为来使用的。 看到了栈那么我们就来看PoolManager的压栈和出栈方法。 void PoolManager::push(AutoreleasePool *pool) { _releasePoolStack.push_back(pool); _curReleasePool = pool; }
通过这里的代码我们可以判断,PoolManager同时只处理一个自动释放对象池,也就是在栈顶的那一个,那么上面我们的疑问,基本可以解决了,因为Pop的永远是_curReleasePool这个对象,也就是栈顶的一个元素。 出栈: void PoolManager::pop() { // Can not pop the pool that created by engine CC_ASSERT(_releasePoolStack.size() >= 1); _releasePoolStack.pop_back(); // Should update _curReleasePool if a temple pool is released if (_releasePoolStack.size() > 1) { _curReleasePool = _releasePoolStack.back(); } } 干了两件事
疑问2: _curReleasePool = _releasePoolStack.back(); 的条件是 _releasePoolStack.size() >1 而不是 >= 1 这是为什么呢?如果releasePoolStack.size()==1 那么 curReleasePool 没有得到重新赋值,这不出现了野指针了吗?带着疑问继续找答案 这里有一个注释我们可以解读一下 不能弹出引擎自己创建的对象池。那么引擎自己创建的自动释放对象池是哪个呢? 我们看一下单例方法来寻找一下线索 PoolManager* PoolManager::getInstance() { if (s_singleInstance == nullptr) { s_singleInstance = new PoolManager();//在PoolManager的析构函数中释放 // Add the first auto release pool s_singleInstance->_curReleasePool = new AutoreleasePool("cocos2d autorelease pool"); s_singleInstance->_releasePoolStack.push_back(s_singleInstance->_curReleasePool); } return s_singleInstance; } 哈哈,果然在这里 单例方法创建PoolManager对象时还创建了一个 AutoreleasePool 对象 起的名字是 "cocos2d autorelease pool"也就是说只要引擎启动就会有一个默认的自动释放对象列表,这个列表被放到了 PoolManager的栈底。 再仔细阅读一下,可以注意到实际上首次调用这个函数得到单例的时候是放到了栈里面两个AutoreleasePool 第一个是 AutoreleasePool new构造函数的时候放进去的第二个是在s_singleInstance->_releasePoolStack.push_back(s_singleInstance->_curReleasePool);放进去的。因为这里面有两个AutoreleasePool对象,所以疑问2就迎刃而解了。 再看看剩下的几个函数。 void PoolManager::destroyInstance() { delete s_singleInstance; s_singleInstance = nullptr; } 销毁PoolManager 这个函数很简单标准的指针delete操作,没什么可研究的。 AutoreleasePool* PoolManager::getCurrentPool() const { return _curReleasePool; } _curReleasePool的get方法。 bool PoolManager::isObjectInPools(Ref* obj) const { for (const auto& pool : _releasePoolStack) { if (pool->contains(obj)) return true; } return false; } 遍历管理器里面所有的自动释放对象队列,寻找是否有obj对象,这里做的是指针的地址比较,返回bool类型。 回过头我们再看看AutoreleasePool这个类的构造函数和析构函数 在构造函数中有这么一行代码 PoolManager::getInstance()->push(this);
这说明,每当我们创建一个 释放列表对象的时候,这个列表已经加入到了列表管理器里面此时的_curReleasePool指向了这个新创建的列表对象 在析构函数中还有一行代码 PoolManager::getInstance()->pop(); 在这个列表对象销毁的时候,会将当前对象出栈操作。如果栈里面只剩下一个默认列表,那么_currReleasePool并不会重新指向它。 从AutoreleasePool的构造和析构来分析,我们可以推断在使用cocos2d-x的内存管理的时候 不需要显示的操作 列表管理器PoolManager的对象,只要重新创建一个AutoreleasePool对象就可以了。 我们再看一下AutoreleasePool构造函数前面有一段注释。 /** * @warn Don't create an auto release pool in heap,create it in stack. * @js NA * @lua NA */ AutoreleasePool(); 终于理解了这段注释的意思,不要在堆中创建 pool 要在栈中创建, 说白一点,就是要控制 AutoreleasePool的作用域,不要new这个对象。 结合上面没有翻译的那一大段Ref引用计数使用方法的注释这里总结一下 Ref的引用计数 AutoreleasePool 及 PoolManager这三个类的工作方式及使用方法
带着疑问3 我们来思考一下,当定义一个局部的autoreleasePool变量时 { AutoreleasePool pool1; ……………… } 这个pool1的作用域只在这对花括号中间,此时PoolManager里面的currReleasePool就是这个pool1,当程序执行完花括号的内容后 pool1的作用域到期,调用了pool1的析构函数 再回忆一下 AutoreleasePool的析构函数 this);
clear();
PoolManager::getInstance()->pop();
}
调用了clear 在clear里面对所有在管理列表的Ref 对象调用了release减少了一次引用计数,并销毁那些引用计数为0的对象。 调用了PoolManager::pop方法, 调用这个方法后将当前的这个内存对象列表从管理器里面删除掉。 这样在这个局部作用域之间所有的内存管理实际上是交给了pool1来完成的,真是好方法,使用简单方便。 那么默认的自动释放管理是在哪里调用 的呢? 回顾一下上一章节,我们在看Director类的时候在主循环里面有这样一行代码。 void DisplayLinkDirector::mainLoop() { if (_purgeDirectorInNextLoop) { _purgeDirectorInNextLoop = false; purgeDirector(); } else if (! _invalid) { drawScene(); // release the objectsPoolManager::getInstance()->getCurrentPool()-> clear();
}
}
看到标红的那行代码,多让人兴奋,在每次主循环的最后都会主动的去释放一次自动管理的对象。 到此疑问3也解决了,临时的自动释放对象池在对象池的作用域内有效,引擎默认的自动释放对象池是在每一次逻辑帧的最后被调用和释放的。 现在,Ref、AutoreleasePool、PoolManager这三个类的源码我们已经分析完成了,并且针对 PoolManager类还提出了一个小小的优化方案。 回过本章节的开始,我们留了一个类放到最后来阅读一下。那就是Clonable class CC_DLL Clonable { public: /** returns a copy of the Ref */ virtual Clonable* clone() const = 0; /** * @js NA * @lua NA */ virtual ~Clonable() {}; /** returns a copy of the Ref. @deprecated Use clone() instead */ CC_DEPRECATED_ATTRIBUTE Ref* copy() const { // use "clone" instead CC_ASSERT(false); return nullptr; } }; 可以看到这个类是一个抽象类,提供了一个抽象方法 virtual Clonable* clone()const = 0; 显而易见这是让我们在想提供clone方法(对象复制方法)的类统一继续这个父类,不同的继承类要实现自己特有的clone方法 我在工程中随便搜索了一下,看看有没有具体用到clone方法的类,果然找到了一些,这里贴一个cocos2d-x中的一个类的代码从使用中来学习 Clonable这个类和总结一下今天所啰嗦的这点事。 class CC_DLL __Bool : public Ref,public Clonable { public: __Bool(bool v) : _value(v) {} bool getValue() const {return _value;} static __Bool* create(bool v) { __Bool* pRet = new __Bool(v); if (pRet) { pRet->autorelease(); } return pRet; } /* override functions */ virtual void acceptVisitor(DataVisitor &visitor) { visitor.visit(this); } __Bool* clone() const { return __Bool::create(_value); } private: bool _value; }; 这是一个bool数据类型的定义(我虎躯一震,连数据类型都有定义啊,果然这个引擎很强大) 可以看到 这个bool类型继承了 Ref(这是要放到内存管理器里面统一管理啊,并且在这个bool类型创建的时候就已经使用到了autorelease来做内存释放,学到啦哈哈) 这个类还继承了 Clonable这个类,实现 了clone函数 ,实现的很简单就是重新创建了一个与当前值相同的对象,返回了新的对象引用。 哈哈,我越发我举的这个类太恰当了,又有内存管理的应用,又有clone方法的使用。 同理其它cocos2d-x的数据类型大多也都是这种创建模式,不要直接去new对象,提供一个静态方法create来创建对象,并且新创建的对象直接加入到自动的内存管理里面,来实现自动释放,这样避免了你到处去想着delete。安全方便。 本章节就到这里, 下一章节,我们看一下出场率很高的 Node 类。
??
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |