分析Cocos2d-x的主线程

2015年03月24日 16:23 0 点赞 0 评论 更新于 2025-11-21 18:30

Cocos2d - x目前仍是一个单线程的游戏引擎,这让我们在处理游戏对象更新时,几乎无需考虑线程安全性问题。不过,对于网络请求、异步加载文件或异步处理逻辑算法等情形,我们仍需予以关注。

一、在主线程执行异步处理结果

部分方法必须在主线程执行,例如与GL相关的方法。此外,为确保Ref对象引用计数的线程安全,一些操作也应在主线程进行。Scheduler提供了一种简单机制,可用于在主线程上执行方法:

void Scheduler::performFunctionInCocosThread(const std::function<void()>& function)
{
_performMutex.lock();
_functionsToPerform.push_back(function);
_performMutex.unlock();
}

首先,我们向Scheduler注册一个方法指针。Scheduler会存储一个需要在主线程执行的方法指针数组。在当前帧所有系统或自定义的schedule执行完毕后,Scheduler会检查该数组并执行其中的方法:

void Scheduler::update(float dt)
{
if (!_functionsToPerform.empty()) {
_performMutex.lock();
// fixed #4123: Save the callback functions, they must be invoked after ‘_performMutex.unlock()’,
// otherwise if new functions are added in callback, it will cause thread deadlock.
auto temp = _functionsToPerform;
_functionsToPerform.clear();
_performMutex.unlock();
for (const auto& function : temp) {
function();
}
}
}

通过这种机制,我们能够将方法转移到主线程执行。需要注意的是,这些方法在主线程的执行时机是所有系统或自定义的schedule之后,也就是在UI树遍历之前。

二、文件异步加载完成

在上述机制中,所有向Scheduler注册的方法会在该帧结束时全部执行。对于简单算法而言,这并无问题,就像图左边的function列表所示。但对于一些耗时的计算,为避免影响游戏性能,我们需要将一系列耗时的方法分布在每一帧执行。

Cocos2d - x纹理异步加载完成后,需将纹理上传至GL内存,因此该传输过程必须在主线程执行。然而,glTexImage2D命令用于上传纹理,是一个耗时操作。假设多个图片同时完成加载,若这些纹理在同一帧上传至GL内存,可能会导致UI界面卡顿,影响用户体验。

为此,Cocos2d - x的纹理异步加载回调采用了自定义的schedule进行处理。在该schedule内部,会检查已完成加载的纹理,每一帧处理一个纹理,直至所有纹理处理完毕,然后注销schedule。最终,纹理在主线程的执行情况如图右边的file列表所示。

TextureCacheScheduler注册一个更新回调addImageAsyncCallBack

void TextureCache::addImageAsyncCallBack(float dt)
{
// the image is generated in loading thread
std::deque<ImageInfo*>* imagesQueue = _imageInfoQueue;

_imageInfoMutex.lock();
if (imagesQueue->empty())
{
_imageInfoMutex.unlock();
}
else
{
ImageInfo* imageInfo = imagesQueue->front();
imagesQueue->pop_front();
_imageInfoMutex.unlock();

AsyncStruct* asyncStruct = imageInfo->asyncStruct;
Image* image = imageInfo->image;

const std::string& filename = asyncStruct->filename;

Texture2D* texture = nullptr;
if (image)
{
// generate texture in render thread
texture = new Texture2D();

texture->initWithImage(image);

#if CC_ENABLE_CACHE_TEXTURE_DATA
// cache the texture file name
VolatileTextureMgr::addImageTexture(texture, filename);
#endif
// cache the texture. retain it, since it is added in the map
_textures.insert(std::make_pair(filename, texture));
texture->retain();

texture->autorelease();
}
else
{
auto it = _textures.find(asyncStruct->filename);
if (it != _textures.end())
texture = it->second;
}

asyncStruct->callback(texture);
if (image)
{
image->release();
}
delete asyncStruct;
delete imageInfo;

--_asyncRefCount;
if (0 == _asyncRefCount)
{
Director::getInstance()->getScheduler()->unschedule(schedule_selector(TextureCache::addImageAsyncCallBack), this);
}
}
}

当向TextureCache发起异步文件加载请求时,TextureCache会向Scheduler注册更新回调addImageAsyncCallback,并开启新线程异步加载文件。新线程中文件加载完成后,会将纹理数据存储在_imageInfoQueue中。主线程每帧更新回调时会检查该队列是否有数据,若有则将纹理数据缓存到TextureCache中,上传纹理至GL内存,然后删除_imageInfoQueue中的数据。最后,当所有文件加载完毕,注销更新回调。

三、异步处理的单元测试

在主线程上执行所有逻辑算法,可显著降低程序复杂度,并且能在某些方面较为自由地使用多线程。然而,Cocos2d - x的这种回调机制给单元测试带来了困难,因为它依赖于Cocos2d - x的主循环。

单元测试通常用于测试同步方法,只需执行该方法,就能知晓运行结果,且单元测试甚至无需依赖过多上下文,过多上下文反而会增加单元测试的难度。

对于异步方法,人们会在单元测试中加入“等待时间”,监听回调函数对某个布尔变量值的修改,以此告知回调完成,进而完成单元测试。通过这种方式可以测试异步方法。

但Cocos2d - x中的异步回调需要游戏循环来驱动,单元测试除了监听异步回调,还需驱动游戏循环才能执行Schedule,这使得单元测试变得复杂。在本书最后一章,我们将给出一种解决方案,用于测试Cocos2d - x中的“异步回调”。

作者信息

feifeila

feifeila

共发布了 3994 篇文章