[√]cocos creator 热更新源码剖析

1,788 阅读9分钟

基于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
};

image.png