Merge pull request #2567 from minggo/AssetsManager-asynchronization

Make AssetsManager download resources in a new thread
This commit is contained in:
minggo 2013-05-15 20:24:05 -07:00
commit 1a947ce1cd
4 changed files with 363 additions and 70 deletions

View File

@ -49,34 +49,49 @@ NS_CC_EXT_BEGIN;
#define BUFFER_SIZE 8192 #define BUFFER_SIZE 8192
#define MAX_FILENAME 512 #define MAX_FILENAME 512
AssetsManager::AssetsManager() // Message type
: _packageUrl("") #define ASSETSMANAGER_MESSAGE_UPDATE_SUCCEED 0
, _versionFileUrl("") #define ASSETSMANAGER_MESSAGE_RECORD_DOWNLOADED_VERSION 1
, _version("") #define ASSETSMANAGER_MESSAGE_PROGRESS 2
, _curl(NULL) #define ASSETSMANAGER_MESSAGE_ERROR 3
// Some data struct for sending messages
struct ErrorMessage
{
AssetsManager::ErrorCode code;
AssetsManager* manager;
};
struct ProgressMessage
{
int percent;
AssetsManager* manager;
};
// Implementation of AssetsManager
AssetsManager::AssetsManager(const char* packageUrl/* =NULL */, const char* versionFileUrl/* =NULL */, const char* storagePath/* =NULL */)
: _storagePath(storagePath)
, _version("")
, _packageUrl(packageUrl)
, _versionFileUrl(versionFileUrl)
, _downloadedVersion("")
, _curl(NULL)
, _tid(0)
, _connectionTimeout(0)
, _delegate(NULL)
{ {
_storagePath = CCFileUtils::sharedFileUtils()->getWritablePath();
checkStoragePath(); checkStoragePath();
_schedule = new Helper();
} }
AssetsManager::AssetsManager(const char* packageUrl, const char* versionFileUrl) AssetsManager::~AssetsManager()
: _packageUrl(packageUrl)
, _version("")
, _versionFileUrl(versionFileUrl)
, _curl(NULL)
{ {
_storagePath = CCFileUtils::sharedFileUtils()->getWritablePath(); if (_schedule)
checkStoragePath(); {
} _schedule->release();
}
AssetsManager::AssetsManager(const char* packageUrl, const char* versionFileUrl, const char* storagePath)
: _packageUrl(packageUrl)
, _version("")
, _versionFileUrl(versionFileUrl)
, _storagePath(storagePath)
, _curl(NULL)
{
checkStoragePath();
} }
void AssetsManager::checkStoragePath() void AssetsManager::checkStoragePath()
@ -114,10 +129,12 @@ bool AssetsManager::checkUpdate()
curl_easy_setopt(_curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(_curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(_curl, CURLOPT_WRITEFUNCTION, getVersionCode); curl_easy_setopt(_curl, CURLOPT_WRITEFUNCTION, getVersionCode);
curl_easy_setopt(_curl, CURLOPT_WRITEDATA, &_version); curl_easy_setopt(_curl, CURLOPT_WRITEDATA, &_version);
if (_connectionTimeout) curl_easy_setopt(_curl, CURLOPT_CONNECTTIMEOUT, _connectionTimeout);
res = curl_easy_perform(_curl); res = curl_easy_perform(_curl);
if (res != 0) if (res != 0)
{ {
sendErrorMessage(kNetwork);
CCLOG("can not get version file content, error code is %d", res); CCLOG("can not get version file content, error code is %d", res);
curl_easy_cleanup(_curl); curl_easy_cleanup(_curl);
return false; return false;
@ -126,6 +143,7 @@ bool AssetsManager::checkUpdate()
string recordedVersion = CCUserDefault::sharedUserDefault()->getStringForKey(KEY_OF_VERSION); string recordedVersion = CCUserDefault::sharedUserDefault()->getStringForKey(KEY_OF_VERSION);
if (recordedVersion == _version) if (recordedVersion == _version)
{ {
sendErrorMessage(kNoNewVersion);
CCLOG("there is not new version"); CCLOG("there is not new version");
// Set resource search path. // Set resource search path.
setSearchPath(); setSearchPath();
@ -137,8 +155,46 @@ bool AssetsManager::checkUpdate()
return true; return true;
} }
void* assetsManagerDownloadAndUncompress(void *data)
{
AssetsManager* self = (AssetsManager*)data;
do
{
if (self->_downloadedVersion != self->_version)
{
if (! self->downLoad()) break;
// Record downloaded version.
AssetsManager::Message *msg1 = new AssetsManager::Message();
msg1->what = ASSETSMANAGER_MESSAGE_RECORD_DOWNLOADED_VERSION;
msg1->obj = self;
self->_schedule->sendMessage(msg1);
}
// Uncompress zip file.
if (! self->uncompress())
{
self->sendErrorMessage(AssetsManager::kUncompress);
break;
}
// Record updated version and remove downloaded zip file
AssetsManager::Message *msg2 = new AssetsManager::Message();
msg2->what = ASSETSMANAGER_MESSAGE_UPDATE_SUCCEED;
msg2->obj = self;
self->_schedule->sendMessage(msg2);
} while (0);
self->_tid = 0;
return NULL;
}
void AssetsManager::update() void AssetsManager::update()
{ {
if (_tid) return;
// 1. Urls of package and version should be valid; // 1. Urls of package and version should be valid;
// 2. Package should be a zip file. // 2. Package should be a zip file.
if (_versionFileUrl.size() == 0 || if (_versionFileUrl.size() == 0 ||
@ -153,36 +209,9 @@ void AssetsManager::update()
if (! checkUpdate()) return; if (! checkUpdate()) return;
// Is package already downloaded? // Is package already downloaded?
string downloadedVersion = CCUserDefault::sharedUserDefault()->getStringForKey(KEY_OF_DOWNLOADED_VERSION); _downloadedVersion = CCUserDefault::sharedUserDefault()->getStringForKey(KEY_OF_DOWNLOADED_VERSION);
if (downloadedVersion != _version)
{
if (! downLoad()) return;
// Record downloaded version. pthread_create(&_tid, NULL, assetsManagerDownloadAndUncompress, this);
CCUserDefault::sharedUserDefault()->setStringForKey(KEY_OF_DOWNLOADED_VERSION, _version.c_str());
CCUserDefault::sharedUserDefault()->flush();
}
// Uncompress zip file.
if (! uncompress()) return;
// Record new version code.
CCUserDefault::sharedUserDefault()->setStringForKey(KEY_OF_VERSION, _version.c_str());
// Unrecord downloaded version code.
CCUserDefault::sharedUserDefault()->setStringForKey(KEY_OF_DOWNLOADED_VERSION, "");
CCUserDefault::sharedUserDefault()->flush();
// Set resource search path.
setSearchPath();
// Delete unloaded zip file.
string zipfileName = _storagePath + TEMP_PACKAGE_FILE_NAME;
if (remove(zipfileName.c_str()) != 0)
{
CCLOG("can not remove downloaded zip file");
}
} }
bool AssetsManager::uncompress() bool AssetsManager::uncompress()
@ -202,6 +231,7 @@ bool AssetsManager::uncompress()
{ {
CCLOG("can not read file global info of %s", outFileName.c_str()); CCLOG("can not read file global info of %s", outFileName.c_str());
unzClose(zipfile); unzClose(zipfile);
return false;
} }
// Buffer to hold data read from the zip file // Buffer to hold data read from the zip file
@ -348,8 +378,19 @@ static size_t downLoadPackage(void *ptr, size_t size, size_t nmemb, void *userda
return written; return written;
} }
static int progressFunc(void *ptr, double totalToDownload, double nowDownloaded, double totalToUpLoad, double nowUpLoaded) int assetsManagerProgressFunc(void *ptr, double totalToDownload, double nowDownloaded, double totalToUpLoad, double nowUpLoaded)
{ {
AssetsManager* manager = (AssetsManager*)ptr;
AssetsManager::Message *msg = new AssetsManager::Message();
msg->what = ASSETSMANAGER_MESSAGE_PROGRESS;
ProgressMessage *progressData = new ProgressMessage();
progressData->percent = (int)(nowDownloaded/totalToDownload*100);
progressData->manager = manager;
msg->obj = progressData;
manager->_schedule->sendMessage(msg);
CCLOG("downloading... %d%%", (int)(nowDownloaded/totalToDownload*100)); CCLOG("downloading... %d%%", (int)(nowDownloaded/totalToDownload*100));
return 0; return 0;
@ -362,6 +403,7 @@ bool AssetsManager::downLoad()
FILE *fp = fopen(outFileName.c_str(), "wb"); FILE *fp = fopen(outFileName.c_str(), "wb");
if (! fp) if (! fp)
{ {
sendErrorMessage(kCreateFile);
CCLOG("can not create file %s", outFileName.c_str()); CCLOG("can not create file %s", outFileName.c_str());
return false; return false;
} }
@ -372,11 +414,13 @@ bool AssetsManager::downLoad()
curl_easy_setopt(_curl, CURLOPT_WRITEFUNCTION, downLoadPackage); curl_easy_setopt(_curl, CURLOPT_WRITEFUNCTION, downLoadPackage);
curl_easy_setopt(_curl, CURLOPT_WRITEDATA, fp); curl_easy_setopt(_curl, CURLOPT_WRITEDATA, fp);
curl_easy_setopt(_curl, CURLOPT_NOPROGRESS, false); curl_easy_setopt(_curl, CURLOPT_NOPROGRESS, false);
curl_easy_setopt(_curl, CURLOPT_PROGRESSFUNCTION, progressFunc); curl_easy_setopt(_curl, CURLOPT_PROGRESSFUNCTION, assetsManagerProgressFunc);
curl_easy_setopt(_curl, CURLOPT_PROGRESSDATA, this);
res = curl_easy_perform(_curl); res = curl_easy_perform(_curl);
curl_easy_cleanup(_curl); curl_easy_cleanup(_curl);
if (res != 0) if (res != 0)
{ {
sendErrorMessage(kNetwork);
CCLOG("error when download package"); CCLOG("error when download package");
fclose(fp); fclose(fp);
return false; return false;
@ -429,4 +473,132 @@ void AssetsManager::deleteVersion()
CCUserDefault::sharedUserDefault()->setStringForKey(KEY_OF_VERSION, ""); CCUserDefault::sharedUserDefault()->setStringForKey(KEY_OF_VERSION, "");
} }
void AssetsManager::setDelegate(AssetsManagerDelegateProtocol *delegate)
{
_delegate = delegate;
}
void AssetsManager::setConnectionTimeout(unsigned int timeout)
{
_connectionTimeout = timeout;
}
unsigned int AssetsManager::getConnectionTimeout()
{
return _connectionTimeout;
}
void AssetsManager::sendErrorMessage(AssetsManager::ErrorCode code)
{
Message *msg = new Message();
msg->what = ASSETSMANAGER_MESSAGE_ERROR;
ErrorMessage *errorMessage = new ErrorMessage();
errorMessage->code = code;
errorMessage->manager = this;
msg->obj = errorMessage;
_schedule->sendMessage(msg);
}
// Implementation of AssetsManagerHelper
AssetsManager::Helper::Helper()
{
_messageQueue = new list<Message*>();
pthread_mutex_init(&_messageQueueMutex, NULL);
CCDirector::sharedDirector()->getScheduler()->scheduleUpdateForTarget(this, 0, false);
}
AssetsManager::Helper::~Helper()
{
CCDirector::sharedDirector()->getScheduler()->unscheduleAllForTarget(this);
delete _messageQueue;
}
void AssetsManager::Helper::sendMessage(Message *msg)
{
pthread_mutex_lock(&_messageQueueMutex);
_messageQueue->push_back(msg);
pthread_mutex_unlock(&_messageQueueMutex);
}
void AssetsManager::Helper::update(float dt)
{
Message *msg = NULL;
// Returns quickly if no message
pthread_mutex_lock(&_messageQueueMutex);
if (0 == _messageQueue->size())
{
pthread_mutex_unlock(&_messageQueueMutex);
return;
}
// Gets message
msg = *(_messageQueue->begin());
_messageQueue->pop_front();
pthread_mutex_unlock(&_messageQueueMutex);
switch (msg->what) {
case ASSETSMANAGER_MESSAGE_UPDATE_SUCCEED:
handleUpdateSucceed(msg);
break;
case ASSETSMANAGER_MESSAGE_RECORD_DOWNLOADED_VERSION:
CCUserDefault::sharedUserDefault()->setStringForKey(KEY_OF_DOWNLOADED_VERSION,
((AssetsManager*)msg->obj)->_version.c_str());
CCUserDefault::sharedUserDefault()->flush();
break;
case ASSETSMANAGER_MESSAGE_PROGRESS:
if (((ProgressMessage*)msg->obj)->manager->_delegate)
{
((ProgressMessage*)msg->obj)->manager->_delegate->onProgress(((ProgressMessage*)msg->obj)->percent);
}
delete (ProgressMessage*)msg->obj;
break;
case ASSETSMANAGER_MESSAGE_ERROR:
// error call back
if (((ErrorMessage*)msg->obj)->manager->_delegate)
{
((ErrorMessage*)msg->obj)->manager->_delegate->onError(((ErrorMessage*)msg->obj)->code);
}
delete ((ErrorMessage*)msg->obj);
break;
default:
break;
}
delete msg;
}
void AssetsManager::Helper::handleUpdateSucceed(Message *msg)
{
AssetsManager* manager = (AssetsManager*)msg->obj;
// Record new version code.
CCUserDefault::sharedUserDefault()->setStringForKey(KEY_OF_VERSION, manager->_version.c_str());
// Unrecord downloaded version code.
CCUserDefault::sharedUserDefault()->setStringForKey(KEY_OF_DOWNLOADED_VERSION, "");
CCUserDefault::sharedUserDefault()->flush();
// Set resource search path.
manager->setSearchPath();
// Delete unloaded zip file.
string zipfileName = manager->_storagePath + TEMP_PACKAGE_FILE_NAME;
if (remove(zipfileName.c_str()) != 0)
{
CCLOG("can not remove downloaded zip file %s", zipfileName.c_str());
}
if (manager) manager->_delegate->onSuccess();
}
NS_CC_EXT_END; NS_CC_EXT_END;

View File

@ -27,12 +27,15 @@
#include <string> #include <string>
#include <curl/curl.h> #include <curl/curl.h>
#include <pthread.h>
#include "cocos2d.h" #include "cocos2d.h"
#include "ExtensionMacros.h" #include "ExtensionMacros.h"
NS_CC_EXT_BEGIN NS_CC_EXT_BEGIN
class AssetsManagerDelegateProtocol;
/* /*
* This class is used to auto update resources, such as pictures or scripts. * This class is used to auto update resources, such as pictures or scripts.
* The updated package should be a zip file. And there should be a file named * The updated package should be a zip file. And there should be a file named
@ -43,20 +46,26 @@ class AssetsManager
public: public:
enum ErrorCode enum ErrorCode
{ {
ServerNotAvailable, /** server address error or timeout */ // Error caused by creating a file to store downloaded data
TimeOut, kCreateFile,
}; /** Error caused by network
-- network unavaivable
//! Default constructor. You should set server address later. -- timeout
AssetsManager(); -- ...
/* @brief Creates a AssetsManager with new package url and version code url.
* AssetsManager will use the value returned by CCFileUtils::getWritablePath() as storage path.
*
* @param packageUrl URL of new package, the package should be a zip file.
* @param versionFileUrl URL of version file. It should contain version code of new package.
*/ */
AssetsManager(const char* packageUrl, const char* versionFileUrl); kNetwork,
/** There is not a new version
*/
kNoNewVersion,
/** Error caused in uncompressing stage
-- can not open zip file
-- can not read file global information
-- can not read file information
-- can not create a directory
-- ...
*/
kUncompress,
};
/* @brief Creates a AssetsManager with new package url, version code url and storage path. /* @brief Creates a AssetsManager with new package url, version code url and storage path.
* *
@ -64,7 +73,9 @@ public:
* @param versionFileUrl URL of version file. It should contain version code of new package. * @param versionFileUrl URL of version file. It should contain version code of new package.
* @param storagePath The path to store downloaded resources. * @param storagePath The path to store downloaded resources.
*/ */
AssetsManager(const char* packageUrl, const char* versionFileUrl, const char* storagePath); AssetsManager(const char* packageUrl = NULL, const char* versionFileUrl = NULL, const char* storagePath = NULL);
virtual ~AssetsManager();
/* @brief Check out if there is a new version resource. /* @brief Check out if there is a new version resource.
* You may use this method before updating, then let user determine whether * You may use this method before updating, then let user determine whether
@ -112,12 +123,55 @@ public:
*/ */
void setStoragePath(const char* storagePath); void setStoragePath(const char* storagePath);
/** @brief Sets delegate, the delegate will receive messages
*/
void setDelegate(AssetsManagerDelegateProtocol *delegate);
/** @brief Sets connection time out in seconds
*/
void setConnectionTimeout(unsigned int timeout);
/** @brief Gets connection time out in secondes
*/
unsigned int getConnectionTimeout();
/* downloadAndUncompress is the entry of a new thread
*/
friend void* assetsManagerDownloadAndUncompress(void*);
friend int assetsManagerProgressFunc(void *, double, double, double, double);
protected: protected:
bool downLoad(); bool downLoad();
void checkStoragePath(); void checkStoragePath();
bool uncompress(); bool uncompress();
bool createDirectory(const char *path); bool createDirectory(const char *path);
void setSearchPath(); void setSearchPath();
void sendErrorMessage(ErrorCode code);
private:
typedef struct _Message
{
public:
_Message() : what(0), obj(NULL){}
unsigned int what; // message type
void* obj;
} Message;
class Helper : public cocos2d::CCObject
{
public:
Helper();
~Helper();
virtual void update(float dt);
void sendMessage(Message *msg);
private:
void handleUpdateSucceed(Message *msg);
std::list<Message*> *_messageQueue;
pthread_mutex_t _messageQueueMutex;
};
private: private:
//! The path to store downloaded resources. //! The path to store downloaded resources.
@ -129,7 +183,33 @@ private:
std::string _packageUrl; std::string _packageUrl;
std::string _versionFileUrl; std::string _versionFileUrl;
std::string _downloadedVersion;
CURL *_curl; CURL *_curl;
Helper *_schedule;
pthread_t _tid;
unsigned int _connectionTimeout;
AssetsManagerDelegateProtocol *_delegate; // weak reference
};
class AssetsManagerDelegateProtocol
{
public:
/* @brief Call back function for error
@param errorCode Type of error
*/
virtual void onError(AssetsManager::ErrorCode errorCode) {};
/** @brief Call back function for recording downloading percent
@param percent How much percent downloaded
@warn This call back function just for recording downloading percent.
AssetsManager will do some other thing after downloading, you should
write code in onSuccess() after downloading.
*/
virtual void onProgress(int percent) {};
/** @brief Call back function for success
*/
virtual void onSuccess() {};
}; };
NS_CC_EXT_END; NS_CC_EXT_END;

View File

@ -77,6 +77,7 @@ UpdateLayer::UpdateLayer()
: pItemEnter(NULL) : pItemEnter(NULL)
, pItemReset(NULL) , pItemReset(NULL)
, pItemUpdate(NULL) , pItemUpdate(NULL)
, pProgressLabel(NULL)
, isUpdateItemClicked(false) , isUpdateItemClicked(false)
{ {
init(); init();
@ -90,6 +91,8 @@ UpdateLayer::~UpdateLayer()
void UpdateLayer::update(cocos2d::CCObject *pSender) void UpdateLayer::update(cocos2d::CCObject *pSender)
{ {
pProgressLabel->setString("");
// update resources // update resources
getAssetsManager()->update(); getAssetsManager()->update();
@ -98,6 +101,8 @@ void UpdateLayer::update(cocos2d::CCObject *pSender)
void UpdateLayer::reset(cocos2d::CCObject *pSender) void UpdateLayer::reset(cocos2d::CCObject *pSender)
{ {
pProgressLabel->setString(" ");
// Remove downloaded files // Remove downloaded files
#if (CC_TARGET_PLATFORM != CC_PLATFORM_WIN32) #if (CC_TARGET_PLATFORM != CC_PLATFORM_WIN32)
string command = "rm -r "; string command = "rm -r ";
@ -152,6 +157,10 @@ bool UpdateLayer::init()
menu->setPosition(ccp(0,0)); menu->setPosition(ccp(0,0));
addChild(menu); addChild(menu);
pProgressLabel = CCLabelTTF::create("", "Arial", 20);
pProgressLabel->setPosition(ccp(100, 50));
addChild(pProgressLabel);
return true; return true;
} }
@ -164,6 +173,8 @@ AssetsManager* UpdateLayer::getAssetsManager()
pAssetsManager = new AssetsManager("https://raw.github.com/minggo/AssetsManagerTest/master/package.zip", pAssetsManager = new AssetsManager("https://raw.github.com/minggo/AssetsManagerTest/master/package.zip",
"https://raw.github.com/minggo/AssetsManagerTest/master/version", "https://raw.github.com/minggo/AssetsManagerTest/master/version",
pathToSave.c_str()); pathToSave.c_str());
pAssetsManager->setDelegate(this);
pAssetsManager->setConnectionTimeout(3);
} }
return pAssetsManager; return pAssetsManager;
@ -190,3 +201,28 @@ void UpdateLayer::createDownloadedDir()
} }
#endif #endif
} }
void UpdateLayer::onError(AssetsManager::ErrorCode errorCode)
{
if (errorCode == AssetsManager::kNoNewVersion)
{
pProgressLabel->setString("no new version");
}
if (errorCode == AssetsManager::kNetwork)
{
pProgressLabel->setString("network error");
}
}
void UpdateLayer::onProgress(int percent)
{
char progress[20];
snprintf(progress, 20, "downloading %d%%", percent);
pProgressLabel->setString(progress);
}
void UpdateLayer::onSuccess()
{
pProgressLabel->setString("download ok");
}

View File

@ -44,7 +44,7 @@ public:
virtual void applicationWillEnterForeground(); virtual void applicationWillEnterForeground();
}; };
class UpdateLayer : public cocos2d::CCLayer class UpdateLayer : public cocos2d::CCLayer, public cocos2d::extension::AssetsManagerDelegateProtocol
{ {
public: public:
UpdateLayer(); UpdateLayer();
@ -55,6 +55,10 @@ public:
void reset(cocos2d::CCObject *pSender); void reset(cocos2d::CCObject *pSender);
void update(cocos2d::CCObject *pSender); void update(cocos2d::CCObject *pSender);
virtual void onError(cocos2d::extension::AssetsManager::ErrorCode errorCode);
virtual void onProgress(int percent);
virtual void onSuccess();
private: private:
cocos2d::extension::AssetsManager* getAssetsManager(); cocos2d::extension::AssetsManager* getAssetsManager();
void createDownloadedDir(); void createDownloadedDir();
@ -62,6 +66,7 @@ private:
cocos2d::CCMenuItemFont *pItemEnter; cocos2d::CCMenuItemFont *pItemEnter;
cocos2d::CCMenuItemFont *pItemReset; cocos2d::CCMenuItemFont *pItemReset;
cocos2d::CCMenuItemFont *pItemUpdate; cocos2d::CCMenuItemFont *pItemUpdate;
cocos2d::CCLabelTTF *pProgressLabel;
std::string pathToSave; std::string pathToSave;
bool isUpdateItemClicked; bool isUpdateItemClicked;
}; };