感谢
- https://www.163.com/dy/article/F7UI0ODL0511G03U.html
- https://blog.csdn.net/bill_man/article/details/43884095
- https://blog.csdn.net/u011004567/article/details/79156899
- https://www.jianshu.com/p/3eb844c9d8df
- https://zhuanlan.zhihu.com/p/106333994
- https://docs.cocos.com/cocos2d-x/manual/zh/advanced_topics/optimizing.html
- https://blog.csdn.net/shimazhuge/article/details/78222023
- https://forum.cocos.org/t/topic/90248
- https://www.codercto.com/a/21132.html
- https://www.233tw.com/cocos/6512
- https://forum.cocos.org/t/creator/77721
- https://forum.cocos.org/t/topic/95043
https://forum.cocos.org/t/topic/41774/2
一、黄金法则
2/8原则:20%的代码消耗80%的性能,我们应致力于寻找这20%的代码,而不是见一个优化一个,这很可能会“吃力不讨好”。
- 够用原则:使用可接受的最低资源精度。比如RGBA4444代替RGBA8888,音频单声道代替多声道、采样率降低等。
数据说话原则:通过数据分析问题,切勿主观臆断。借助工具跟踪GPU、CPU、内存占用情况。
二、数据分析
1、开发测试阶段
GPU跟踪
- Xcode OpenGL ES Tools
- ARM Mali GPU: mali-graphics-debugge
- Imagination PowerVR GPU: pvrtune
- Qualcomm Adreno GPU: adreno-gpu-profiler
- CPU跟踪
- Xcode-Instrument
- VS CPU profiler
- Android monitor/Profiler
- Memory Monitor:内存分配、内存泄露
- Network Monitor:App网络请求
- CPU Monitor
- GPU Monitor
- GT APP:对App进行快速的性能测试(CPU、内存、流量、电量、帧率/流畅度等)、开发日志的查看、Crash日志查看、网络数据包的抓取、App内部参数的调试、真机代码耗时统计等。
- Emmagee:Andorid APP,监控CPU、内存、网络流量、电池电流和状态(某些设备不受支持)。此外,它还支持自定义收集数据的时间间隔,在浮动窗口中呈现实时进程状态等。
- Soloπ:APP,支付宝在移动端上实现的一套无线化、非侵入、免Root的Android专项测试方案。
- Testin
- Mi:OneAPM针对移动设备上App推出的移动应用性能监控工具
- 听云App:监控真实用户使用过程中的崩溃、错误、卡顿、网络性能差等问题。
2、运营阶段
统计数据:统计移动端版本情况、Android/IOS版本、CPU/GPU版本。
统计工具:友盟、openinstall、matomo、bugly(异常监控)
三、优化手段
程序优化的根本是为达到用户满意的体验,而在(计算)时间与(内存)空间上进行的权衡,这两者是矛盾关系,更多的计算可以换取空间上的减少,更多的空间也可以换取计算量的减少,具体如何权衡就看哪种做法可以更有效地改善体验。
1、内存优化(CPU)
缓存
根据2/8原则,我们要找出游戏中占据内存较多的模块。包含但不限于以下:
- 引擎缓存
- AudioCache
- SpriteFrameCache
- GLProgramStateCache
- TextureCache
- ActionTimelineCache
- AnimationCache
- FontAtlasCache
- GLProgramCache
- GLProgramStateCache
- 游戏(业务)缓存
- 各种文件配置数据、视频数据等等。
如果不加以管理,很可能导致内存浪费,我们可以在此基础上再做一套针对应用层的资源管理模块,参考TextureCache模式,采用引用计数的方式跟踪全部类型的缓存资源,至于在合适何时进行加载和清理,可以再建立一套UI栈(Android Activity),建立一个界面基类,制定好生命周期函数,规范好有序的创建/退出界面,自然地我们也就可以做到有序地加载/卸载资源。
Lua缓存
这部分可以好好研究大做文章,日后再仔细研究。
collectgarbage("setpause", 100 )collectgarbage("setstepmul", 5000)
对象池
有些对象可能会被频繁的create/delete,add/remove,可以利用池机制,避免频繁的内存分配。
示意代码如下:
#include "Singleton.h"// 多多使用C++的模板技术。template<class T>class ObjectPool : public Singleton<ObjectPool<T>>{public:ObjectPool();~ObjectPool();//get a bulletT* getObject();void freeObject(T *obj);private:std::list<T*> objects;const int INITALCAPACITY = 256;};
等等(待添加)
tableView列表缓存,类似Android的ListView,缓存那个出到列表之外的Item,留给即将进入的那个Item使用,防止卡顿。
……
2、显存优化(GPU)
OpenGL对象创建之后一般都常驻于显存之中,需要及时的手动删除。
GLuint objHandle = glCreateCreate(......); // 创建对象。......; // 可能会为这个对象开辟由它管理的内存,并填充数据。glDeleteObject(objHandle); // 删除对象。// ************************************// 常用到的OpenGL对象类型。// ************************************glGenBuffers(...); // Buffer缓冲类型,如VBO、EBOglGenSamplers(...); // Sampler采样器glGenTextures(...); // Texture纹理对象glGenFrameBuffers(...); // 帧缓冲对象glGenVertexArrays(...); // VAOglGenRenderBuffers(...); // 渲染缓冲对象类型glGenProgramPipelines(...); // 管线对象glGenTransformFeedback(...); // TransformFeedback
如果我们自己编写了GL代码,比如CustomCommand自定义绘制,别忘记glDelete*()。
上面我们讲到的引擎中的各种Cache,比如TextureCache缓存Texture2D其实封装了一个GL Texture纹理对象(虽然只是一个GLuint),因此我们只需要管理好Texture2D对象的生命周期,在析构的时候就会glDeleteTexture清除这个纹理对象。
压缩纹理
压缩纹理,指上传至GL显存中的纹理数据就是一种压缩的格式,这种格式的数据可以直接被GPU读取采样(GPU芯片支持的压缩格式),这既可以减少压缩纹理的文件大小,也可以减少它的内存占用和CPU至GPU的带宽占用,所以我们应该大大地使用压缩纹理。
- Android
- ETC1:POT,GLES 2.0及以上支持,不支持Alpha Channel,cocos已有支持。
- ETC2:NPOT,支持Alpha Channel,GLES 3.0及以上支持。
- ASTC:Android 5.0/OpenGLES 3.1及以上支持,市面上98.5%的Android手机支持。
- IOS
- ASTC:iphone 6以上支持。
- PVRTC2:POT,iphone都支持。
注意POT纹理像素宽高必须是2的次幂,美术设计的就要注意,否则会被压缩工具自主拉伸。
字体选择
性能Charmap>BMFont>TTF,打死不用systemFont,每个SystemFont都是一个纹理,内容改变就要生成新的纹理。
- TTF:根据显示字符,动态生成纹理,计算量最大,纹理会随着显示的字符类型数目增大而增大,如果是英文版那就舒服了,字符类型就那么几个,可以就用charmap/bmfont,好处是TTF可以支持各种字符(各种语言、符号)。
- BMFont:折中。
- Charmap:等宽的纹理组成的大纹理,根据偏移量显示指定纹理区域,性能最好,但是字符固定,也没有特效。
字符的特效酌情使用:
- outline,两个drawcall完成文本渲染
- shadow,两个drawcall
- glow,, 一个drawcall
- shadow + outline, 三个drawcall。
- 没有特效时,TTF自动合并drawcall。
等等(待添加)
骨骼动画代替帧数多的帧动画。
多多使用九宫格图。
设置默认纹理转化格式。
// 设置好加载纹理文件时,默认转化成的纹理格式。// 默认是RGBA888,可以考虑RGBA4444Texture2D::setDefaultAlphaPixelFormat(PixelFormat::RGBA4444);enum class PixelFormat{//! auto detect the typeAUTO,//! 32-bit texture: BGRA8888BGRA8888,//! 32-bit texture: RGBA8888RGBA8888,//! 24-bit texture: RGBA888RGB888,//! 16-bit texture without Alpha channelRGB565,//! 8-bit textures used as masksA8,//! 8-bit intensity textureI8,//! 16-bit textures used as masksAI88,//! 16-bit textures: RGBA4444RGBA4444,//! 16-bit textures: RGB5A1RGB5A1,//! 4-bit PVRTC-compressed texture: PVRTC4PVRTC4,//! 4-bit PVRTC-compressed texture: PVRTC4 (has alpha channel)PVRTC4A,//! 2-bit PVRTC-compressed texture: PVRTC2PVRTC2,//! 2-bit PVRTC-compressed texture: PVRTC2 (has alpha channel)PVRTC2A,//! ETC-compressed texture: ETCETC,//! S3TC-compressed texture: S3TC_Dxt1S3TC_DXT1,//! S3TC-compressed texture: S3TC_Dxt3S3TC_DXT3,//! S3TC-compressed texture: S3TC_Dxt5S3TC_DXT5,//! ATITC-compressed texture: ATC_RGBATC_RGB,//! ATITC-compressed texture: ATC_EXPLICIT_ALPHAATC_EXPLICIT_ALPHA,//! ATITC-compressed texture: ATC_INTERPOLATED_ALPHAATC_INTERPOLATED_ALPHA,//! Default texture format: AUTODEFAULT = AUTO,NONE = -1};
3、计算优化(CPU)
draw call
一帧的draw call次数最好在50以内。
看下面一段OpenGL伪代码,理解何为draw call(绘制)以及batch draw call(批绘制)。
// ******************************************// 下面代码为绘制2个Sprite的cocos2dx整体OpenGL伪代码// ******************************************void main(){intGLLib(); // 初始化GL库auto window = glCeateWindow(......); // 创建GL窗口initGLState(); // 初始化OpenGL的初始Statewhile(!window->shouldClose()){ // 一个循环就是一帧......;// ********************// 普通方法绘制两个Sprite// ********************for(......) drawSprite(); // 循环两次,2个draw call// ********************// 批绘制绘制两个Sprite// ********************drawSpriteBatch(); // 一个draw call搞定。......;}}// ******************************************// 绘制一个Sprite// ******************************************void drawCall(){// 第一步、切换至绘制这个Sprite的GL StateglBindBuffer(...); // 绑定VBO、EBO(顶点数据、索引数据)glBindVertexArray(...); // 绑定VAO(shader的attribs)glUseProgram(...); // 设置shaderglUniform(...); // 设置shader的uniforms(矩阵、texture sampler等)glBindTexture(...); // 绑定Texture纹理glBlendFunc(...); // 混合,如果有开启的话// 第二步、执行draw callglDrawElements(GL_TRIANGLES,...); // 顶点数据开始进入渲染管线,绘制Sprite,这就是一个draw call}// ******************************************// 批绘制,一个draw call 绘制两个Sprite// ******************************************void drawCallBatch(){// 第一步、切换至绘制这两个Sprite的GL State(代价高昂的)// 在这个VBO顶点数据中包含了这两个Sprite的顶点数据,EBO根据这个VBO动态生成glBindBuffer(...); // 绑定VBO、EBO(顶点数据、索引数据)// 两个Sprite的shader必须相同,这是批绘制的前提条件(1/3)glBindVertexArray(...); // 绑定VAO(shader的attribs)glUseProgram(...); // 设置shader4// 两个Sprite的纹理是同一张,这是批绘制的前提条件(2/3)// 在顶点数据中已经指定了每个Sprite的采样区域glUniform(...); // 设置shader的uniforms(矩阵、texture sampler等)glBindTexture(...); // 绑定Texture纹理// 两个Sprite的混合必须一样,这是批绘制的前提条件(3/3)glBlendFunc(...); // 混合,如果有开启的话// 第二步、执行draw callglDrawElements(GL_TRIANGLES,...); // 绘制真正绘制Sprite}
GL State的切换是代价高昂的,第二个批绘制少了一次GL State的切换,将明显提高绘制速度。这就是draw call优化(尽量减少每帧的draw call次数),实际上就是draw call的合并。需要注意的是,满足以下条件的两个draw call才能进行合并:
- 这两个draw call必须是先后连续执行的。
- 两个绘制之间的shader必须相同
- 两个绘制之间的texture必须相同
- 两个绘制之间的混合(blending)必须相同
我们要做的draw call优化,就从如何促成这4个条件下手。
将同一图层的图像TP打包成大图(texture相同),根据够用原则,还可以用RGBA4444+抖动代替RGBA8888
将相同类型的Node尽量放一起add(shader相同)
-
I/O操作
一般就指文件加载(纹理、字体、配置、音频、视频),比较耗时。文件加载其实也包含在上面的资源管理方案中:
- 预加载,在进入指定界面前预加载,比如界面的生命周期函数中执行。
- 异步加载(多线程)
- 分帧加载,避免峰值。
这里注意下大图的异步加载:
auto textureCache = TextureCache::getInstance();auto spFrameCache = SpriteFrameCache::getInstance();spFrameCache->addSpriteFramesWithFile(plist); // 这个加载并不是异步加载。textureCache->addImageAsync(plistPNG, [](Texture2D* texture){spFrameCache->addSpriteFramesWithFile(plist, texture); // 这样就可以异步加载。});
visit和draw
Node的visit和draw方法是每帧调用的,见下面GL伪代码:
Scene* runningScene = createMainScene(); // 当前UI树的根节点void main() {while(!window->shouldClose()){ // 一个循环一帧......;runningScene->visit(); // 每帧执行draw......;}}// UI树的遍历是左->根->右的深度遍历顺序。void Scene::visit(){for(...) child->visit(); // localZOrder<0的childthis->draw(); // 绘制本节点for(...) child->visit(); // localZOrder>0的child}
因此当我们要重写这两个方法时,最好将他们的代码优化到极致,在这里面非常值得用空间换时间:
- 避免繁重计算
- CC_USE_CULLING,遮盖剔除,默认是开启的 ,可以考虑关闭。
尽量少破坏UI树的结构,减少UI树的深度
四、其他优化
使用 armabi-v7a构建Android工程,这会有更好的性能表现,因为在此架构下面 Cocos2d-x 会启用 neon指令集,矩阵运算的效率会大大提高。
等等(待添加)。
