Cocos2D-JS 加载速度优化

前一段时间对Cocos2D-JS的项目做了一次载入速度优化,在这里记录一下。

一、问题分析

优化前游戏在iPhone 4上从启动画面到渲染第一帧需要8秒左右,一直卡在启动画面不动。分析了一下代码,怀疑AppDelegate::didFinishLaunchWithOptions里做了太多事情。用Instruments分析一下,果然didFinishLaunchWithOptions用了5s,其中ScriptingCore::runScript用了2.5s,向JSContext注入binding用了0.5s,剩下各种SDK初始化用了2s。

Instruments

二、优化方案

1. 加速代码的执行速度

ScriptingCore::runScript

ScriptingCore::runScript主要在读取js代码、编译然后执行。这里有几个优化的方法: 1. 将JS代码编译成bytecode(jsc)再打到包里,这样加载时就不用再编译了。 2. 将JS代码用UglifyJS、JSMin等压缩工具压缩,并合并成一个JS文件,减少磁盘IO的大小和次数。

压缩打包JS会带来一些问题。压缩后错误信息会比较难看,因为symbol都被压成1个字母了。另一个更严重的问题是,游戏有动态更新代码的需求,不打包可以单独更新改动的JS文件,打包成一个文件后每次都得更新整个JS文件。

并行化

Instruments的数据里可以看出有米广告的SDK居然用了1.3s载入,在5s上也需要200ms,干脆放到单独的线程里去做,这样不会block主线程(iPhone4还是单核的A4处理器,所以开多少线程都没有什么卵用,4s和iPad2之后用的至少是双核的A5,收效就很明显)。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
    // 有米广告的初始化代码
    [CocoBVideo cBVideoInitWithAppID:appID cBVideoAppIDSecret:secret];
    [CocoBVideo cBCloseAlertViewWhenWantExit:false];
});

2. 从用户体验的角度优化

上面这些耗时的事都是启动游戏时必须要做的,即使充分优化也可能要占用可观的时间。我们想一下优化速度的目的是什么:给用户更好的启动体验避免加载时间过长导致流失。从这角度想:假如游戏载入必须要花很长时间,有什么方法能让载入过程的体验更好。

我的方案是,didFinishLaunchWithOptions里显示出一个带进度条的载入界面,然后一边载入各种东西一边更新进度条,避免特别长时间的UI静止。这样玩家就不会以为游戏卡死而流失。整个启动载入的流程如下: 1. 显示C++的LoadingLayer(游戏背景 + 进入条) 2. 初始化各种第三方SDK 3. 初始化JS Context 4. JS代码中的游戏逻辑初始化,载入存档、数据表等等 5. JS代码载入主界面资源,显示有主界面

LoadingLayer得用cocos2D-x C++来写,这样就可以在JS Context初始化之前显示出来。并且尽量让这个UI不要用太多资源。JS Context初始化完成后,LoadingLayer上的进度条转由JS来控制,为了让JS能方便得到ProgressTimer对象,最简单的方式是在C++里给LoadingLayer和ProgressTimer设置一个特殊的tag。

想让用户看到能进度条更新,需要把上面的工作分到不同帧里来完成,好在这在3.0版本里很容易,把工作拆开放到一个lambda里schedule一下就行了,就和JS里德setTimeout(func, 0)一样,这样UI就有时间更新了。另外JS的资源载入最好用TextureCache的addImageAsync方法,以免block主线程。

auto func = [this] (float t) { _setupLibraries(); };
director->getScheduler()->schedule(func, this, 0.0, 0, 0, false, "_setupLibraries");

另外有一个问题是,Cocos2D-JS 3.0之后didFinishLaunchWithOptions返回时,第一帧并没有渲染,导致屏幕会黑一下,需要强制渲染避免这个问题。

用关键代码来描述一下整个加载过程

AppDelegate.cpp:

bool AppDelegate::applicationDidFinishLaunching() {
    ...
    // 生成 LoadingLayer
    _loadingLayer = LoadingLayer::create();
    _loadingLayer->setTag(100);
    Scene * scene = Scene::create();
    scene->addChild(_loadingLayer);
    // 强制渲染第一帧
    director->runWithScene(scene);
    director->drawScene();
    director->getRenderer()->render();
    auto func = [this] (float t) { _setupLibraries(); };
    director->getScheduler()->schedule(func, this, 0.0, 0, 0, false, "_setupLibraries");
}

void AppDelegate::_setupLibraries() {
    // 在这里初始化各种SDK ...
    // 更新进度,下一帧载入JS Context
    _loadingLayer->setPercent(40);
    _loadingLayer->setPercent(20);
    auto func = [this] (float t) { _setupJSBEnv(); };
    Director::getInstance()->getScheduler()->schedule(func, this, 0.0, 0, 0, false, "_setupJSBEnv");
}

void AppDelegate::_setupJSBEnv() {
    // 初始化JS Context
    ScriptingCore* sc = ScriptingCore::getInstance();
    sc->addRegisterCallback(register_all_cocos2dx);
    ...
    // 执行Cocos2D-JS的启动脚本
    auto func = [this] (float t) { _runBootScript(); };
    Director::getInstance()->getScheduler()->schedule(func, this, 0.0, 0, 0, false, "_runBootScript");
}

void AppDelegate::_runBootScript() {
    ScriptingCore::getInstance()->runScript("script/jsb_boot.js");
    ScriptingCore::getInstance()->runScript("js/main.js");
    _loadingLayer->setPercent(50);
}

main.js:

cc.game.onStart = function() {
    var addProgress = function(percent) {
        if (!cc.sys.isNative) return;
        var progress = cc.director.getRunningScene().getChildByTag(100).getChildByTag(100);
        progress.setPercentage(progress.getPercentage() + percent);
    };
    //load resources
    cc.LoaderScene.preload(g_resources, function() {
        cl.init(); // js init code
        addProgress(10);
        setTimeout(function() {
            var res = cl.getCommonRes().concat(cl.ModeSelectLayer.getRes());
            Util.preloadRes(res, function() {
                addProgress(100);
                setTimeout(function() { Util.runScene(cl.ModeSelectLayer)  }, 0);
            }, addProgress.bind(null, 2));
        }, 0);
    });
}

经过这些优化,游戏会很快从Launch Image进入Loading界面,进度条随着载入的进行不断更新,整个载入过程在iPhone4上也从6s降到了3s,体验比之前好了很多。

三、一些问题

强制渲染第一帧cc.game.restart方法重启游戏时崩溃,因为restart时也会调用didFinishLaunchWithOptions,解决办法是重启时不强制渲染,需要用一个全局变量startCount来识别是否为重启。

if (startCount == 1) {
    director->drawScene();
    director->getRenderer()->render();
    auto func = [this] (float t) { _setupLibraries(); };
    director->getScheduler()->schedule(func, this, 0.0, 0, 0, false, "_setupLibraries");
} else {
    _setupJSBEnv();
}

另外由于JS Context的初始化被延后了,有一些刚起动就执行的功能会遇到一些问题。比如点击微信的App消息、remote push启动游戏,需要先把状态暂存,等JS Context起来后,在调用相关逻辑。

This entry was posted in 技术学习 and tagged , . Bookmark the permalink.

One Response to Cocos2D-JS 加载速度优化

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据