基于cocos creator 2.4.11
版本,其他版本应该差异不大,可以作为参考,
热更新的核心实现都在jsb.AssetsManager
,对应的C++实现就是frameworks\cocos2d-x\extensions\assets-manager\AssetsManagerEx.cpp
基于热更新的流程阅读分析代码,更加直观。
热更新的一切的源头
this._assetsMgr.update();
void AssetsManagerEx::update()
{
if (_updateEntry != UpdateEntry::NONE)
{
CCLOGERROR("AssetsManagerEx::update, updateEntry isn't NONE");
return;
}
// 检查是否进行了初始化
if (!_inited){
CCLOG("AssetsManagerEx : Manifests uninited.\n");
dispatchUpdateEvent(EventAssetsManagerEx::EventCode::ERROR_NO_LOCAL_MANIFEST);
return;
}
if (!_localManifest->isLoaded())
{
CCLOG("AssetsManagerEx : No local manifest file found error.\n");
dispatchUpdateEvent(EventAssetsManagerEx::EventCode::ERROR_NO_LOCAL_MANIFEST);
return;
}
_updateEntry = UpdateEntry::DO_UPDATE;
switch (_updateState) {
case State::UNCHECKED:
{
_updateState = State::PREDOWNLOAD_VERSION;
}
case State::PREDOWNLOAD_VERSION:
{
downloadVersion();
}
break;
case State::VERSION_LOADED:
{
parseVersion();
}
break;
case State::PREDOWNLOAD_MANIFEST:
{
downloadManifest();
}
break;
case State::MANIFEST_LOADED:
{
parseManifest();
}
break;
case State::FAIL_TO_UPDATE:
case State::READY_TO_UPDATE:
case State::NEED_UPDATE:
{
// Manifest not loaded yet
if (!_remoteManifest->isLoaded())
{
_updateState = State::PREDOWNLOAD_MANIFEST;
downloadManifest();
}
else if (_updateEntry == UpdateEntry::DO_UPDATE)
{
startUpdate();
}
}
break;
case State::UP_TO_DATE:
case State::UPDATING:
case State::UNZIPPING:
_updateEntry = UpdateEntry::NONE;
break;
default:
break;
}
}
- 设置初始化的逻辑
void AssetsManagerEx::initManifests()
{
_inited = true;
_canceled = false;
// Init and load temporary manifest
_tempManifest = new (std::nothrow) Manifest();
if (_tempManifest)
{
// 尝试去解析manifest文件,而这个文件
_tempManifest->parseFile(_tempManifestPath);
// Previous update is interrupted
if (_fileUtils->isFileExist(_tempManifestPath))
{
// Manifest parse failed, remove all temp files
if (!_tempManifest->isLoaded())
{
_fileUtils->removeDirectory(_tempStoragePath);
CC_SAFE_RELEASE(_tempManifest);
_tempManifest = nullptr;
}
}
}
else
{
_inited = false;
}
// Init remote manifest for future usage
_remoteManifest = new (std::nothrow) Manifest();
if (!_remoteManifest)
{
_inited = false;
}
if (!_inited)
{
CC_SAFE_RELEASE(_localManifest);
CC_SAFE_RELEASE(_tempManifest);
CC_SAFE_RELEASE(_remoteManifest);
_localManifest = nullptr;
_tempManifest = nullptr;
_remoteManifest = nullptr;
}
}
manifest数据结构
- project.manfest的数据结构
{
"version": "1.0",
"packageUrl": "http://192.168.1.134:5520",
"remoteManifestUrl": "http://192.168.1.134:5520/project.manifest",
"remoteVersionUrl": "http://192.168.1.134:5520/version.manifest",
"searchPaths": []
"assets": {
"src/cocos2d-jsb.js": {
"size": 3335419,
"md5": "eb37f3077bcb635d9b68bea6893ed217"
},
}
}
- version.manifest
{
"version": "1.0",
"packageUrl": "http://192.168.1.134:5520",
"remoteManifestUrl": "http://192.168.1.134:5520/project.manifest",
"remoteVersionUrl": "http://192.168.1.134:5520/version.manifest"
}
version是project的精简版,可能是考虑到传输速率,尽快得知是否需要热更。
AssetsManagerEx初始化
热更新的入口逻辑,一般我们会在js层初始化
let url = manifest.nativeUrl;// manifest: cc.Asset
let storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'remote-asset');
this._assetsMgr = new jsb.AssetsManager(url, storagePath,
(versionLocal, versionRemote) => {
}
);
在初始化AssetsManager的时候,有2个参数、3个参数:
#define MANIFEST_FILENAME "project.manifest"
AssetsManagerEx::AssetsManagerEx(
// 项目对应的本机manifest url
// 比如assets/main/native/a1/a1d2694c-d3e7-4fd9-b7fc-58b171be56b1.manifest
const std::string& manifestUrl,
// 热更文件的存储路径,必须是可写目录下的一个目录
const std::string& storagePath,
// 自定义的版本对比函数,js层可以不设置,会执行对应2个参数的构造函数
const VersionCompareHandle& handle
)
{
setStoragePath(storagePath);
_cacheManifestPath = _storagePath + MANIFEST_FILENAME;
loadLocalManifest(manifestUrl);
}
void AssetsManagerEx::setStoragePath(const std::string& storagePath)
{
// 设置storagePath
_storagePath = storagePath;
_fileUtils->createDirectory(_storagePath);
// 同步创建一个临时目录 remote-asset_temp
_tempStoragePath = _storagePath;
_tempStoragePath.insert(_storagePath.size() - 1, TEMP_PACKAGE_SUFFIX);
_fileUtils->createDirectory(_tempStoragePath);
}
// loadLocalManifest的逻辑有点多,需要耐心看,难度不大。
bool AssetsManagerEx::loadLocalManifest(const std::string& manifestUrl)
{
_manifestUrl = manifestUrl;
// Init and load local manifest
_localManifest = new (std::nothrow) Manifest();
Manifest *cachedManifest = nullptr;
// 查找上次下载的project.manifest.cache
if (_fileUtils->isFileExist(_cacheManifestPath))
{
cachedManifest = new (std::nothrow) Manifest();
cachedManifest->parseFile(_cacheManifestPath);// 读取解析文件
if (!cachedManifest->isLoaded())
{
// 如果文件无效,会删除掉
_fileUtils->removeFile(_cacheManifestPath);
CC_SAFE_RELEASE(cachedManifest);
cachedManifest = nullptr;
}
}
std::vector<std::string> searchPaths = _fileUtils->getSearchPaths();
if (cachedManifest)
{
// 将project.manifest.cache配置的searchPaths从游戏搜索路径中删除
std::vector<std::string> cacheSearchPaths = cachedManifest->getSearchPaths();
std::vector<std::string> trimmedPaths = searchPaths;
for (auto path : cacheSearchPaths)
{
const auto pos = std::find(trimmedPaths.begin(), trimmedPaths.end(), path);
if (pos != trimmedPaths.end())
{
trimmedPaths.erase(pos);
}
}
_fileUtils->setSearchPaths(trimmedPaths);
}
// Load local manifest in app package
_localManifest->parseFile(_manifestUrl);
if (cachedManifest)
{
// Restore search paths
_fileUtils->setSearchPaths(searchPaths);
}
if (_localManifest->isLoaded())
{
if (cachedManifest)
{
// 比较project.manifest.local和project.manifest.cache的版本号
// 这里用到了handle参数,如果没有则使用默认的比较函数
bool localNewer = _localManifest->versionGreater(cachedManifest, _versionCompareHandle);
if (localNewer)
{
// 如果local高于cache,清空下载目录
// 一般不会出现这种情况,不过用户很容易配置错误,导致这种情况
_fileUtils->removeDirectory(_storagePath);
_fileUtils->createDirectory(_storagePath);
CC_SAFE_RELEASE(cachedManifest);
}
else
{
CC_SAFE_RELEASE(_localManifest);
// 将cache的数据作为后续的基准
_localManifest = cachedManifest;
}
}
// 将manifest文件所在的目录(assets/main/native/a1/),包括配置里面searchPaths的路径,都加入到搜索路径
prepareLocalManifest();
}
// Fail to load local manifest
if (!_localManifest->isLoaded())
{
// 初始化阶段会遇到第一个异常,没有manifest文件或者解析json失败发生问题时,会抛出这个异常
CCLOG("AssetsManagerEx : No local manifest file found error.\n");
dispatchUpdateEvent(EventAssetsManagerEx::EventCode::ERROR_NO_LOCAL_MANIFEST);
return false;
}
// 这里就没啥逻辑了,都是一些准备工作
initManifests();
_updateState = State::UNCHECKED;
return true;
}
这里有一个问题,如果发生了异常,这个异常无论是c++还是js层都无法捕获到,因为这是发生在new阶段,压根就没有机会去注册_eventCallback
。
// 内部的实现
void AssetsManagerEx::dispatchUpdateEvent(){
if (_eventCallback != nullptr) {
EventAssetsManagerEx* event = new (std::nothrow) EventAssetsManagerEx(_eventName, this, code, assetId, message, curle_code, curlm_code);
_eventCallback(event);// 回调
event->release();
}
}
// 构造函数初始化
AssetsManagerEx::AssetsManagerEx(): _eventCallback(nullptr){}
// 设置回调的api
void setEventCallback(const EventCallback& callback) {_eventCallback = callback;};
虽然也有提供api setEventCallback
,但是你必须实例化类之后才能调用,在实例化类的过程中就会触发这个异常。
manifest解析
里面牵扯到了对manifest的数据解析,发现的其他字段:
- groupVersions
- updating
- engineVersion
assets的其他字段
- downloadState
- compressed
- path
能不能使用2个参数的构造函数
可以,但是js层这样就拿不到版本号了,目前也没有其他方式让js层知道版本号信息,这是一个比较蛋疼的设计。
默认的比较版本函数
static int cmpVersion(const std::string& v1, const std::string& v2)
{
int i;
int oct_v1[4] = {0}, oct_v2[4] = {0};
int filled1 = std::sscanf(v1.c_str(), "%d.%d.%d.%d", &oct_v1[0], &oct_v1[1], &oct_v1[2], &oct_v1[3]);
int filled2 = std::sscanf(v2.c_str(), "%d.%d.%d.%d", &oct_v2[0], &oct_v2[1], &oct_v2[2], &oct_v2[3]);
if (filled1 == 0 || filled2 == 0)
{
return strcmp(v1.c_str(), v2.c_str());
}
for (i = 0; i < 4; i++)
{
if (oct_v1[i] > oct_v2[i])
return 1;
else if (oct_v1[i] < oct_v2[i])
return -1;
}
return 0;
}
回调函数
_versionCompareHandle
int versionCompareHandle (string versionLocal, string versionRemote){}
- 0 (false)表示
versionLocal < versionRemote
,即会触发热更新 - !0 (true) 表示
versionLocal >= versionRemote
,即不需要热更新,会触发ALREADY_UP_TO_DATE
事件
setVerifyCallback
对照c++,js能看的更加明白。
struct ManifestAsset {
std::string md5;
std::string path;
bool compressed;
float size;
int downloadState;
};
typedef ManifestAsset Asset;
typedef std::function<bool(const std::string& path, Manifest::Asset asset)> VerifyCallback;
void setVerifyCallback(const VerifyCallback& callback) {_verifyCallback = callback;};
this._assetsMgr.setVerifyCallback((assetsFullPath, asset) => {
let {compressed, md5, path, size} = asset;
if (compressed) {
return true;
} else {
return true;
}
})
当下载一个文件后,会调用该回调,感觉设计上是希望用户作一些校验工作
void AssetsManagerEx::onSuccess(const std::string &/*srcUrl*/, const std::string &storagePath, const std::string &customId)
{
bool ok = true;
auto &assets = _remoteManifest->getAssets();
auto assetIt = assets.find(customId);
if (assetIt != assets.end())
{
Manifest::Asset asset = assetIt->second;
if (_verifyCallback != nullptr)
{
ok = _verifyCallback(storagePath, asset);
}
}
if (ok)
{
bool compressed = assetIt != assets.end() ? assetIt->second.compressed : false;
if (compressed)
{
decompressDownloadedZip(customId, storagePath);
}
else
{
fileSuccess(customId, storagePath);
}
}
else
{
fileError(customId, "Asset file verification failed after downloaded");
}
}
setEventCallback
热更新发生的异常,都会通过该回调函数通知到js层。 一般来说只需要设置一次就行了。
class CC_EX_DLL EventAssetsManagerEx : public cocos2d::Ref
{
public:
//! Update events code
enum class EventCode
{
ERROR_NO_LOCAL_MANIFEST,
ERROR_DOWNLOAD_MANIFEST,
ERROR_PARSE_MANIFEST,
NEW_VERSION_FOUND,
ALREADY_UP_TO_DATE,
UPDATE_PROGRESSION,
ASSET_UPDATED,
ERROR_UPDATING,
UPDATE_FINISHED,
UPDATE_FAILED,
ERROR_DECOMPRESS
};
inline EventCode getEventCode() const { return _code; };
inline int getCURLECode() const { return _curle_code; };
inline int getCURLMCode() const { return _curlm_code; };
inline std::string getMessage() const { return _message; };
inline std::string getAssetId() const { return _assetId; };
inline cocos2d::extension::AssetsManagerEx *getAssetsManagerEx() const { return _manager; };
bool isResuming() const;
float getPercent() const;
float getPercentByFile() const;
double getDownloadedBytes() const;
double getTotalBytes() const;
int getDownloadedFiles() const;
int getTotalFiles() const;
}
typedef std::function<void(EventAssetsManagerEx *event)> EventCallback;
void setEventCallback(const EventCallback& callback) {_eventCallback = callback;};
checkUpdate
void AssetsManagerEx::checkUpdate()
{
if (_updateEntry != UpdateEntry::NONE)
{
CCLOGERROR("AssetsManagerEx::checkUpdate, updateEntry isn't NONE");
return;
}
// 检查更新的时候会做一些基础的检查工作,比如本地的project.manifest是无效的,就会抛出异常:
if (!_inited){
CCLOG("AssetsManagerEx : Manifests uninited.\n");
dispatchUpdateEvent(EventAssetsManagerEx::EventCode::ERROR_NO_LOCAL_MANIFEST);
return;
}
if (!_localManifest->isLoaded())
{
CCLOG("AssetsManagerEx : No local manifest file found error.\n");
dispatchUpdateEvent(EventAssetsManagerEx::EventCode::ERROR_NO_LOCAL_MANIFEST);
return;
}
_updateEntry = UpdateEntry::CHECK_UPDATE;
switch (_updateState) {
case State::FAIL_TO_UPDATE:
_updateState = State::UNCHECKED;
case State::UNCHECKED:
case State::PREDOWNLOAD_VERSION:
{
// 首先会尝试下载version.manifest
downloadVersion();
}
break;
case State::UP_TO_DATE:
{
dispatchUpdateEvent(EventAssetsManagerEx::EventCode::ALREADY_UP_TO_DATE);
}
break;
case State::NEED_UPDATE:
{
dispatchUpdateEvent(EventAssetsManagerEx::EventCode::NEW_VERSION_FOUND);
}
break;
default:
break;
}
}
void AssetsManagerEx::downloadVersion()
{
// 获取version.manifest的url
std::string versionUrl = _localManifest->getVersionFileUrl();
if (versionUrl.size() > 0)
{
_updateState = State::DOWNLOADING_VERSION;
// 创建下载version.manifest的任务,当version文件下载下来后,就会比对版本号,如果需要热更就会继续去下载project.manifest文件
_downloader->createDownloadFileTask(versionUrl, _tempVersionPath, VERSION_ID);
}else{
// 没有version.manifest字段的信息,就会去尝试下载project.manifest
CCLOG("AssetsManagerEx : No version file found, step skipped\n");
_updateState = State::PREDOWNLOAD_MANIFEST;
downloadManifest();
}
}
void AssetsManagerEx::downloadManifest()
{
// 获取project.version的地址
std::string manifestUrl = _localManifest->getManifestFileUrl();
if (manifestUrl.size() > 0)
{
_updateState = State::DOWNLOADING_MANIFEST;
// 创建下载project.manifest的任务,当project.manifest下载完成后,会再次比对版本号
// 同时收集需要更新的文件大小数量等信息,但是这些信息js层无法拿到
// getTotalBytes getTotalFiles是挂在EventAssetsManagerEx身上
// 并派发事件,告诉js层需要更新 NEW_VERSION_FOUND
_downloader->createDownloadFileTask(manifestUrl, _tempManifestPath, MANIFEST_ID);
}
else
{
// 如果project.manifest都没有,就会抛出异常
CCLOG("AssetsManagerEx : No manifest file found, check update failed\n");
dispatchUpdateEvent(EventAssetsManagerEx::EventCode::ERROR_DOWNLOAD_MANIFEST);
_updateState = State::UNCHECKED;
}
}
下载的文件都会放到temp里面,接下来的事情就交给Downloader
Downloader
几个比较重要的下载回调
- onDataTaskSuccess
- onFileTaskSuccess
- onTaskProgress
- onTaskError
开启了一个线程进行下载,使用到了curl,每个下载文件都会创建一个DownloadTask
manifest下载失败的处理
void AssetsManagerEx::onError(const network::DownloadTask& task,
int errorCode,
int errorCodeInternal,
const std::string& errorStr)
{
if (task.identifier == VERSION_ID)
{
// 当收到下载version.manifest失败后,会尝试下载project.manifest
CCLOG("AssetsManagerEx : Fail to download version file, step skipped\n");
_updateState = State::PREDOWNLOAD_MANIFEST;
downloadManifest();
}
else if (task.identifier == MANIFEST_ID)
{
// 当收到project.manifest也下载失败了,就会抛出异常
dispatchUpdateEvent(EventAssetsManagerEx::EventCode::ERROR_DOWNLOAD_MANIFEST, task.identifier, errorStr, errorCode, errorCodeInternal);
_updateState = State::FAIL_TO_UPDATE;
}
else
{
if (_downloadingTask.find(task.identifier) != _downloadingTask.end()) {
_downloadingTask.erase(task.identifier);
}
// 热更过程中下载失败
fileError(task.identifier, errorStr, errorCode, errorCodeInternal);
}
}
update
开始更新下载需要的文件
void AssetsManagerEx::update()
{
// .. 一些错误检查
_updateEntry = UpdateEntry::DO_UPDATE;
switch (_updateState) {
case State::FAIL_TO_UPDATE:
case State::READY_TO_UPDATE:
case State::NEED_UPDATE:
{
if (_updateEntry == UpdateEntry::DO_UPDATE)
{
// 一般正常流程都会命中这个逻辑
startUpdate();
}
}
break;
}
}
void AssetsManagerEx::startUpdate()
{
if (_updateState == State::READY_TO_UPDATE)
{
// Start to update 6 files from remote package.
msg = StringUtils::format("Start to update %d files from remote package.", _totalToDownload);
dispatchUpdateEvent(EventAssetsManagerEx::EventCode::UPDATE_PROGRESSION, "", msg);
batchDownload(); // 批量创建下载任务队列
}
}
更新成功
触发事件UPDATE_FINISHED
,js层收到后软重启即可
cc.audioEngine.stopAll();
cc.game.restart();
Uncaught TypeError: Cannot read property 'emit' of null, location: src/cocos2d-jsb.js:0:0
STACK:
[0]_onTouchEnded@src/cocos2d-jsb.js:25740
[1]253.proto.emit@src/cocos2d-jsb.js:41837
[2]_doDispatchEvent@src/cocos2d-jsb.js:15311
[3]dispatchEvent@src/cocos2d-jsb.js:16193
[4]_touchEndHandler@src/cocos2d-jsb.js:15200
[5]_onTouchEventCallback@src/cocos2d-jsb.js:32750
[6]_dispatchEventToListeners@src/cocos2d-jsb.js:32836
[7]_dispatchTouchEvent@src/cocos2d-jsb.js:32785
[8]dispatchEvent@src/cocos2d-jsb.js:33037
[9]handleTouchesEnd@src/cocos2d-jsb.js:39827
[10]_mouseEventsOnElement@src/cocos2d-jsb.js:39986
[11]anonymous@src/cocos2d-jsb.js:40005
[12]dispatchEvent@jsb-adapter/jsb-builtin.js:3077
[13]anonymous@jsb-adapter/jsb-builtin.js:3149
_onTouchEnded: function _onTouchEnded(event) {
if (!this.interactable || !this.enabledInHierarchy) return;
if (this._pressed) {
cc.Component.EventHandler.emitEvents(this.clickEvents, event);
this.node.emit("click", this); // 这里报的错
}
this._pressed = false;
this._updateState();
event.stopPropagation();
},
ERROR: Uncaught TypeError: Cannot read property 'length' of undefined, location: src/cocos2d-jsb.js:0:0
STACK:
[0]lookupClasses@src/cocos2d-jsb.js:42198
[1]unpackJSONs@src/cocos2d-jsb.js:42274
[2]unpackJson@src/cocos2d-jsb.js:19647
[3]unpack@src/cocos2d-jsb.js:19680
[4]anonymous@src/cocos2d-jsb.js:19703
[5]finale@src/cocos2d-jsb.js:18985
[6]anonymous@src/cocos2d-jsb.js:20636
[7]anonymous@src/cocos2d-jsb.js:18962
[8]anonymous@jsb-adapter/jsb-engine.js:3183
[9]readFile@jsb-adapter/jsb-engine.js:3149
[10]readJson@jsb-adapter/jsb-engine.js:3171
[11]parseJson@jsb-adapter/jsb-engine.js:3468
[12]download@jsb-adapter/jsb-engine.js:3354
[13]downloadJson@jsb-adapter/jsb-engine.js:3480
[14]invoke@src/cocos2d-jsb.js:18956
[15]process@src/cocos2d-jsb.js:18966
[16]retry@src/cocos2d-jsb.js:20634
[17]download@src/cocos2d-jsb.js:18990
[18]load@src/cocos2d-jsb.js:19700
[19]fetch@src/cocos2d-jsb.js:19514
事件ID
enum class EventCode
{
ERROR_NO_LOCAL_MANIFEST,
ERROR_DOWNLOAD_MANIFEST,
ERROR_PARSE_MANIFEST,
NEW_VERSION_FOUND,
ALREADY_UP_TO_DATE,
UPDATE_PROGRESSION,
ASSET_UPDATED,
ERROR_UPDATING,
UPDATE_FINISHED,
UPDATE_FAILED,
ERROR_DECOMPRESS
};