前一段时间对Cocos2D-JS的项目做了一次载入速度优化,在这里记录一下。
一、问题分析
优化前游戏在iPhone 4上从启动画面到渲染第一帧需要8秒左右,一直卡在启动画面不动。分析了一下代码,怀疑AppDelegate::didFinishLaunchWithOptions里做了太多事情。用Instruments分析一下,果然didFinishLaunchWithOptions用了5s,其中ScriptingCore::runScript用了2.5s,向JSContext注入binding用了0.5s,剩下各种SDK初始化用了2s。
二、优化方案
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起来后,在调用相关逻辑。
One Response to Cocos2D-JS 加载速度优化