这里介绍cocos2d-x的一种消息/数据传递方式,内置的观察者模式,也称消息通知中心,CCNotificationCenter。
虽然引擎没有为我们封装线程类,但还是提供了一些组件,辅助我们进行并发编程。除了上面提到的异步加载图片,引擎还提供了消息中心 CCNotificationCenter。这是一个类似 Qt 中消息槽的机制,一个对象可以注册到消息中心,指定要接收的消息;而某个事件完成时,则可以发送对应的消息,之前注册过的对象会得到通知。主要有以下两个关键的接口函数:
void addObserver(CCObject *target, //接收消息的对象 SEL_CallFuncO selector, //响应消息的函数 const char *name, //待接收的消息 CCObject *obj); //指定消息的发送者,目前暂时为无用参数 void postNotification(const char *name); //发送一个消息
借助消息中心,异步事件之间的对象可以进一步减少耦合,使用事件驱动的方式编写代码。以游戏中的金币数变动为例,我们将菜单层添加为金币数量变化的消息的观察者,相关代码如下:
CCNotificationCenter::sharedNotificationCenter()->addObserver(this,callfuncO_selector(GameMenuLayer::coinChange), "CoinChange", NULL);
然后在开炮、捕获鱼等引起金币变化的地方发出该消息,从而触发菜单层的 coinChange 函数:
CCNotificationCenter::sharedNotificationCenter()->postNotification("CoinChange", NULL);
当然,在多线程的环境中,考虑到之前提到的原则,不可能直接在分离的线程中调用消息中心发送消息,我们可以建立一个线程间共享的消息池,让消息可以在不同线程间流动,或者说,我们需要建立一个线程安全的消息队列。下面我们创建一个线程安全的消息队列,代码如下:
class MTNotificationQueue : CCNode { typedef struct { string name; CCObject* object; } NotificationArgs; vector<NotificationArgs> notifications; MTNotificationQueue(void); public: static MTNotificationQueue* sharedNotificationQueue(); void postNotifications(ccTime dt); ~MTNotificationQueue(void); void postNotification(const char* name, CCObject* object); };
从接口上看,这个消息队列可以看做引擎自带的消息中心的补充,因为这里并不提供消息接收者的注册,仅仅是允许线程安全地向消息中心发出一个消息。这样也对应了一种处理模式:主线程负责绘图实现,在分离出来的子线程中完成重计算任务,计算完成后向主线程发回处理完毕的消息,消息是单向流动的,数据从磁盘、网络或其他任何地方经过处理后最终以视图的形式流向了屏幕。
在实现上,我们通过一个数组缓冲了各线程间提交的消息,稍后在主线程中将这些消息一次性地向 CCNotificationCenter发出。其中需要保证的是,缓冲用的数组在不同线程间的访问必须是安全的,因此需要一个互斥锁。
不同线程间可共享的数据必须是静态的或全局的,因此互斥锁也必须是全局的。考虑到这个消息队列应该是全局唯一的单例,仅仅需要一个全局唯一的互斥锁与之对应:
pthread_mutex_t sharedNotificationQueueLock;
而考虑到这个互斥锁必须进行合适的初始化和清理,可以用一个类的全局变量管理其生命周期:
class LifeManager_PThreadMutex { pthread_mutex_t* mutex; public: LifeManager_PThreadMutex(pthread_mutex_t* mut) : mutex(mut) { pthread_mutex_init(mutex, NULL); } ~LifeManager_PThreadMutex() { pthread_mutex_destroy(mutex); } }__LifeManager_sharedNotificationQueueLock(&sharedNotificationQueueLock);
在 pthread 库中,我们使用下面一对函数进行互斥锁的上锁和解锁:
int pthread_mutex_lock (pthread_mutex_t * mutex); //上锁 int pthread_mutex_unlock (pthread_mutex_t * mutex); //解锁
这里的上锁函数是阻塞性的,如果目标互斥锁已经被锁上,会一直阻塞线程直到解锁,然后再次尝试解锁直到成功从当前线程上锁为止。
同样,考虑到上锁过程往往对应了一段函数或一个程序段的开始和结束,可以对应到一个临时变量的生命周期中,我们再次封装一个"生命周期锁类":
class LifeCircleMutexLocker { pthread_mutex_t* mutex; public: LifeCircleMutexLocker(pthread_mutex_t* aMutex) : mutex(aMutex) { pthread_mutex_lock(mutex); } ~LifeCircleMutexLocker(){ pthread_mutex_unlock(mutex); } }; #define LifeCircleMutexLock(mutex) LifeCircleMutexLocker __locker__(mutex)
一切准备就绪后,就剩下两个核心的接口函数--向队列发出消息以及由队列将消息发到消息中心中,相关代码如下:
//由队列将消息发到消息中心 void MTNotificationQueue::postNotifications(ccTime dt) { //生命周期锁 // 用一个类LifeCircleMutexLock管理互斥锁sharedNotificationQueueLock的生命周期
LifeCircleMutexLock(&sharedNotificationQueueLock);
for(int i = 0; i < notifications.size(); i++) { NotificationArgs &arg = notifications[i]; // 调用主线程通知函数,将所有消息发送到消息中心
CCNotificationCenter::sharedNotificationCenter()->postNotification(arg.name.c_str(), arg.object); } notifications.clear(); } // 向队列发出消息 void MTNotificationQueue::postNotification(const char* name, CCObject* object) { //生命周期锁 LifeCircleMutexLock(&sharedNotificationQueueLock); NotificationArgs arg; arg.name = name; if(object != NULL) arg.object = object->copy(); else arg.object = NULL; notifications.push_back(arg); }
实际上,这是两个非常简短的函数,仅仅是将传入的消息缓冲到数组中并取出。唯一的特别之处只在于函数在开始时,使用了我们前面定义的"生命周期锁",保证了在访问缓冲数组的过程中是线程安全的,整个读写过程中缓冲数组由当前线程独占。
最后,我们启动消息队列的定时器,使 postNotifications 函数每帧被调用,保证不同线程间发出的消息能第一时间送达主线程:
CCDirector::sharedDirector()->getScheduler()->scheduleSelector( schedule_selector(MTNotificationQueue::postNotifications), MTNotificationQueue::sharedNotificationQueue(), 1.0 / 60.0, false);
有了这个消息池,就可以进一步简化之前的图片加载过程了。下面仍然使用背景层的例子,再次重写游戏背景层的初始化函数:
bool BackgroundLayer::init() { LOG_FUNCTION_LIFE; bool bRet = false; do { CC_BREAK_IF(! CCLayer::init()); CCNotificationCenter::sharedNotificationCenter()->addObserver( this, callfuncO_selector(BackgroundLayer::loadImageFinish), "loadImageFinish", NULL); pthread_t tid; pthread_create(&tid, NULL, &loadImages, NULL); bRet = true; } while (0); return bRet; }
我们不再按照注释中的做法那样使用系统的纹理缓存来异步添加背景图片,而是先注册到消息中心,而后主动创建一个线程负责加载图片。在该线程中,我们仅完成图片向内存的加载,相关代码如下:
void* loadImages(void* arg) { bgImage = new CCImage(); bgImage->initWithImageFileThreadSafe("background.png"); MTNotificationQueue::sharedNotificationQueue()->postNotification("loadImageFinish", NULL); return NULL; }
在加载完成之后,我们通过消息队列发出了一个加载完成的消息,在稍后的消息队列更新时,这个消息将会被发送到消息中心,而后通知到背景层的响应函数中。我们为背景层添加相应的响应函数 loadImageFinish,其代码如下:
void BackgroundLayer::loadImageFinish(CCObject* sender) { CCSize winSize = CCDirector::sharedDirector()->getWinSize(); CCTexture2D* texture = CCTextureCache::sharedTextureCache()->addUIImage(bgImage, "background.png"); bgImage->release(); CCSprite* bg = CCSprite::create(texture); CCSize size = bg->getContentSize(); bg->setPosition(ccp(winSize.width / 2, winSize.height / 2)); float f = max(winSize.width/size.width,winSize.height/size.height); bg->setScale(f); this->addChild(bg); }
这里 bgImage 是用 new 方式创建的,堆空间是除了全局静态对象之外唯一可以在线程间共享的数据空间。
必须注意的是,作为共享数据的 bgImage 的内存管理方式,我们在加载线程中用 new 从堆空间中分配了该内存,但是并没有遵守内存管理规范在该函数中将其释放,因为此时 bgImage 还未使用完毕,也不可能调用自动释放池,因为在子线程中是不存在自动释放池的,如果跨线程调用了自动释放池,将造成严重的紊乱。因此,我们最后在 loadimageFinish 中添加到纹理缓存后才将其释放。
这也是使用多线程进行并发编程时的一个比较大的障碍,由于引擎的内存管理体系 CCObject 是非线程安全的,而整个引擎又是搭建在 CCObject 提供的内存管理机制基础上的,因此我们在多线程环境中使用引擎的任何对象都必须分外小心。
二、cocos2dx消息中心:
自行查阅相关源码, 没有使用多线程.
使用CCNotificationCenter需要注意以下几点:
(1)一个对象可以注册多个消息,一个消息也可以由多个消息注册。
(2)传递参数,A可以向B传递参数,而B在注册的时候也可以带一个参数,如果这两个数据不是指向同一对象的话,消息不会传递。也就是说要么A传递NULL对象,要么B注册时带NULL对象,要么都不是NULL但必须是同一对象,消息传递才会成功。以下是发送消息执行的判断:
if (!strcmp(name,observer->getName()) && (observer->getObject() == object || observer->getObject() == NULL || object == NULL))
(3)局部变量的传递,注意到上例,传递的是CCString的一个局部变量(但还是要autorelease),从CCNotificationCenter的实现上来看,这是没有问题的,因为数据是在postNotification被调用的,也就是整个函数体并没结束,数据不会被销毁。