Cocos2d-x目前仍然是一個單線程的遊戲引擎,這使得我們幾乎不需要考慮遊戲對象更新的線程安全性。然而我們仍然需要關注一些情形如網絡請求,異步加載檔案,或者異步處理一些邏輯算法等等。分析Cocos2d-x的主線程
一、在主線程執行異步處理結果
有一些方法必須在主線程執行,例如GL相關的方法。另一些時候,為了保證如Ref對象引用計數的線程安全,我們也應該在主線程去執行這些操作。Scheduler提供了一種簡單的機制,使可以在主線程上執行一個方法:
void Scheduler::performFunctionInCocosThread(const std::function &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清單:
TextureCache向Scheduler注冊一個更新回調addImageAsyncCallBack:
void TextureCache::addImageAsyncCallBack(float dt)
{
// the image is generated in loading thread
std::deque *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中的“異步回調”。