cocos2d-x 中有三个跟 OpenGL 相关的性能指标,游戏运行时显示在左下角;cocos2d-x 默认会显示这些性能指标,可以手动在代码在开启或关闭,C++ 项目在 AppDelegate 的 applicationDidFinishLaunching 函数中设置
Director::getInstance()->setDisplayStats(true);
lua 项目在配置文件 config.lua 中设置
CC_SHOW_FPS = true
FPS(Frames Per Second)
fps 是帧率,顾名思义就是每秒绘制多少帧。帧率设置得超高,画面刷新得越频繁,游戏看起来更顺畅;但这也意味着每秒要做的运算更多,更加耗 CPU 或 GPU,而且如果运算量过大的话,可能会出现卡帧。一般游戏的帧率不会低于 30 帧,实时性要求比较高的场景可以设置成 60 帧。这个指标可以帮助我们查看游戏在什么时候计算量过大,然后降低计算量或分散计算到多个帧中去。
GL Calls
gl call 是 opengl 渲染的次数,即每一帧调用 opengl 指令的次数。这个数字越小越好,优化的方向有
使用 SpriteBatchNode 是 2.x 的做法,3.0 之后官方不建议使用。这种方式要求所有 Sprite 都要先添加到 SpriteBatchNode 上,虽然能提升效率,但有很多限制,而且某些情况下反而会降低效率。比如有 Sprite 在屏幕外时,SpriteBtachNode 并不会作检查,而是直接渲染所有的 Sprite;而直接使用 Sprite 的方式会判断 Sprite 是否在屏幕外,如果在屏幕外则不渲染。3.0 之后直接使用 Sprite 也可批量处理,只要多个 Sprite 使用同张纹理并且使用相同的着色器和混合函数(SpriteBatchNode 也有这些要求),所以推荐直接使用 Sprite 而不是 SpriteBatchNode。
参考文章 https://github.com/fusijie/Cocos2dx-Release-Note/blob/master/cocos2d-x_v3.0_release_notes.md#user-content-sprite-%E5%92%8C-spritebatchnode
使用 SpriteBatchNode
local batch = cc.SpriteBatchNode:create("popcap.png")
for i = 1, 10 do
local sprite = cc.Sprite:create("popcap.png")
batch:addChild(sprite)
end
self:addChild(batch)
batch:setPosition(cc.p(win_size.width / 2, win_size.height / 2))
直接使用 Sprite
for i = 1, 10 do
self._icon=cc.Sprite:create("popcap.png")
self._icon:setPosition(cc.p(win_size.width / 2, win_size.height / 2))
self:addChild(self._icon)
end
两种情况的 gl call 都是 1
把图片事先加载到精灵帧缓冲区并不影响 gl call,只是创建精灵时会快一些而已;但把小图片打包成合图,再加载到精灵帧缓冲区,则可以降低 gl call。
直接使用 Sprite
for i = 1, 10 do
self._icon=cc.Sprite:create("popcap.png")
self._icon:setPosition(cc.p(win_size.width / 2, win_size.height / 2))
self:addChild(self._icon)
end
for i = 1, 10 do
self._icon=cc.Sprite:create("popcap2.png")
self._icon:setPosition(cc.p(win_size.width / 2, win_size.height / 2))
self:addChild(self._icon)
end
使用 SpriteFrameCache
local sf = sfc:getSpriteFrameByName("popcap.png")
for i = 1, 10 do
self._icon = cc.Sprite:createWithSpriteFrame(sf)
self._icon:setPosition(cc.p(win_size.width / 2, win_size.height / 2))
self:addChild(self._icon)
end
sf = sfc:getSpriteFrameByName("popcap2.png")
for i = 1, 10 do
self._icon = cc.Sprite:createWithSpriteFrame(sf)
self._icon:setPosition(cc.p(win_size.width / 2, win_size.height / 2))
self:addChild(self._icon)
end
第二种方式因为 popcap.png 和 popcap2.png 在同一个合图里,所以创建二十个精灵只需要绘制一次;而第一种方式 popcap.png 和 popcap2.png 是两张不同的精灵,所以会绘制两次。
对于屏幕外或者被隐藏的 UI,将其 visible 设为 false,这样就不会绘制;如果设置透明度为 0,则还是会绘制。
self._icon = cc.Sprite:create("popcap.png")
self._icon:setPosition(cc.p(win_size.width / 2, win_size.height / 2))
self._icon:setOpacity(0)
self:addChild(self._icon)
self._icon = cc.Sprite:create("popcap.png")
self._icon:setPosition(cc.p(win_size.width / 2, win_size.height / 2))
self._icon:setVisible(false)
self:addChild(self._icon)
首先,从 android-ndk-profiler 上下载,直接 download zip 压缩包或者使用 git
git clone git@github.com:richq/android-ndk-profiler.git
下下来之后的目录结构
|-android-ndk-profiler
|-docs
|-example
|-jni
|-test
我们要用的就是 jni 目录下的文件,在特定的目录下新建一个文件夹 android-ndk-profiler
,把 jni 目录下的所有文件拷到新建的文件夹下面。这里说的特定的目录是指 ndk 能搜索到的目录,我用的 cocos2d-x3.10,有两种创建项目的方式,源代码方式和预编译库方式,使用源代码方式创建的项目 ndk 能搜索到的目录如下:
使用预编译库创建的项目 ndk 能搜索到的目录如下:
总结起来就是 \%COCOS_ROOT%; %COCOS_ROOT%/cocos; %COCOS_ROOT%/external; %COCOS_ROOT%/scripting 这四个路径。只不过源代码的方式会在项目的根目录下创建一个 cocos2d-x 目录,然后把引擎的源代码拷贝过来,因此 ndk 搜索的目录基于项目路径。而预编译库的方式不会拷贝任何源代码,因此 ndk 搜索的路径基于引擎安装的目录。
把 android-ndk-profiler/jni 下面的文件拷贝到这四个路径之一,比如拷贝到 %COCOS_ROOT%/external/android-ndk-profiler 目录下,然后修改 android.mk 文件,添加下面的代码
APP_DEBUG := $(strip $(NDK_DEBUG))
ifeq ($(APP_DEBUG),1)
LOCAL_CFLAGS := -pg
LOCAL_STATIC_LIBRARIES += android-ndk-profiler
endif
ifeq ($(APP_DEBUG),1)
$(call import-module,android-ndk-profiler)
endif
然后以 debug 方式进行编译,编译成功说明 android-ndk-profiler 已经成功集成到项目了。编译方式有
默认工程的编译方式和 ndk 的编译方式都是 debug,所以直接使用下面命令即可
cocos compile -p android
如果不想把 android-ndk-profiler/jni 下面的文件拷贝到这四个路径之一,而是想放在其它地方,则要扩展 ndk 路径,在 android.mk 中添加
ANDROID_NDK_PROFILER_PATH := D:/work/project/frameworks
$(call import-add-path,$(ANDROID_NDK_PROFILER_PATH))
LOCAL_C_INCLUDES += $(ANDROID_NDK_PROFILER_PATH)
这样就可以把文件放在 D:/work/project/frameworks
目录下了。
接下来就是在代码中使用 android-ndk-profiler 了,打开 AppDelegate.cpp,添加下面代码
#if (COCOS2D_DEBUG>0 && CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include "android-ndk-profiler/prof.h"
#endif
bool AppDelegate::applicationDidFinishLaunching()
{
//...
#if (COCOS2D_DEBUG>0 && CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
monstartup("libcocos2dlua.so");
#endif
}
void AppDelegate::applicationDidEnterBackground()
{
//...
#if (COCOS2D_DEBUG>0 && CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
moncleanup();
#endif
}
monstartup 的参数根据具体的项目类型而定,我创建的是 lua 项目,所以是 libcocos2dlua.so,可以到 proj.android/lib/\armeabi
下查看。
然后重新进行编译,可能会出现下面的错误
jni/../../Classes/AppDelegate.cpp:82: error: undefined reference to 'monstartup'
这是因为在链接 android-ndk-profiler 库时失败,打开 android.mk,添加
LOCAL_STATIC_LIBRARIES := cocos2d_lua_static
LOCAL_STATIC_LIBRARIES += cocos2d_simulator_static
# 新添加
LOCAL_STATIC_LIBRARIES += android-ndk-profiler
再重新编译,可能会出现下面的错误
这是因为使用的 ndk 版本没有 ucontext_t 这个类型,换了 ndk 版本就可以了,像我开始用的是 ndk-r10c,改用 ndk-r10e 就可以了。
还有一点要注意的,为了能正常生成报告,我们的游戏必须有写入文件的权限,打开 AndroidMainfest.xml 文件,看看有没有下面这一句,如果没有的话则加上
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
把 android-ndk-profiler 集成在游戏之后,在 android 手机上跑一遍游戏,会生成一个 gmon.out 文件,这个文件就是我们需要的性能分析报告。默认生成的 gmon.out 的路径为 /sdcard/gmon.out,即放在内部存储的根目录。如果想自定义存放位置,则在调用 moncleanup
之前指定存放位置
setenv("CPUPROFILE", "/data/data/com.example.application/files/gmon.out", 1);
moncleanup();
把 gmon.out 拷贝到电脑上,这是一个二进制文件,无法直接查看内容,得先转换成文本文件。ndk 提供了专门的转换工具
cd android-ndk-r10e/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/
./arm-linux-androideabi-gprof \
项目路径/proj.android/obj/local/armeabi/libcocos2dlua.so \
存放路径/gmon.out > gmon.txt
下面看看 gmon.txt 的内容
Flat profile:
Each sample counts as inf seconds.
% cumulative self self total
time seconds seconds calls Ts/call Ts/call name
6.52 inf inf __addsf3
5.98 inf inf cocos2d::MathUtilC::transformVec4(float const*, float, float, float, float, float*)
4.89 inf inf inflate_fast
3.26 inf inf __mulsf3
2.17 inf inf cocos2d::Node::processParentFlags(cocos2d::Mat4 const&, unsigned int)
1.63 inf inf cocos2d::ComponentContainer::visit(float)
1.63 inf inf cocos2d::Node::visit(cocos2d::Renderer*, cocos2d::Mat4 const&, unsigned int)
1.36 inf inf cocos2d::Renderer::fillQuads(cocos2d::QuadCommand const*)
1.36 inf inf void cocos2d::Scheduler::scheduleUpdate::Node>(cocos2d::Node*, int, bool)::{lambda(float)#1}::operator()(float) const
1.36 inf inf adler32
1.09 inf inf std::_Deque_iterator::Mat4, cocos2d::Mat4&, cocos2d::Mat4*>::operator--()
0.82 inf inf cocos2d::ui::Button::adaptRenderers()
0.82 inf inf cocos2d::Director::drawScene()
0.82 inf inf cocos2d::Renderer::drawBatchedTriangles()
0.82 inf inf cocos2d::Node::isVisitableByVisitingCamera() const
Call graph (explanation follows)
granularity: each sample hit covers 2 byte(s) for 0.17% of 5.99 seconds
index % time self children called name
[1] 19.4 1.16 0.00 png_read_filter_row_paeth_multibyte_pixel [1]
-----------------------------------------------
[2] 15.5 0.93 0.00 cocos2d::Image::premultipliedAlpha() [2]
-----------------------------------------------
[3] 14.5 0.87 0.00 cocos2d::Texture2D::convertRGBA8888ToRGBA4444(unsigned char const*, int, unsigned char*) [3]
-----------------------------------------------
[4] 7.2 0.43 0.00 profCount [4]
-----------------------------------------------
[5] 4.0 0.24 0.00 png_read_filter_row_up [5]
-----------------------------------------------
解释一下含义
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
函数 程序 函数 函数 函数 函数 函数名
消耗 累计 本身 调用 平均 平均
时间 执行 执行 次数 执行 执行
占程 时间 时间 时间 时间
序运 (不 (包
行时 包 括
间的 括 被
百分 被 调
比 调 用
用 时
时 间)
间)
Call graph (explanation follows)
granularity: each sample hit covers 2 byte(s) for 0.17% of 5.99 seconds
index % time self children called name
索引 函数 函数 函数的 被调用 函数名
值 执行 本身 子函数 次数
时间 执行 执行
占程 时间 时间
序运
行时
间百
分比